From 7c4f35d6865e345a4ce8983b045b8a9499f431e8 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Thu, 3 Jun 2021 10:47:38 -0700 Subject: [PATCH 01/15] feat(typescript-estree): allow ts program to be provided in options --- .../create-program/createProjectProgram.ts | 22 +-------- .../src/create-program/shared.ts | 25 ++++++++++ .../src/create-program/useProvidedProgram.ts | 28 +++++++++++ .../typescript-estree/src/parser-options.ts | 7 +++ packages/typescript-estree/src/parser.ts | 48 ++++++++++++------- 5 files changed, 94 insertions(+), 36 deletions(-) create mode 100644 packages/typescript-estree/src/create-program/useProvidedProgram.ts diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index bca6fda1005..69903a5bcaf 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -3,19 +3,12 @@ import path from 'path'; import { getProgramsForProjects } from './createWatchProgram'; import { firstDefined } from '../node-utils'; import { Extra } from '../parser-options'; -import { ASTAndProgram } from './shared'; +import { ASTAndProgram, getAstFromProgram } from './shared'; const log = debug('typescript-eslint:typescript-estree:createProjectProgram'); const DEFAULT_EXTRA_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx']; -function getExtension(fileName: string | undefined): string | null { - if (!fileName) { - return null; - } - return fileName.endsWith('.d.ts') ? '.d.ts' : path.extname(fileName); -} - /** * @param code The code of the file being linted * @param createDefaultProgram True if the default program should be created @@ -31,18 +24,7 @@ function createProjectProgram( const astAndProgram = firstDefined( getProgramsForProjects(code, extra.filePath, extra), - currentProgram => { - const ast = currentProgram.getSourceFile(extra.filePath); - - // working around https://github.com/typescript-eslint/typescript-eslint/issues/1573 - const expectedExt = getExtension(extra.filePath); - const returnedExt = getExtension(ast?.fileName); - if (expectedExt !== returnedExt) { - return; - } - - return ast && { ast, program: currentProgram }; - }, + currentProgram => getAstFromProgram(currentProgram, extra), ); if (!astAndProgram && !createDefaultProgram) { diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index ae296252804..d3375a498d9 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -1,5 +1,6 @@ import path from 'path'; import * as ts from 'typescript'; +import { Program } from 'typescript'; import { Extra } from '../parser-options'; interface ASTAndProgram { @@ -93,6 +94,29 @@ function getScriptKind( } } +function getExtension(fileName: string | undefined): string | null { + if (!fileName) { + return null; + } + return fileName.endsWith('.d.ts') ? '.d.ts' : path.extname(fileName); +} + +function getAstFromProgram( + currentProgram: Program, + extra: Extra, +): ASTAndProgram | undefined { + const ast = currentProgram.getSourceFile(extra.filePath); + + // working around https://github.com/typescript-eslint/typescript-eslint/issues/1573 + const expectedExt = getExtension(extra.filePath); + const returnedExt = getExtension(ast?.fileName); + if (expectedExt !== returnedExt) { + return undefined; + } + + return ast && { ast, program: currentProgram }; +} + export { ASTAndProgram, canonicalDirname, @@ -101,4 +125,5 @@ export { ensureAbsolutePath, getCanonicalFileName, getScriptKind, + getAstFromProgram, }; diff --git a/packages/typescript-estree/src/create-program/useProvidedProgram.ts b/packages/typescript-estree/src/create-program/useProvidedProgram.ts new file mode 100644 index 00000000000..148a9a6d48e --- /dev/null +++ b/packages/typescript-estree/src/create-program/useProvidedProgram.ts @@ -0,0 +1,28 @@ +import debug from 'debug'; +import { Program } from 'typescript'; +import { Extra } from '../parser-options'; +import { ASTAndProgram, getAstFromProgram } from './shared'; + +const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); + +function useProvidedProgram( + programInstance: Program, + extra: Extra, +): ASTAndProgram | undefined { + log('Retrieving ast for %s from provided program instance', extra.filePath); + + const astAndProgram = getAstFromProgram(programInstance, extra); + + if (!astAndProgram) { + const errorLines = [ + '"parserOptions.program" has been provided for @typescript-eslint/parser.', + `The file was not found in the provided program instance: ${extra.filePath}`, + ]; + + throw new Error(errorLines.join('\n')); + } + + return astAndProgram; +} + +export { useProvidedProgram }; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index a3758fffb79..34964452fcc 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -20,6 +20,7 @@ export interface Extra { loc: boolean; log: (message: string) => void; preserveNodeMaps?: boolean; + program: null | Program; projects: CanonicalPath[]; range: boolean; strict: boolean; @@ -169,6 +170,12 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ tsconfigRootDir?: string; + /** + * TypeScript program instance to be used in place of a project built and managed by this library. + * Intended for use by CI scenarios only. + */ + program?: Program; + /** *************************************************************************************** * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index fdbeaa6fa06..068a90bf590 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -18,6 +18,8 @@ import { ensureAbsolutePath, getCanonicalFileName, } from './create-program/shared'; +import { Program } from 'typescript'; +import { useProvidedProgram } from './create-program/useProvidedProgram'; const log = debug('typescript-eslint:typescript-estree:parser'); @@ -61,10 +63,12 @@ function enforceString(code: unknown): string { */ function getProgramAndAST( code: string, + programInstance: Program | null, shouldProvideParserServices: boolean, shouldCreateDefaultProgram: boolean, ): ASTAndProgram { return ( + (programInstance && useProvidedProgram(programInstance, extra)) || (shouldProvideParserServices && createProjectProgram(code, shouldCreateDefaultProgram, extra)) || (shouldProvideParserServices && @@ -105,6 +109,7 @@ function resetExtra(): void { loc: false, log: console.log, // eslint-disable-line no-console preserveNodeMaps: true, + program: null, projects: [], range: false, strict: false, @@ -264,22 +269,32 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { // NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra extra.filePath = ensureAbsolutePath(extra.filePath, extra); - const projectFolderIgnoreList = ( - options.projectFolderIgnoreList ?? ['**/node_modules/**'] - ) - .reduce((acc, folder) => { - if (typeof folder === 'string') { - acc.push(folder); - } - return acc; - }, []) - // prefix with a ! for not match glob - .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); - // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra - extra.projects = prepareAndTransformProjects( - options.project, - projectFolderIgnoreList, - ); + if (typeof options.program === 'object') { + extra.program = options.program; + log( + 'parserOptions.program was provided, so parserOptions.project will be ignored.', + ); + } + + if (extra.program !== null) { + // providing a program overrides project resolution + const projectFolderIgnoreList = ( + options.projectFolderIgnoreList ?? ['**/node_modules/**'] + ) + .reduce((acc, folder) => { + if (typeof folder === 'string') { + acc.push(folder); + } + return acc; + }, []) + // prefix with a ! for not match glob + .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); + // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra + extra.projects = prepareAndTransformProjects( + options.project, + projectFolderIgnoreList, + ); + } if ( Array.isArray(options.extraFileExtensions) && @@ -453,6 +468,7 @@ function parseAndGenerateServices( extra.projects && extra.projects.length > 0; const { ast, program } = getProgramAndAST( code, + extra.program, shouldProvideParserServices, extra.createDefaultProgram, )!; From 4c6aca98e3a4693a2a25e19e6de2abd635cd8792 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Thu, 3 Jun 2021 11:18:05 -0700 Subject: [PATCH 02/15] chore(types): add program option to ParserOptions --- packages/types/package.json | 3 +++ packages/types/src/parser-options.ts | 2 ++ yarn.lock | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/types/package.json b/packages/types/package.json index 7024d906f31..cdb835d738f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -48,5 +48,8 @@ "_ts3.4/*" ] } + }, + "devDependencies": { + "typescript": ">=2.6.0" } } diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index 8f6632df9a4..f3cd8218a27 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -1,4 +1,5 @@ import { Lib } from './lib'; +import { Program } from 'typescript'; type DebugLevel = boolean | ('typescript-eslint' | 'eslint' | 'typescript')[]; @@ -41,6 +42,7 @@ interface ParserOptions { extraFileExtensions?: string[]; filePath?: string; loc?: boolean; + program?: Program; project?: string | string[]; projectFolderIgnoreList?: (string | RegExp)[]; range?: boolean; diff --git a/yarn.lock b/yarn.lock index fde7283b060..61ce7a4514c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8878,7 +8878,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@*, typescript@4.3.2, "typescript@>=3.3.1 <4.4.0", typescript@^4.1.0-dev.20201026, typescript@~4.2.4: +typescript@*, typescript@4.3.2, typescript@>=2.6.0, "typescript@>=3.3.1 <4.4.0", typescript@^4.1.0-dev.20201026, typescript@~4.2.4: version "4.3.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== From f07d72e6a874c1111b7e9705ed3ea7e33e4510d7 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Thu, 3 Jun 2021 12:07:19 -0700 Subject: [PATCH 03/15] test(typescript-estree): add tests for program option --- packages/typescript-estree/package.json | 3 + .../__snapshots__/semanticInfo.test.ts.snap | 5 + .../tests/lib/semanticInfo.test.ts | 106 ++++++++++++++---- 3 files changed, 92 insertions(+), 22 deletions(-) diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 377fee66555..5fd22cf7011 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -64,6 +64,9 @@ "jest-specific-snapshot": "*", "make-dir": "*", "tmp": "^0.2.1", + "typescript": "^4.3.2" + }, + "peerDependencies": { "typescript": "*" }, "peerDependenciesMeta": { diff --git a/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap index 0fe1c5a0c57..2b3168a43f4 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap @@ -1,5 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`semanticInfo file not in provided program instance 1`] = ` +"\\"parserOptions.program\\" has been provided for @typescript-eslint/parser. +The file was not found in the provided program instance: C:\\\\Repos\\\\typescript-eslint\\\\packages\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo\\\\non-existent-file.ts" +`; + exports[`semanticInfo fixtures/export-file.src 1`] = ` Object { "body": Array [ diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 3d8cb41bd25..0c0e5c0cf13 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -1,6 +1,6 @@ -import { readFileSync } from 'fs'; +import * as fs from 'fs'; import glob from 'glob'; -import { extname, join, resolve } from 'path'; +import * as path from 'path'; import * as ts from 'typescript'; import { TSESTreeOptions } from '../../src/parser-options'; import { @@ -30,7 +30,7 @@ function createOptions(fileName: string): TSESTreeOptions & { cwd?: string } { useJSXTextNode: false, errorOnUnknownASTType: true, filePath: fileName, - tsconfigRootDir: join(process.cwd(), FIXTURES_DIR), + tsconfigRootDir: path.join(process.cwd(), FIXTURES_DIR), project: `./tsconfig.json`, loggerFn: false, }; @@ -42,9 +42,9 @@ beforeEach(() => clearCaches()); describe('semanticInfo', () => { // test all AST snapshots testFiles.forEach(filename => { - const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); it( - formatSnapshotName(filename, FIXTURES_DIR, extname(filename)), + formatSnapshotName(filename, FIXTURES_DIR, path.extname(filename)), createSnapshotTestBlock( code, createOptions(filename), @@ -55,7 +55,7 @@ describe('semanticInfo', () => { it(`should cache the created ts.program`, () => { const filename = testFiles[0]; - const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsProjectString = { ...options, @@ -70,7 +70,7 @@ describe('semanticInfo', () => { it(`should handle "project": "./tsconfig.json" and "project": ["./tsconfig.json"] the same`, () => { const filename = testFiles[0]; - const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsProjectString = { ...options, @@ -87,7 +87,7 @@ describe('semanticInfo', () => { it(`should resolve absolute and relative tsconfig paths the same`, () => { const filename = testFiles[0]; - const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsAbsolutePath = { ...options, @@ -119,9 +119,9 @@ describe('semanticInfo', () => { // case-specific tests it('isolated-file tests', () => { - const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const parseResult = parseCodeAndGenerateServices( - readFileSync(fileName, 'utf8'), + fs.readFileSync(fileName, 'utf8'), createOptions(fileName), ); @@ -129,9 +129,9 @@ describe('semanticInfo', () => { }); it('isolated-vue-file tests', () => { - const fileName = resolve(FIXTURES_DIR, 'extra-file-extension.vue'); + const fileName = path.resolve(FIXTURES_DIR, 'extra-file-extension.vue'); const parseResult = parseCodeAndGenerateServices( - readFileSync(fileName, 'utf8'), + fs.readFileSync(fileName, 'utf8'), { ...createOptions(fileName), extraFileExtensions: ['.vue'], @@ -142,9 +142,12 @@ describe('semanticInfo', () => { }); it('non-existent-estree-nodes tests', () => { - const fileName = resolve(FIXTURES_DIR, 'non-existent-estree-nodes.src.ts'); + const fileName = path.resolve( + FIXTURES_DIR, + 'non-existent-estree-nodes.src.ts', + ); const parseResult = parseCodeAndGenerateServices( - readFileSync(fileName, 'utf8'), + fs.readFileSync(fileName, 'utf8'), createOptions(fileName), ); @@ -166,9 +169,9 @@ describe('semanticInfo', () => { }); it('imported-file tests', () => { - const fileName = resolve(FIXTURES_DIR, 'import-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'import-file.src.ts'); const parseResult = parseCodeAndGenerateServices( - readFileSync(fileName, 'utf8'), + fs.readFileSync(fileName, 'utf8'), createOptions(fileName), ); @@ -242,20 +245,26 @@ describe('semanticInfo', () => { }); it('non-existent project file', () => { - const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = './tsconfigs.json'; expect(() => - parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), ).toThrow(/Cannot read file .+tsconfigs\.json'/); }); it('fail to read project file', () => { - const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = '.'; expect(() => - parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), ).toThrow( // case insensitive because unix based systems are case insensitive /Cannot read file .+semanticInfo'./i, @@ -263,11 +272,14 @@ describe('semanticInfo', () => { }); it('malformed project file', () => { - const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = './badTSConfig/tsconfig.json'; expect(() => - parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), ).toThrowErrorMatchingSnapshot(); }); @@ -279,6 +291,33 @@ describe('semanticInfo', () => { expect(parseResult.services.program).toBeDefined(); }); + + it(`provided program instance is returned in result`, () => { + const filename = testFiles[0]; + const program = createTSProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); + const options = createOptions(filename); + const optionsProjectString = { + ...options, + program: program, + project: './tsconfig.json', + }; + const parseResult = parseAndGenerateServices(code, optionsProjectString); + expect(parseResult.services.program).toBe(program); + }); + + it('file not in provided program instance', () => { + const filename = 'non-existent-file.ts'; + const program = createTSProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + const optionsProjectString = { + ...options, + program: program, + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsProjectString), + ).toThrowErrorMatchingSnapshot(); + }); }); function testIsolatedFile( @@ -341,3 +380,26 @@ function checkNumberArrayType(checker: ts.TypeChecker, tsNode: ts.Node): void { expect(typeArguments).toHaveLength(1); expect(typeArguments[0].flags).toBe(ts.TypeFlags.Number); } + +function createTSProgram(configFile: string): ts.Program { + const projectDirectory = path.dirname(configFile); + const config = ts.readConfigFile(configFile, ts.sys.readFile); + expect(config.error).toBeUndefined(); + const parseConfigHost: ts.ParseConfigHost = { + fileExists: fs.existsSync, + readDirectory: ts.sys.readDirectory, + readFile: file => fs.readFileSync(file, 'utf8'), + useCaseSensitiveFileNames: true, + }; + const parsed = ts.parseJsonConfigFileContent( + config.config, + parseConfigHost, + path.resolve(projectDirectory), + { noEmit: true }, + ); + expect(parsed.errors).toHaveLength(0); + const host = ts.createCompilerHost(parsed.options, true); + const program = ts.createProgram(parsed.fileNames, parsed.options, host); + + return program; +} From e4cbd2c3f5bde7dabe56a4e358a1311207439d06 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Thu, 3 Jun 2021 12:18:27 -0700 Subject: [PATCH 04/15] test(parser): add test for program option --- packages/parser/tests/lib/services.ts | 13 +++++++----- packages/parser/tests/tools/test-utils.ts | 26 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/parser/tests/lib/services.ts b/packages/parser/tests/lib/services.ts index 28cada9d251..2280ba73768 100644 --- a/packages/parser/tests/lib/services.ts +++ b/packages/parser/tests/lib/services.ts @@ -4,6 +4,7 @@ import glob from 'glob'; import { ParserOptions } from '../../src/parser'; import { createSnapshotTestBlock, + createTSProgram, formatSnapshotName, testServices, } from '../tools/test-utils'; @@ -30,15 +31,17 @@ function createConfig(filename: string): ParserOptions { //------------------------------------------------------------------------------ describe('services', () => { + const program = createTSProgram(path.resolve(FIXTURES_DIR, 'tsconfig.json')); testFiles.forEach(filename => { const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const config = createConfig(filename); - it( - formatSnapshotName(filename, FIXTURES_DIR, '.ts'), - createSnapshotTestBlock(code, config), - ); - it(`${formatSnapshotName(filename, FIXTURES_DIR, '.ts')} services`, () => { + const snapshotName = formatSnapshotName(filename, FIXTURES_DIR, '.ts'); + it(snapshotName, createSnapshotTestBlock(code, config)); + it(`${snapshotName} services`, () => { testServices(code, config); }); + it(`${snapshotName} services with provided program`, () => { + testServices(code, { ...config, program }); + }); }); }); diff --git a/packages/parser/tests/tools/test-utils.ts b/packages/parser/tests/tools/test-utils.ts index 575ac1dc5b7..712834c8efa 100644 --- a/packages/parser/tests/tools/test-utils.ts +++ b/packages/parser/tests/tools/test-utils.ts @@ -1,4 +1,7 @@ import { TSESTree } from '@typescript-eslint/typescript-estree'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; import * as parser from '../../src/parser'; import { ParserOptions } from '../../src/parser'; @@ -89,3 +92,26 @@ export function formatSnapshotName( .replace(fixturesDir + '/', '') .replace(fileExtension, '')}`; } + +export function createTSProgram(configFile: string): ts.Program { + const projectDirectory = path.dirname(configFile); + const config = ts.readConfigFile(configFile, ts.sys.readFile); + expect(config.error).toBeUndefined(); + const parseConfigHost: ts.ParseConfigHost = { + fileExists: fs.existsSync, + readDirectory: ts.sys.readDirectory, + readFile: file => fs.readFileSync(file, 'utf8'), + useCaseSensitiveFileNames: true, + }; + const parsed = ts.parseJsonConfigFileContent( + config.config, + parseConfigHost, + path.resolve(projectDirectory), + { noEmit: true }, + ); + expect(parsed.errors).toHaveLength(0); + const host = ts.createCompilerHost(parsed.options, true); + const program = ts.createProgram(parsed.fileNames, parsed.options, host); + + return program; +} From 648caaf9747276ab384c7786b6bd0f53776d9f62 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Thu, 3 Jun 2021 12:49:54 -0700 Subject: [PATCH 05/15] docs: document new program option --- packages/parser/README.md | 10 ++++++++++ packages/typescript-estree/README.md | 7 +++++++ packages/typescript-estree/src/parser-options.ts | 5 +++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/parser/README.md b/packages/parser/README.md index 0599f61b236..3d15aa294ce 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -64,6 +64,8 @@ interface ParserOptions { tsconfigRootDir?: string; extraFileExtensions?: string[]; warnOnUnsupportedTypeScriptVersion?: boolean; + + program?: import('typescript').Program; } ``` @@ -211,6 +213,14 @@ Default `false`. This option allows you to request that when the `project` setting is specified, files will be allowed when not included in the projects defined by the provided `tsconfig.json` files. **Using this option will incur significant performance costs. This option is primarily included for backwards-compatibility.** See the **`project`** section above for more information. +### `parserOptions.program` + +Default `undefined`. + +This option allows you to programmatically provide an instance of a TypeScript Program object that will provide type information to rules. +This will override any program that would have been computed from `parserOptions.project` or `parserOptions.createDefaultProgram`. +All linted files must be part of the provided program. + ## Supported TypeScript Version Please see [`typescript-eslint`](https://github.com/typescript-eslint/typescript-eslint) for the supported TypeScript version. diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md index b9d40fb1ec0..33220edf0c3 100644 --- a/packages/typescript-estree/README.md +++ b/packages/typescript-estree/README.md @@ -208,6 +208,13 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ tsconfigRootDir?: string; + /** + * Instance of a TypeScript Program object to be used for type information. + * This overrides any program or programs that would have been computed from the `project` option. + * All linted files must be part of the provided program. + */ + program?: import('typescript').Program; + /** *************************************************************************************** * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 34964452fcc..3915d5945eb 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -171,8 +171,9 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { tsconfigRootDir?: string; /** - * TypeScript program instance to be used in place of a project built and managed by this library. - * Intended for use by CI scenarios only. + * Instance of a TypeScript Program object to be used for type information. + * This overrides any program or programs that would have been computed from the `project` option. + * All linted files must be part of the provided program. */ program?: Program; From defed5d8f9e05022429d4ed9cffe4314286f3a17 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Thu, 3 Jun 2021 14:03:55 -0700 Subject: [PATCH 06/15] fix(typescript-estree): fix condition in option application --- packages/typescript-estree/src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 068a90bf590..8e33f296984 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -276,7 +276,7 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { ); } - if (extra.program !== null) { + if (extra.program === null) { // providing a program overrides project resolution const projectFolderIgnoreList = ( options.projectFolderIgnoreList ?? ['**/node_modules/**'] From b1f621363a7e0a7329daffb452a89f2b7118891a Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Thu, 3 Jun 2021 14:48:51 -0700 Subject: [PATCH 07/15] test(typescript-estree): stop using machine-specific snapshot --- .../tests/lib/__snapshots__/semanticInfo.test.ts.snap | 5 ----- packages/typescript-estree/tests/lib/semanticInfo.test.ts | 7 ++++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap index 2b3168a43f4..0fe1c5a0c57 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap @@ -1,10 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`semanticInfo file not in provided program instance 1`] = ` -"\\"parserOptions.program\\" has been provided for @typescript-eslint/parser. -The file was not found in the provided program instance: C:\\\\Repos\\\\typescript-eslint\\\\packages\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo\\\\non-existent-file.ts" -`; - exports[`semanticInfo fixtures/export-file.src 1`] = ` Object { "body": Array [ diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 0c0e5c0cf13..19fa057e4d6 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -316,7 +316,12 @@ describe('semanticInfo', () => { }; expect(() => parseAndGenerateServices('const foo = 5;', optionsProjectString), - ).toThrowErrorMatchingSnapshot(); + ).toThrow( + `The file was not found in the provided program instance: ${path.resolve( + FIXTURES_DIR, + filename, + )}`, + ); }); }); From 4aa4ee0ccbcf26158f3bdc8744eecc082bb2ed7b Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 3 Jun 2021 16:57:49 -0700 Subject: [PATCH 08/15] refactor: address PR comments --- packages/types/package.json | 2 +- packages/types/src/parser-options.ts | 2 +- packages/typescript-estree/package.json | 3 --- .../src/create-program/useProvidedProgram.ts | 7 ++++++- packages/typescript-estree/src/parser.ts | 4 ++-- yarn.lock | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/types/package.json b/packages/types/package.json index cdb835d738f..aa0ef75349c 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -50,6 +50,6 @@ } }, "devDependencies": { - "typescript": ">=2.6.0" + "typescript": "*" } } diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index f3cd8218a27..a8ecb896726 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -1,5 +1,5 @@ import { Lib } from './lib'; -import { Program } from 'typescript'; +import type { Program } from 'typescript'; type DebugLevel = boolean | ('typescript-eslint' | 'eslint' | 'typescript')[]; diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 5fd22cf7011..377fee66555 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -64,9 +64,6 @@ "jest-specific-snapshot": "*", "make-dir": "*", "tmp": "^0.2.1", - "typescript": "^4.3.2" - }, - "peerDependencies": { "typescript": "*" }, "peerDependenciesMeta": { diff --git a/packages/typescript-estree/src/create-program/useProvidedProgram.ts b/packages/typescript-estree/src/create-program/useProvidedProgram.ts index 148a9a6d48e..e60db3f64ee 100644 --- a/packages/typescript-estree/src/create-program/useProvidedProgram.ts +++ b/packages/typescript-estree/src/create-program/useProvidedProgram.ts @@ -1,4 +1,5 @@ import debug from 'debug'; +import * as path from 'path'; import { Program } from 'typescript'; import { Extra } from '../parser-options'; import { ASTAndProgram, getAstFromProgram } from './shared'; @@ -14,9 +15,13 @@ function useProvidedProgram( const astAndProgram = getAstFromProgram(programInstance, extra); if (!astAndProgram) { + const relativeFilePath = path.relative( + extra.tsconfigRootDir || process.cwd(), + extra.filePath, + ); const errorLines = [ '"parserOptions.program" has been provided for @typescript-eslint/parser.', - `The file was not found in the provided program instance: ${extra.filePath}`, + `The file was not found in the provided program instance: ${relativeFilePath}`, ]; throw new Error(errorLines.join('\n')); diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 8e33f296984..f7c2bdde166 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -269,14 +269,14 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { // NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra extra.filePath = ensureAbsolutePath(extra.filePath, extra); - if (typeof options.program === 'object') { + if (options.program && typeof options.program === 'object') { extra.program = options.program; log( 'parserOptions.program was provided, so parserOptions.project will be ignored.', ); } - if (extra.program === null) { + if (!extra.program) { // providing a program overrides project resolution const projectFolderIgnoreList = ( options.projectFolderIgnoreList ?? ['**/node_modules/**'] diff --git a/yarn.lock b/yarn.lock index 61ce7a4514c..fde7283b060 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8878,7 +8878,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@*, typescript@4.3.2, typescript@>=2.6.0, "typescript@>=3.3.1 <4.4.0", typescript@^4.1.0-dev.20201026, typescript@~4.2.4: +typescript@*, typescript@4.3.2, "typescript@>=3.3.1 <4.4.0", typescript@^4.1.0-dev.20201026, typescript@~4.2.4: version "4.3.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== From ae7005f90df39d193614d8c17b549ddcd774cd7e Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 4 Jun 2021 16:30:43 -0700 Subject: [PATCH 09/15] fix(typescript-estree): ensure correct value for hasFullTypeInformation --- packages/typescript-estree/src/parser.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index f7c2bdde166..a7cea0d771b 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -461,11 +461,10 @@ function parseAndGenerateServices( warnAboutTSVersion(); /** - * Generate a full ts.Program in order to be able to provide parser - * services, such as type-checking + * Generate a full ts.Program or offer provided instance in order to be able to provide parser services, such as type-checking */ const shouldProvideParserServices = - extra.projects && extra.projects.length > 0; + !!extra.program || (extra.projects && extra.projects.length > 0); const { ast, program } = getProgramAndAST( code, extra.program, From 98e515d99c6f66fc12ac25e2dcb742a3f6b8551a Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 4 Jun 2021 16:32:25 -0700 Subject: [PATCH 10/15] feat: provide utility from parser for creating program instance --- packages/parser/src/index.ts | 1 + .../src/create-program/useProvidedProgram.ts | 59 ++++++++++++++++++- packages/typescript-estree/src/index.ts | 1 + .../tests/lib/semanticInfo.test.ts | 5 +- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts index 451f263d62b..c1bb82bf593 100644 --- a/packages/parser/src/index.ts +++ b/packages/parser/src/index.ts @@ -2,6 +2,7 @@ export { parse, parseForESLint, ParserOptions } from './parser'; export { ParserServices, clearCaches, + createProgram, } from '@typescript-eslint/typescript-estree'; // note - cannot migrate this to an import statement because it will make TSC copy the package.json to the dist folder diff --git a/packages/typescript-estree/src/create-program/useProvidedProgram.ts b/packages/typescript-estree/src/create-program/useProvidedProgram.ts index e60db3f64ee..760048b79de 100644 --- a/packages/typescript-estree/src/create-program/useProvidedProgram.ts +++ b/packages/typescript-estree/src/create-program/useProvidedProgram.ts @@ -1,17 +1,20 @@ import debug from 'debug'; +import * as fs from 'fs'; import * as path from 'path'; -import { Program } from 'typescript'; +import * as ts from 'typescript'; import { Extra } from '../parser-options'; import { ASTAndProgram, getAstFromProgram } from './shared'; const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); function useProvidedProgram( - programInstance: Program, + programInstance: ts.Program, extra: Extra, ): ASTAndProgram | undefined { log('Retrieving ast for %s from provided program instance', extra.filePath); + programInstance.getTypeChecker(); // ensure parent pointers are set in source files + const astAndProgram = getAstFromProgram(programInstance, extra); if (!astAndProgram) { @@ -30,4 +33,54 @@ function useProvidedProgram( return astAndProgram; } -export { useProvidedProgram }; +/** + * Utility offered by parser to help consumers construct their own program instance. + */ +function createProgramFromConfigFile( + configFile: string, + projectDirectory: string = path.dirname(configFile), +): ts.Program { + const config = ts.readConfigFile(configFile, ts.sys.readFile); + if (config.error !== undefined) { + throw new Error( + ts.formatDiagnostics([config.error], { + getCanonicalFileName: f => f, + getCurrentDirectory: process.cwd, + getNewLine: () => '\n', + }), + ); + } + const parseConfigHost: ts.ParseConfigHost = { + fileExists: fs.existsSync, + readDirectory: ts.sys.readDirectory, + readFile: file => fs.readFileSync(file, 'utf8'), + useCaseSensitiveFileNames: true, + }; + const parsed = ts.parseJsonConfigFileContent( + config.config, + parseConfigHost, + path.resolve(projectDirectory), + { noEmit: true }, + ); + if (parsed.errors !== undefined) { + // ignore warnings and 'TS18003: No inputs were found in config file ...' + const errors = parsed.errors.filter( + d => d.category === ts.DiagnosticCategory.Error && d.code !== 18003, + ); + if (errors.length !== 0) { + throw new Error( + ts.formatDiagnostics(errors, { + getCanonicalFileName: f => f, + getCurrentDirectory: process.cwd, + getNewLine: () => '\n', + }), + ); + } + } + const host = ts.createCompilerHost(parsed.options, true); + const program = ts.createProgram(parsed.fileNames, parsed.options, host); + + return program; +} + +export { useProvidedProgram, createProgramFromConfigFile }; diff --git a/packages/typescript-estree/src/index.ts b/packages/typescript-estree/src/index.ts index 81f3d69446b..c80cd8f9de5 100644 --- a/packages/typescript-estree/src/index.ts +++ b/packages/typescript-estree/src/index.ts @@ -3,6 +3,7 @@ export { ParserServices, TSESTreeOptions } from './parser-options'; export { simpleTraverse } from './simple-traverse'; export * from './ts-estree'; export { clearCaches } from './create-program/createWatchProgram'; +export { createProgramFromConfigFile as createProgram } from './create-program/useProvidedProgram'; // re-export for backwards-compat export { visitorKeys } from '@typescript-eslint/visitor-keys'; diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 19fa057e4d6..fa0855ca7da 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -317,10 +317,7 @@ describe('semanticInfo', () => { expect(() => parseAndGenerateServices('const foo = 5;', optionsProjectString), ).toThrow( - `The file was not found in the provided program instance: ${path.resolve( - FIXTURES_DIR, - filename, - )}`, + `The file was not found in the provided program instance: ${filename}`, ); }); }); From def60e72b408a5e86942b5bf4529b2f2499f8264 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 4 Jun 2021 16:51:02 -0700 Subject: [PATCH 11/15] test: use createProgram in tests and add tests for it --- packages/parser/tests/lib/services.ts | 4 +- packages/parser/tests/tools/test-utils.ts | 23 ----------- .../src/create-program/useProvidedProgram.ts | 20 ++++------ .../tests/lib/semanticInfo.test.ts | 40 +++++++------------ 4 files changed, 23 insertions(+), 64 deletions(-) diff --git a/packages/parser/tests/lib/services.ts b/packages/parser/tests/lib/services.ts index 2280ba73768..4567f5f85a4 100644 --- a/packages/parser/tests/lib/services.ts +++ b/packages/parser/tests/lib/services.ts @@ -4,10 +4,10 @@ import glob from 'glob'; import { ParserOptions } from '../../src/parser'; import { createSnapshotTestBlock, - createTSProgram, formatSnapshotName, testServices, } from '../tools/test-utils'; +import { createProgram } from '@typescript-eslint/typescript-estree'; //------------------------------------------------------------------------------ // Setup @@ -31,7 +31,7 @@ function createConfig(filename: string): ParserOptions { //------------------------------------------------------------------------------ describe('services', () => { - const program = createTSProgram(path.resolve(FIXTURES_DIR, 'tsconfig.json')); + const program = createProgram(path.resolve(FIXTURES_DIR, 'tsconfig.json')); testFiles.forEach(filename => { const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const config = createConfig(filename); diff --git a/packages/parser/tests/tools/test-utils.ts b/packages/parser/tests/tools/test-utils.ts index 712834c8efa..109d3efcfed 100644 --- a/packages/parser/tests/tools/test-utils.ts +++ b/packages/parser/tests/tools/test-utils.ts @@ -92,26 +92,3 @@ export function formatSnapshotName( .replace(fixturesDir + '/', '') .replace(fileExtension, '')}`; } - -export function createTSProgram(configFile: string): ts.Program { - const projectDirectory = path.dirname(configFile); - const config = ts.readConfigFile(configFile, ts.sys.readFile); - expect(config.error).toBeUndefined(); - const parseConfigHost: ts.ParseConfigHost = { - fileExists: fs.existsSync, - readDirectory: ts.sys.readDirectory, - readFile: file => fs.readFileSync(file, 'utf8'), - useCaseSensitiveFileNames: true, - }; - const parsed = ts.parseJsonConfigFileContent( - config.config, - parseConfigHost, - path.resolve(projectDirectory), - { noEmit: true }, - ); - expect(parsed.errors).toHaveLength(0); - const host = ts.createCompilerHost(parsed.options, true); - const program = ts.createProgram(parsed.fileNames, parsed.options, host); - - return program; -} diff --git a/packages/typescript-estree/src/create-program/useProvidedProgram.ts b/packages/typescript-estree/src/create-program/useProvidedProgram.ts index 760048b79de..41263b88197 100644 --- a/packages/typescript-estree/src/create-program/useProvidedProgram.ts +++ b/packages/typescript-estree/src/create-program/useProvidedProgram.ts @@ -62,20 +62,14 @@ function createProgramFromConfigFile( path.resolve(projectDirectory), { noEmit: true }, ); - if (parsed.errors !== undefined) { - // ignore warnings and 'TS18003: No inputs were found in config file ...' - const errors = parsed.errors.filter( - d => d.category === ts.DiagnosticCategory.Error && d.code !== 18003, + if (parsed.errors.length) { + throw new Error( + ts.formatDiagnostics(parsed.errors, { + getCanonicalFileName: f => f, + getCurrentDirectory: process.cwd, + getNewLine: () => '\n', + }), ); - if (errors.length !== 0) { - throw new Error( - ts.formatDiagnostics(errors, { - getCanonicalFileName: f => f, - getCurrentDirectory: process.cwd, - getNewLine: () => '\n', - }), - ); - } } const host = ts.createCompilerHost(parsed.options, true); const program = ts.createProgram(parsed.fileNames, parsed.options, host); diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index fa0855ca7da..fb6a4cf0e23 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -10,6 +10,7 @@ import { } from '../../tools/test-utils'; import { clearCaches, + createProgram, parseAndGenerateServices, ParseAndGenerateServicesResult, } from '../../src'; @@ -294,7 +295,7 @@ describe('semanticInfo', () => { it(`provided program instance is returned in result`, () => { const filename = testFiles[0]; - const program = createTSProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsProjectString = { @@ -307,8 +308,8 @@ describe('semanticInfo', () => { }); it('file not in provided program instance', () => { - const filename = 'non-existent-file.ts'; - const program = createTSProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const filename = 'non-existant-file.ts'; + const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); const options = createOptions(filename); const optionsProjectString = { ...options, @@ -320,6 +321,16 @@ describe('semanticInfo', () => { `The file was not found in the provided program instance: ${filename}`, ); }); + + it('createProgram fails on non-existant file', () => { + expect(() => createProgram('tsconfig.non-existant.json')).toThrow(); + }); + + it('createProgram fails on tsconfig with errors', () => { + expect(() => + createProgram(path.join(FIXTURES_DIR, 'badTSConfig', 'tsconfig.json')), + ).toThrow(); + }); }); function testIsolatedFile( @@ -382,26 +393,3 @@ function checkNumberArrayType(checker: ts.TypeChecker, tsNode: ts.Node): void { expect(typeArguments).toHaveLength(1); expect(typeArguments[0].flags).toBe(ts.TypeFlags.Number); } - -function createTSProgram(configFile: string): ts.Program { - const projectDirectory = path.dirname(configFile); - const config = ts.readConfigFile(configFile, ts.sys.readFile); - expect(config.error).toBeUndefined(); - const parseConfigHost: ts.ParseConfigHost = { - fileExists: fs.existsSync, - readDirectory: ts.sys.readDirectory, - readFile: file => fs.readFileSync(file, 'utf8'), - useCaseSensitiveFileNames: true, - }; - const parsed = ts.parseJsonConfigFileContent( - config.config, - parseConfigHost, - path.resolve(projectDirectory), - { noEmit: true }, - ); - expect(parsed.errors).toHaveLength(0); - const host = ts.createCompilerHost(parsed.options, true); - const program = ts.createProgram(parsed.fileNames, parsed.options, host); - - return program; -} From 71d74eb681310bd5d06d2fc2afb2a3786ff4e36c Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 4 Jun 2021 17:03:16 -0700 Subject: [PATCH 12/15] docs: document createProgram utility --- packages/parser/README.md | 25 +++++++++++++++++++++++++ packages/typescript-estree/README.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/packages/parser/README.md b/packages/parser/README.md index 3d15aa294ce..433c175bfef 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -221,6 +221,31 @@ This option allows you to programmatically provide an instance of a TypeScript P This will override any program that would have been computed from `parserOptions.project` or `parserOptions.createDefaultProgram`. All linted files must be part of the provided program. +## Utilities + +### `createProgram(configFile, projectDirectory)` + +This serves as a utility method for users of the `parserOptions.program` feature to create a TypeScript program instance from a config file. + +```ts +declare function createProgram( + configFile: string, + projectDirectory?: string, +): import('typescript').Program; +``` + +Example usage in .eslintrc.js: + +```js +const parser = require('@typescript-eslint/parser'); +const program = parser.createProgram('tsconfig.json'); +module.exports = { + parserOptions: { + program, + }, +}; +``` + ## Supported TypeScript Version Please see [`typescript-eslint`](https://github.com/typescript-eslint/typescript-eslint) for the supported TypeScript version. diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md index 33220edf0c3..59c225438c9 100644 --- a/packages/typescript-estree/README.md +++ b/packages/typescript-estree/README.md @@ -310,6 +310,34 @@ Types for the AST produced by the parse functions. - `AST_NODE_TYPES` is an enum which provides the values for every single AST node's `type` property. - `AST_TOKEN_TYPES` is an enum which provides the values for every single AST token's `type` property. +### Utilities + +#### `createProgram(configFile, projectDirectory)` + +This serves as a utility method for users of the `ParseOptions.program` feature to create a TypeScript program instance from a config file. + +```ts +declare function createProgram( + configFile: string, + projectDirectory?: string, +): import('typescript').Program; +``` + +Example usage: + +```js +const tsESTree = require('@typescript-eslint/typescript-estree'); + +const program = tsESTree.createProgram('tsconfig.json'); +const code = `const hello: string = 'world';`; +const { ast, services } = parseAndGenerateServices(code, { + filePath: '/some/path/to/file/foo.ts', + loc: true, + program, + range: true, +}); +``` + ## Supported TypeScript Version See the [Supported TypeScript Version](../../README.md#supported-typescript-version) section in the project root. From 8b0151e8fe1322aca4418a082de4e20deb97e0ee Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Fri, 4 Jun 2021 17:31:39 -0700 Subject: [PATCH 13/15] style: remove unused imports --- packages/parser/tests/tools/test-utils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/parser/tests/tools/test-utils.ts b/packages/parser/tests/tools/test-utils.ts index 109d3efcfed..575ac1dc5b7 100644 --- a/packages/parser/tests/tools/test-utils.ts +++ b/packages/parser/tests/tools/test-utils.ts @@ -1,7 +1,4 @@ import { TSESTree } from '@typescript-eslint/typescript-estree'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as ts from 'typescript'; import * as parser from '../../src/parser'; import { ParserOptions } from '../../src/parser'; From 6361d998dc235cfb97d46f978b971dd4265326b4 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Fri, 4 Jun 2021 18:53:47 -0700 Subject: [PATCH 14/15] test: use consolidated method for program creation --- .../src/create-program/useProvidedProgram.ts | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/packages/typescript-estree/src/create-program/useProvidedProgram.ts b/packages/typescript-estree/src/create-program/useProvidedProgram.ts index 41263b88197..62c720ab712 100644 --- a/packages/typescript-estree/src/create-program/useProvidedProgram.ts +++ b/packages/typescript-estree/src/create-program/useProvidedProgram.ts @@ -38,43 +38,37 @@ function useProvidedProgram( */ function createProgramFromConfigFile( configFile: string, - projectDirectory: string = path.dirname(configFile), + projectDirectory?: string, ): ts.Program { - const config = ts.readConfigFile(configFile, ts.sys.readFile); - if (config.error !== undefined) { - throw new Error( - ts.formatDiagnostics([config.error], { - getCanonicalFileName: f => f, - getCurrentDirectory: process.cwd, - getNewLine: () => '\n', - }), - ); - } - const parseConfigHost: ts.ParseConfigHost = { - fileExists: fs.existsSync, - readDirectory: ts.sys.readDirectory, - readFile: file => fs.readFileSync(file, 'utf8'), - useCaseSensitiveFileNames: true, - }; - const parsed = ts.parseJsonConfigFileContent( - config.config, - parseConfigHost, - path.resolve(projectDirectory), + const parsed = ts.getParsedCommandLineOfConfigFile( + configFile, { noEmit: true }, + { + onUnRecoverableConfigFileDiagnostic: diag => { + throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined. + }, + fileExists: fs.existsSync, + getCurrentDirectory: () => + (projectDirectory && path.resolve(projectDirectory)) || process.cwd(), + readDirectory: ts.sys.readDirectory, + readFile: file => fs.readFileSync(file, 'utf-8'), + useCaseSensitiveFileNames: true, + }, ); - if (parsed.errors.length) { - throw new Error( - ts.formatDiagnostics(parsed.errors, { - getCanonicalFileName: f => f, - getCurrentDirectory: process.cwd, - getNewLine: () => '\n', - }), - ); + const result = parsed!; // parsed is not undefined, since we throw on failure. + if (result.errors.length) { + throw new Error(formatDiagnostics(result.errors)); } - const host = ts.createCompilerHost(parsed.options, true); - const program = ts.createProgram(parsed.fileNames, parsed.options, host); + const host = ts.createCompilerHost(result.options, true); + return ts.createProgram(result.fileNames, result.options, host); +} - return program; +function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { + return ts.formatDiagnostics(diagnostics, { + getCanonicalFileName: f => f, + getCurrentDirectory: process.cwd, + getNewLine: () => '\n', + }); } export { useProvidedProgram, createProgramFromConfigFile }; From c6df39067c39700d10e5461bba9ecbd6ee4d2013 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Mon, 7 Jun 2021 22:44:38 -0700 Subject: [PATCH 15/15] refactor: respond to PR comments --- packages/typescript-estree/README.md | 2 +- .../src/create-program/shared.ts | 22 +++++++++++++------ .../src/create-program/useProvidedProgram.ts | 19 +++++++++++++--- packages/typescript-estree/src/parser.ts | 2 +- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md index 59c225438c9..fca834e2209 100644 --- a/packages/typescript-estree/README.md +++ b/packages/typescript-estree/README.md @@ -319,7 +319,7 @@ This serves as a utility method for users of the `ParseOptions.program` feature ```ts declare function createProgram( configFile: string, - projectDirectory?: string, + projectDirectory: string = process.cwd(), ): import('typescript').Program; ``` diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index d3375a498d9..8202c538d34 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -9,14 +9,11 @@ interface ASTAndProgram { } /** - * Default compiler options for program generation from single root file + * Compiler options required to avoid critical functionality issues */ -const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { - allowNonTsExtensions: true, - allowJs: true, - checkJs: true, - noEmit: true, - // extendedDiagnostics: true, +const CORE_COMPILER_OPTIONS: ts.CompilerOptions = { + noEmit: true, // required to avoid parse from causing emit to occur + /** * Flags required to make no-unused-vars work */ @@ -24,6 +21,16 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { noUnusedParameters: true, }; +/** + * Default compiler options for program generation + */ +const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { + ...CORE_COMPILER_OPTIONS, + allowNonTsExtensions: true, + allowJs: true, + checkJs: true, +}; + function createDefaultCompilerOptionsFromExtra( extra: Extra, ): ts.CompilerOptions { @@ -119,6 +126,7 @@ function getAstFromProgram( export { ASTAndProgram, + CORE_COMPILER_OPTIONS, canonicalDirname, CanonicalPath, createDefaultCompilerOptionsFromExtra, diff --git a/packages/typescript-estree/src/create-program/useProvidedProgram.ts b/packages/typescript-estree/src/create-program/useProvidedProgram.ts index 62c720ab712..16aebbf8f69 100644 --- a/packages/typescript-estree/src/create-program/useProvidedProgram.ts +++ b/packages/typescript-estree/src/create-program/useProvidedProgram.ts @@ -3,7 +3,11 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; import { Extra } from '../parser-options'; -import { ASTAndProgram, getAstFromProgram } from './shared'; +import { + ASTAndProgram, + CORE_COMPILER_OPTIONS, + getAstFromProgram, +} from './shared'; const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); @@ -35,14 +39,23 @@ function useProvidedProgram( /** * Utility offered by parser to help consumers construct their own program instance. + * + * @param configFile the path to the tsconfig.json file, relative to `projectDirectory` + * @param projectDirectory the project directory to use as the CWD, defaults to `process.cwd()` */ function createProgramFromConfigFile( configFile: string, projectDirectory?: string, ): ts.Program { + if (ts.sys === undefined) { + throw new Error( + '`createProgramFromConfigFile` is only supported in a Node-like environment.', + ); + } + const parsed = ts.getParsedCommandLineOfConfigFile( configFile, - { noEmit: true }, + CORE_COMPILER_OPTIONS, { onUnRecoverableConfigFileDiagnostic: diag => { throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined. @@ -52,7 +65,7 @@ function createProgramFromConfigFile( (projectDirectory && path.resolve(projectDirectory)) || process.cwd(), readDirectory: ts.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf-8'), - useCaseSensitiveFileNames: true, + useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, }, ); const result = parsed!; // parsed is not undefined, since we throw on failure. diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index a7cea0d771b..8d6d7df312f 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -464,7 +464,7 @@ function parseAndGenerateServices( * Generate a full ts.Program or offer provided instance in order to be able to provide parser services, such as type-checking */ const shouldProvideParserServices = - !!extra.program || (extra.projects && extra.projects.length > 0); + extra.program != null || (extra.projects && extra.projects.length > 0); const { ast, program } = getProgramAndAST( code, extra.program,