From cd780056af888e0d8b7900a4875afed177823d7d Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 20 Dec 2018 15:42:00 -0800 Subject: [PATCH 1/7] feat: add option for extra file exts to include in ts project context --- package.json | 1 + src/node-utils.ts | 7 ++---- src/parser.ts | 32 +++++++++++++++++----------- src/temp-types-based-on-js-source.ts | 2 ++ src/tsconfig-parser.ts | 28 +++++++++++++++++++++++- 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 2199885..737da05 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "build": "tsc", "test": "npm run unit-tests && npm run ast-alignment-tests", "unit-tests": "jest", + "unit-tests-brk": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand", "ast-alignment-tests": "jest --config=./tests/ast-alignment/jest.config.js", "precommit": "npm test && lint-staged", "cz": "git-cz", diff --git a/src/node-utils.ts b/src/node-utils.ts index fe6ef76..4eac746 100644 --- a/src/node-utils.ts +++ b/src/node-utils.ts @@ -205,14 +205,11 @@ function isESTreeClassMember(node: ts.Node): boolean { /** * Checks if a ts.Node has a modifier - * @param {ts.KeywordSyntaxKind} modifierKind TypeScript SyntaxKind modifier + * @param {ts.SyntaxKind} modifierKind TypeScript SyntaxKind modifier * @param {ts.Node} node TypeScript AST node * @returns {boolean} has the modifier specified */ -function hasModifier( - modifierKind: ts.KeywordSyntaxKind, - node: ts.Node -): boolean { +function hasModifier(modifierKind: ts.SyntaxKind, node: ts.Node): boolean { return ( !!node.modifiers && !!node.modifiers.length && diff --git a/src/parser.ts b/src/parser.ts index 41135b8..a2dce93 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -57,7 +57,8 @@ function resetExtra(): void { projects: [], errorOnUnknownASTType: false, code: '', - tsconfigRootDir: process.cwd() + tsconfigRootDir: process.cwd(), + extraFileExtensions: [] }; } @@ -67,19 +68,17 @@ function resetExtra(): void { * @returns {{ast: ts.SourceFile, program: ts.Program} | undefined} If found, returns the source file corresponding to the code and the containing program */ function getASTFromProject(code: string, options: ParserOptions) { - return util.firstDefined( - calculateProjectParserOptions( - code, - options.filePath || getFileName(options), - extra - ), - (currentProgram: ts.Program) => { - const ast = currentProgram.getSourceFile( - options.filePath || getFileName(options) - ); - return ast && { ast, program: currentProgram }; - } + const programs = calculateProjectParserOptions( + code, + options.filePath || getFileName(options), + extra ); + return util.firstDefined(programs, currentProgram => { + const ast = currentProgram.getSourceFile( + options.filePath || getFileName(options) + ); + return ast && { ast, program: currentProgram }; + }); } /** @@ -254,6 +253,13 @@ function generateAST( if (typeof options.tsconfigRootDir === 'string') { extra.tsconfigRootDir = options.tsconfigRootDir; } + + if ( + Array.isArray(options.extraFileExtensions) && + options.extraFileExtensions.every(ext => typeof ext === 'string') + ) { + extra.extraFileExtensions = options.extraFileExtensions; + } } if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) { diff --git a/src/temp-types-based-on-js-source.ts b/src/temp-types-based-on-js-source.ts index 9ce5dae..fdc2337 100644 --- a/src/temp-types-based-on-js-source.ts +++ b/src/temp-types-based-on-js-source.ts @@ -70,6 +70,7 @@ export interface Extra { log: Function; projects: string[]; tsconfigRootDir: string; + extraFileExtensions: string[]; } export interface ParserOptions { @@ -84,4 +85,5 @@ export interface ParserOptions { project?: string | string[]; filePath?: string; tsconfigRootDir?: string; + extraFileExtensions?: string[]; } diff --git a/src/tsconfig-parser.ts b/src/tsconfig-parser.ts index 63d472f..0e11490 100644 --- a/src/tsconfig-parser.ts +++ b/src/tsconfig-parser.ts @@ -90,7 +90,7 @@ export default function calculateProjectParserOptions( // create compiler host const watchCompilerHost = ts.createWatchCompilerHost( tsconfigPath, - /*optionsToExtend*/ undefined, + /*optionsToExtend*/ { allowNonTsExtensions: true } as ts.CompilerOptions, ts.sys, ts.createSemanticDiagnosticsBuilderProgram, diagnosticReporter, @@ -136,6 +136,32 @@ export default function calculateProjectParserOptions( // 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(); From 6d8bdb0b0bd2d04a9c68572d26aed13ce07816e2 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 21 Dec 2018 10:43:22 -0800 Subject: [PATCH 2/7] feat: add fallback single-file program creation --- src/parser.ts | 18 ++++++++++++++- src/tsconfig-parser.ts | 51 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index a2dce93..e4029b6 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -5,7 +5,10 @@ * @copyright jQuery Foundation and other contributors, https://jquery.org/ * MIT License */ -import calculateProjectParserOptions from './tsconfig-parser'; +import { + calculateProjectParserOptions, + createProgram +} from './tsconfig-parser'; import semver from 'semver'; import ts from 'typescript'; import convert from './ast-converter'; @@ -81,6 +84,18 @@ function getASTFromProject(code: string, options: ParserOptions) { }); } +/** + * @param {string} code The code of the file being linted + * @param {Object} options The config object + * @returns {{ast: ts.SourceFile, program: ts.Program} | undefined} If found, returns the source file corresponding to the code and the containing program + */ +function getASTAndDefaultProject(code: string, options: ParserOptions) { + const fileName = options.filePath || getFileName(options); + const program = createProgram(code, fileName, extra); + const ast = program && program.getSourceFile(fileName); + return ast && { ast, program }; +} + /** * @param {string} code The code of the file being linted * @returns {{ast: ts.SourceFile, program: ts.Program}} Returns a new source file and program corresponding to the linted code @@ -153,6 +168,7 @@ function getProgramAndAST( ) { return ( (shouldProvideParserServices && getASTFromProject(code, options)) || + (shouldProvideParserServices && getASTAndDefaultProject(code, options)) || createNewProgram(code) ); } diff --git a/src/tsconfig-parser.ts b/src/tsconfig-parser.ts index 0e11490..9995327 100644 --- a/src/tsconfig-parser.ts +++ b/src/tsconfig-parser.ts @@ -8,6 +8,15 @@ import { Extra } from './temp-types-based-on-js-source'; // Environment calculation //------------------------------------------------------------------------------ +/** + * Default compiler options for program generation from single root file + * @type {ts.CompilerOptions} + */ +const defaultCompilerOptions: ts.CompilerOptions = { + allowNonTsExtensions: true, + allowJs: true +}; + /** * Maps tsconfig paths to their corresponding file contents and resulting watches * @type {Map>} @@ -54,7 +63,7 @@ const noopFileWatcher = { close: () => {} }; * @param {string[]} extra.project Provided tsconfig paths * @returns {ts.Program[]} The programs corresponding to the supplied tsconfig paths */ -export default function calculateProjectParserOptions( +export function calculateProjectParserOptions( code: string, filePath: string, extra: Extra @@ -173,3 +182,43 @@ export default function calculateProjectParserOptions( return results; } + +/** + * Create program from single root file. Requires a single tsconfig to be specified. + * @param code The code being linted + * @param filePath The file being linted + * @param {string} extra.tsconfigRootDir The root directory for relative tsconfig paths + * @param {string[]} extra.project Provided tsconfig paths + * @returns {ts.Program} The program containing just the file being linted and associated library files + */ +export function createProgram(code: string, filePath: string, extra: Extra) { + if (!extra.projects || extra.projects.length !== 1) { + 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 commandLine = ts.getParsedCommandLineOfConfigFile( + tsconfigPath, + defaultCompilerOptions, + { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} } + ); + + if (!commandLine) { + return undefined; + } + + const compilerHost = ts.createCompilerHost(commandLine.options); + const oldReadFile = compilerHost.readFile; + compilerHost.readFile = (fileName: string) => + path.normalize(fileName) === path.normalize(filePath) + ? code + : oldReadFile(fileName); + + return ts.createProgram([filePath], commandLine.options, compilerHost); +} From 62913e4a03f9a367d01f10c330e0b1688f8a818a Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 21 Dec 2018 11:12:56 -0800 Subject: [PATCH 3/7] test: add tests --- .../semanticInfo/extra-file-extension.vue | 1 + tests/lib/semanticInfo.ts | 118 ++++++++++++------ 2 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 tests/fixtures/semanticInfo/extra-file-extension.vue diff --git a/tests/fixtures/semanticInfo/extra-file-extension.vue b/tests/fixtures/semanticInfo/extra-file-extension.vue new file mode 100644 index 0000000..ca04667 --- /dev/null +++ b/tests/fixtures/semanticInfo/extra-file-extension.vue @@ -0,0 +1 @@ +const x = [3, 4, 5]; \ No newline at end of file diff --git a/tests/lib/semanticInfo.ts b/tests/lib/semanticInfo.ts index d0c1f37..5e5d472 100644 --- a/tests/lib/semanticInfo.ts +++ b/tests/lib/semanticInfo.ts @@ -17,6 +17,7 @@ import { } from '../../tools/test-utils'; import ts from 'typescript'; import { ParserOptions } from '../../src/temp-types-based-on-js-source'; +import { Node, Program } from '../../src/estree/spec'; //------------------------------------------------------------------------------ // Setup @@ -77,48 +78,17 @@ describe('semanticInfo', () => { createOptions(fileName) ); - // get type checker - expect(parseResult).toHaveProperty('services.program.getTypeChecker'); - const checker = parseResult.services.program!.getTypeChecker(); - - // get number node (ast shape validated by snapshot) - const arrayMember = (parseResult.ast as any).body[0].declarations[0].init - .elements[0]; - expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap'); - - // get corresponding TS node - const tsArrayMember = parseResult.services.esTreeNodeToTSNodeMap!.get( - arrayMember - ); - expect(tsArrayMember).toBeDefined(); - expect(tsArrayMember.kind).toBe(ts.SyntaxKind.NumericLiteral); - expect(tsArrayMember.text).toBe('3'); - - // get type of TS node - const arrayMemberType: any = checker.getTypeAtLocation(tsArrayMember); - expect(arrayMemberType.flags).toBe(ts.TypeFlags.NumberLiteral); - expect(arrayMemberType.value).toBe(3); - - // make sure it maps back to original ESTree node - expect(parseResult).toHaveProperty('services.tsNodeToESTreeNodeMap'); - expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsArrayMember)).toBe( - arrayMember - ); - - // get bound name - const boundName = (parseResult.ast as any).body[0].declarations[0].id; - expect(boundName.name).toBe('x'); - - const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get( - boundName - ); - expect(tsBoundName).toBeDefined(); + testIsolatedFile(parseResult); + }); - checkNumberArrayType(checker, tsBoundName); + test('isolated-vue-file tests', () => { + const fileName = path.resolve(FIXTURES_DIR, 'extra-file-extension.vue'); + const parseResult = parseCodeAndGenerateServices(shelljs.cat(fileName), { + ...createOptions(fileName), + extraFileExtensions: ['.vue'] + }); - expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe( - boundName - ); + testIsolatedFile(parseResult); }); test('imported-file tests', () => { @@ -150,6 +120,32 @@ describe('semanticInfo', () => { ).toBe(arrayBoundName); }); + test('non-existent file tests', () => { + const parseResult = parseCodeAndGenerateServices( + `const x = [parseInt("5")];`, + createOptions('') + ); + + // get type checker + expect(parseResult).toHaveProperty('services.program.getTypeChecker'); + const checker = parseResult.services.program!.getTypeChecker(); + + // get bound name + const boundName = (parseResult.ast as any).body[0].declarations[0].id; + expect(boundName.name).toBe('x'); + + const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get( + boundName + ); + expect(tsBoundName).toBeDefined(); + + checkNumberArrayType(checker, tsBoundName); + + expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe( + boundName + ); + }); + test('non-existent project file', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); @@ -178,6 +174,48 @@ describe('semanticInfo', () => { }); }); +function testIsolatedFile(parseResult: any) { + // get type checker + expect(parseResult).toHaveProperty('services.program.getTypeChecker'); + const checker = parseResult.services.program!.getTypeChecker(); + + // get number node (ast shape validated by snapshot) + const arrayMember = (parseResult.ast as any).body[0].declarations[0].init + .elements[0]; + expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap'); + + // get corresponding TS node + const tsArrayMember = parseResult.services.esTreeNodeToTSNodeMap!.get( + arrayMember + ); + expect(tsArrayMember).toBeDefined(); + expect(tsArrayMember.kind).toBe(ts.SyntaxKind.NumericLiteral); + expect((tsArrayMember as ts.NumericLiteral).text).toBe('3'); + + // get type of TS node + const arrayMemberType: any = checker.getTypeAtLocation(tsArrayMember); + expect(arrayMemberType.flags).toBe(ts.TypeFlags.NumberLiteral); + expect(arrayMemberType.value).toBe(3); + + // make sure it maps back to original ESTree node + expect(parseResult).toHaveProperty('services.tsNodeToESTreeNodeMap'); + expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsArrayMember)).toBe( + arrayMember + ); + + // get bound name + const boundName = (parseResult.ast as any).body[0].declarations[0].id; + expect(boundName.name).toBe('x'); + const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get( + boundName + ); + expect(tsBoundName).toBeDefined(); + checkNumberArrayType(checker, tsBoundName); + expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe( + boundName + ); +} + /** * Verifies that the type of a TS node is number[] as expected * @param {ts.TypeChecker} checker From e3fe4061d2bcca1c64183a50100395e1c92027ef Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 21 Dec 2018 11:19:40 -0800 Subject: [PATCH 4/7] revert: remove personal script from package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 737da05..2199885 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "build": "tsc", "test": "npm run unit-tests && npm run ast-alignment-tests", "unit-tests": "jest", - "unit-tests-brk": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand", "ast-alignment-tests": "jest --config=./tests/ast-alignment/jest.config.js", "precommit": "npm test && lint-staged", "cz": "git-cz", From 6734d34cd44cf975803cf3e9f9898a75ecbad04c Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 21 Dec 2018 11:21:23 -0800 Subject: [PATCH 5/7] revert: un-extract local for project results --- src/parser.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index e4029b6..64d6c88 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -71,17 +71,19 @@ function resetExtra(): void { * @returns {{ast: ts.SourceFile, program: ts.Program} | undefined} If found, returns the source file corresponding to the code and the containing program */ function getASTFromProject(code: string, options: ParserOptions) { - const programs = calculateProjectParserOptions( - code, - options.filePath || getFileName(options), - extra + return util.firstDefined( + calculateProjectParserOptions( + code, + options.filePath || getFileName(options), + extra + ), + currentProgram => { + const ast = currentProgram.getSourceFile( + options.filePath || getFileName(options) + ); + return ast && { ast, program: currentProgram }; + } ); - return util.firstDefined(programs, currentProgram => { - const ast = currentProgram.getSourceFile( - options.filePath || getFileName(options) - ); - return ast && { ast, program: currentProgram }; - }); } /** From cb5d590ab2cfda15591541a0e53a2f46026e44b3 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 21 Dec 2018 11:27:18 -0800 Subject: [PATCH 6/7] revert: remove unnecessary type import --- tests/lib/semanticInfo.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/lib/semanticInfo.ts b/tests/lib/semanticInfo.ts index 5e5d472..6abda98 100644 --- a/tests/lib/semanticInfo.ts +++ b/tests/lib/semanticInfo.ts @@ -17,7 +17,6 @@ import { } from '../../tools/test-utils'; import ts from 'typescript'; import { ParserOptions } from '../../src/temp-types-based-on-js-source'; -import { Node, Program } from '../../src/estree/spec'; //------------------------------------------------------------------------------ // Setup From 9af389a077697c2729a91f4231dad23b61f3fc1e Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 21 Dec 2018 13:17:56 -0800 Subject: [PATCH 7/7] revert: undo type widening --- src/node-utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/node-utils.ts b/src/node-utils.ts index 87bb446..bc35214 100644 --- a/src/node-utils.ts +++ b/src/node-utils.ts @@ -205,11 +205,14 @@ function isESTreeClassMember(node: ts.Node): boolean { /** * Checks if a ts.Node has a modifier - * @param {ts.SyntaxKind} modifierKind TypeScript SyntaxKind modifier + * @param {ts.KeywordSyntaxKind} modifierKind TypeScript SyntaxKind modifier * @param {ts.Node} node TypeScript AST node * @returns {boolean} has the modifier specified */ -function hasModifier(modifierKind: ts.SyntaxKind, node: ts.Node): boolean { +function hasModifier( + modifierKind: ts.KeywordSyntaxKind, + node: ts.Node +): boolean { return ( !!node.modifiers && !!node.modifiers.length &&