From a2528f61bfaa5b28d84c10cb1978cec7c9a86acb Mon Sep 17 00:00:00 2001 From: Dylan Kirkby Date: Sun, 24 Feb 2019 19:00:04 -0800 Subject: [PATCH] feat: cache code parse result between files --- .eslintrc.json | 3 +- packages/typescript-estree/src/parser.ts | 26 +-- .../typescript-estree/src/tsconfig-parser.ts | 187 +++++------------- .../tests/lib/semanticInfo.ts | 51 ++++- 4 files changed, 105 insertions(+), 162 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index e3abc256901a..818b90dfab53 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,7 +25,8 @@ "sourceType": "module", "ecmaFeatures": { "jsx": false - } + }, + "project": "./tsconfig.base.json" }, "overrides": [ { diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 28a0c157a29e..29d95da8425d 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -6,7 +6,6 @@ import semver from 'semver'; import ts from 'typescript'; import convert from './ast-converter'; import { convertError } from './convert'; -import { firstDefined } from './node-utils'; import { TSESTree } from './ts-estree'; import { Extra, ParserOptions, ParserServices } from './parser-options'; import { getFirstSemanticOrSyntacticError } from './semantic-errors'; @@ -65,20 +64,15 @@ function resetExtra(): void { * @param options The config object * @returns If found, returns the source file corresponding to the code and the containing program */ -function getASTFromProject(code: string, options: ParserOptions) { - return firstDefined( - calculateProjectParserOptions( - code, - options.filePath || getFileName(options), - extra, - ), - currentProgram => { - const ast = currentProgram.getSourceFile( - options.filePath || getFileName(options), - ); - return ast && { ast, program: currentProgram }; - }, - ); +function getASTFromProject(options: ParserOptions) { + const filePath = options.filePath || getFileName(options); + for (const program of calculateProjectParserOptions(extra)) { + const ast = program.getSourceFile(filePath); + if (ast !== undefined) { + return { ast, program }; + } + } + return undefined; } /** @@ -162,7 +156,7 @@ function getProgramAndAST( shouldProvideParserServices: boolean, ) { return ( - (shouldProvideParserServices && getASTFromProject(code, options)) || + (shouldProvideParserServices && getASTFromProject(options)) || (shouldProvideParserServices && getASTAndDefaultProject(code, options)) || createNewProgram(code) ); diff --git a/packages/typescript-estree/src/tsconfig-parser.ts b/packages/typescript-estree/src/tsconfig-parser.ts index c136c518c2d6..146fd51c6b38 100644 --- a/packages/typescript-estree/src/tsconfig-parser.ts +++ b/packages/typescript-estree/src/tsconfig-parser.ts @@ -14,28 +14,6 @@ const defaultCompilerOptions: ts.CompilerOptions = { allowJs: true, }; -/** - * Maps tsconfig paths to their corresponding file contents and resulting watches - */ -const knownWatchProgramMap = new Map< - string, - ts.WatchOfConfigFile ->(); - -/** - * Maps file paths to their set of corresponding watch callbacks - * There may be more than one per file if a file is shared between projects - */ -const watchCallbackTrackingMap = new Map(); - -/** - * Holds information about the file currently being linted - */ -const currentLintOperationState = { - code: '', - filePath: '', -}; - /** * Appropriately report issues found when reading a config file * @param diagnostic The diagnostic raised when creating a program @@ -46,7 +24,11 @@ function diagnosticReporter(diagnostic: ts.Diagnostic): void { ); } -const noopFileWatcher = { close: () => {} }; +function getTsconfigPath(tsconfigPath: string, extra: Extra): string { + return path.isAbsolute(tsconfigPath) + ? tsconfigPath + : path.join(extra.tsconfigRootDir || process.cwd(), tsconfigPath); +} /** * Calculate project environments using options provided by consumer and paths from config @@ -56,123 +38,47 @@ const noopFileWatcher = { close: () => {} }; * @param extra.project Provided tsconfig paths * @returns The programs corresponding to the supplied tsconfig paths */ -export function calculateProjectParserOptions( - code: string, - filePath: string, - extra: Extra, -): ts.Program[] { - const results = []; - const tsconfigRootDir = extra.tsconfigRootDir; - - // preserve reference to code and file being linted - currentLintOperationState.code = code; - currentLintOperationState.filePath = filePath; - - // Update file version if necessary - // TODO: only update when necessary, currently marks as changed on every lint - const watchCallback = watchCallbackTrackingMap.get(filePath); - if (typeof watchCallback !== 'undefined') { - watchCallback(filePath, ts.FileWatcherEventKind.Changed); - } +const cache: Map = new Map(); +export function calculateProjectParserOptions(extra: Extra): ts.Program[] { + const results: ts.Program[] = []; + + extra.projects + .map(project => getTsconfigPath(project, extra)) + .forEach(tsconfigPath => { + if (cache.has(tsconfigPath)) { + results.push(cache.get(tsconfigPath) as ts.Program); + return; + } - for (let tsconfigPath of extra.projects) { - // if absolute paths aren't provided, make relative to tsconfigRootDir - if (!path.isAbsolute(tsconfigPath)) { - tsconfigPath = path.join(tsconfigRootDir, tsconfigPath); - } - - const existingWatch = knownWatchProgramMap.get(tsconfigPath); - - if (typeof existingWatch !== 'undefined') { - // get new program (updated if necessary) - results.push(existingWatch.getProgram().getProgram()); - continue; - } - - // create compiler host - const watchCompilerHost = ts.createWatchCompilerHost( - tsconfigPath, - /*optionsToExtend*/ { allowNonTsExtensions: true } as ts.CompilerOptions, - ts.sys, - ts.createSemanticDiagnosticsBuilderProgram, - diagnosticReporter, - /*reportWatchStatus*/ () => {}, - ); - - // ensure readFile reads the code being linted instead of the copy on disk - const oldReadFile = watchCompilerHost.readFile; - watchCompilerHost.readFile = (filePath, encoding) => - path.normalize(filePath) === - path.normalize(currentLintOperationState.filePath) - ? currentLintOperationState.code - : oldReadFile(filePath, encoding); - - // ensure process reports error on failure instead of exiting process immediately - watchCompilerHost.onUnRecoverableConfigFileDiagnostic = diagnosticReporter; - - // ensure process doesn't emit programs - watchCompilerHost.afterProgramCreate = program => { - // report error if there are any errors in the config file - const configFileDiagnostics = program - .getConfigFileParsingDiagnostics() - .filter( - diag => - diag.category === ts.DiagnosticCategory.Error && - diag.code !== 18003, - ); - if (configFileDiagnostics.length > 0) { - diagnosticReporter(configFileDiagnostics[0]); + const config = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + if (config.error !== undefined) { + diagnosticReporter(config.error); } - }; - - // register callbacks to trigger program updates without using fileWatchers - watchCompilerHost.watchFile = (fileName, callback) => { - const normalizedFileName = path.normalize(fileName); - watchCallbackTrackingMap.set(normalizedFileName, callback); - return { - close: () => { - watchCallbackTrackingMap.delete(normalizedFileName); - }, + const parseConfigHost: ts.ParseConfigHost = { + fileExists: ts.sys.fileExists, + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile, + useCaseSensitiveFileNames: true, }; - }; - - // ensure fileWatchers aren't created for directories - watchCompilerHost.watchDirectory = () => noopFileWatcher; - - // allow files with custom extensions to be included in program (uses internal ts api) - const oldOnDirectoryStructureHostCreate = (watchCompilerHost as any) - .onCachedDirectoryStructureHostCreate; - (watchCompilerHost as any).onCachedDirectoryStructureHostCreate = ( - host: any, - ) => { - const oldReadDirectory = host.readDirectory; - host.readDirectory = ( - path: string, - extensions?: ReadonlyArray, - exclude?: ReadonlyArray, - include?: ReadonlyArray, - depth?: number, - ) => - oldReadDirectory( - path, - !extensions - ? undefined - : extensions.concat(extra.extraFileExtensions), - exclude, - include, - depth, - ); - oldOnDirectoryStructureHostCreate(host); - }; - - // create program - const programWatch = ts.createWatchProgram(watchCompilerHost); - const program = programWatch.getProgram().getProgram(); - - // cache watch program and return current program - knownWatchProgramMap.set(tsconfigPath, programWatch); - results.push(program); - } + const parsed = ts.parseJsonConfigFileContent( + config.config, + parseConfigHost, + extra.tsconfigRootDir || path.dirname(tsconfigPath), + { noEmit: true }, + ); + if (parsed.errors !== undefined && parsed.errors.length > 0) { + diagnosticReporter(parsed.errors[0]); + } + const host = ts.createCompilerHost( + { ...defaultCompilerOptions, ...parsed.options }, + true, + ); + const program = ts.createProgram(parsed.fileNames, parsed.options, host); + + cache.set(tsconfigPath, program); + + results.push(program); + }); return results; } @@ -190,12 +96,7 @@ export function createProgram(code: string, filePath: string, extra: Extra) { return undefined; } - let tsconfigPath = extra.projects[0]; - - // if absolute paths aren't provided, make relative to tsconfigRootDir - if (!path.isAbsolute(tsconfigPath)) { - tsconfigPath = path.join(extra.tsconfigRootDir, tsconfigPath); - } + const tsconfigPath = getTsconfigPath(extra.projects[0], extra); const commandLine = ts.getParsedCommandLineOfConfigFile( tsconfigPath, diff --git a/packages/typescript-estree/tests/lib/semanticInfo.ts b/packages/typescript-estree/tests/lib/semanticInfo.ts index ab7df7278a07..c50d56cf02e9 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.ts @@ -48,6 +48,21 @@ describe('semanticInfo', () => { ); }); + it(`should cache the created ts.program`, () => { + const filename = testFiles[0]; + const code = readFileSync(filename, 'utf8'); + const options = createOptions(filename); + const optionsProjectString = { + ...options, + project: './tsconfig.json', + }; + expect( + parseAndGenerateServices(code, optionsProjectString).services.program, + ).toBe( + parseAndGenerateServices(code, optionsProjectString).services.program, + ); + }); + it(`should handle "project": "./tsconfig.json" and "project": ["./tsconfig.json"] the same`, () => { const filename = testFiles[0]; const code = readFileSync(filename, 'utf8'); @@ -65,6 +80,38 @@ describe('semanticInfo', () => { ); }); + it(`should resolve absolute and relative tsconfig paths the same`, () => { + const filename = testFiles[0]; + const code = readFileSync(filename, 'utf8'); + const options = createOptions(filename); + const optionsAbsolutePath = { + ...options, + project: `${__dirname}/../fixtures/semanticInfo/tsconfig.json`, + }; + const optionsRelativePath = { + ...options, + project: `./tsconfig.json`, + }; + const absolutePathResult = parseAndGenerateServices( + code, + optionsAbsolutePath, + ); + const relativePathResult = parseAndGenerateServices( + code, + optionsRelativePath, + ); + if (absolutePathResult.services.program === undefined) { + throw new Error('Unable to create ts.program for absolute tsconfig'); + } else if (relativePathResult.services.program === undefined) { + throw new Error('Unable to create ts.program for relative tsconfig'); + } + expect( + absolutePathResult.services.program.getResolvedProjectReferences(), + ).toEqual( + relativePathResult.services.program.getResolvedProjectReferences(), + ); + }); + // case-specific tests it('isolated-file tests', () => { const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); @@ -190,7 +237,7 @@ describe('semanticInfo', () => { badConfig.project = './tsconfigs.json'; expect(() => parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), - ).toThrow(/File .+tsconfigs\.json' not found/); + ).toThrow(/The specified path does not exist: .+tsconfigs\.json'/); }); it('fail to read project file', () => { @@ -199,7 +246,7 @@ describe('semanticInfo', () => { badConfig.project = '.'; expect(() => parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), - ).toThrow(/File .+semanticInfo' not found/); + ).toThrow(/The specified path does not exist: .+semanticInfo'/); }); it('malformed project file', () => { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy