From 7c4f35d6865e345a4ce8983b045b8a9499f431e8 Mon Sep 17 00:00:00 2001 From: Ben Lichtman Date: Thu, 3 Jun 2021 10:47:38 -0700 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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, + )}`, + ); }); });