diff --git a/.eslintrc.js b/.eslintrc.js index 4dbde1fb20c..41f25f55671 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,7 @@ module.exports = { ], tsconfigRootDir: __dirname, warnOnUnsupportedTypeScriptVersion: false, - EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, }, rules: { // diff --git a/packages/experimental-utils/src/ast-utils/eslint-utils/ReferenceTracker.ts b/packages/experimental-utils/src/ast-utils/eslint-utils/ReferenceTracker.ts index ddd24e1c029..e0b47222949 100644 --- a/packages/experimental-utils/src/ast-utils/eslint-utils/ReferenceTracker.ts +++ b/packages/experimental-utils/src/ast-utils/eslint-utils/ReferenceTracker.ts @@ -3,12 +3,10 @@ import * as eslintUtils from 'eslint-utils'; import { TSESTree } from '../../ts-estree'; import * as TSESLint from '../../ts-eslint'; -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ const ReferenceTrackerREAD: unique symbol = eslintUtils.ReferenceTracker.READ; const ReferenceTrackerCALL: unique symbol = eslintUtils.ReferenceTracker.CALL; const ReferenceTrackerCONSTRUCT: unique symbol = eslintUtils.ReferenceTracker.CONSTRUCT; -/* eslint-enable @typescript-eslint/no-unsafe-assignment */ interface ReferenceTracker { /** diff --git a/packages/experimental-utils/src/ts-eslint-scope/index.ts b/packages/experimental-utils/src/ts-eslint-scope/index.ts index 7cd1bed4021..870c34fce1b 100644 --- a/packages/experimental-utils/src/ts-eslint-scope/index.ts +++ b/packages/experimental-utils/src/ts-eslint-scope/index.ts @@ -10,5 +10,4 @@ export * from './Scope'; export * from './ScopeManager'; export * from './Variable'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment export const version: string = ESLintVersion; diff --git a/packages/parser/README.md b/packages/parser/README.md index 433c175bfef..04f5c53d0f4 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -213,19 +213,19 @@ 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` +### `parserOptions.programs` 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. +This option allows you to programmatically provide an array of one or more instances of a TypeScript Program object that will provide type information to rules. +This will override any programs that would have been computed from `parserOptions.project` or `parserOptions.createDefaultProgram`. +All linted files must be part of the provided program(s). ## 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. +This serves as a utility method for users of the `parserOptions.programs` feature to create a TypeScript program instance from a config file. ```ts declare function createProgram( @@ -238,10 +238,10 @@ Example usage in .eslintrc.js: ```js const parser = require('@typescript-eslint/parser'); -const program = parser.createProgram('tsconfig.json'); +const programs = [parser.createProgram('tsconfig.json')]; module.exports = { parserOptions: { - program, + programs, }, }; ``` diff --git a/packages/typescript-estree/src/create-program/useProvidedProgram.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts similarity index 75% rename from packages/typescript-estree/src/create-program/useProvidedProgram.ts rename to packages/typescript-estree/src/create-program/useProvidedPrograms.ts index 16aebbf8f69..d807a0f4675 100644 --- a/packages/typescript-estree/src/create-program/useProvidedProgram.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -11,15 +11,23 @@ import { const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); -function useProvidedProgram( - programInstance: ts.Program, +function useProvidedPrograms( + programInstances: 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 + log( + 'Retrieving ast for %s from provided program instance(s)', + extra.filePath, + ); - const astAndProgram = getAstFromProgram(programInstance, extra); + let astAndProgram: ASTAndProgram | undefined; + for (const programInstance of programInstances) { + astAndProgram = getAstFromProgram(programInstance, extra); + // Stop at the first applicable program instance + if (astAndProgram) { + break; + } + } if (!astAndProgram) { const relativeFilePath = path.relative( @@ -27,13 +35,15 @@ function useProvidedProgram( extra.filePath, ); const errorLines = [ - '"parserOptions.program" has been provided for @typescript-eslint/parser.', - `The file was not found in the provided program instance: ${relativeFilePath}`, + '"parserOptions.programs" has been provided for @typescript-eslint/parser.', + `The file was not found in any of the provided program instance(s): ${relativeFilePath}`, ]; throw new Error(errorLines.join('\n')); } + astAndProgram.program.getTypeChecker(); // ensure parent pointers are set in source files + return astAndProgram; } @@ -84,4 +94,4 @@ function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { }); } -export { useProvidedProgram, createProgramFromConfigFile }; +export { useProvidedPrograms, createProgramFromConfigFile }; diff --git a/packages/typescript-estree/src/index.ts b/packages/typescript-estree/src/index.ts index c80cd8f9de5..3345d4d46ce 100644 --- a/packages/typescript-estree/src/index.ts +++ b/packages/typescript-estree/src/index.ts @@ -3,7 +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'; +export { createProgramFromConfigFile as createProgram } from './create-program/useProvidedPrograms'; // re-export for backwards-compat export { visitorKeys } from '@typescript-eslint/visitor-keys'; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 3915d5945eb..9e33627ac9a 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -20,7 +20,7 @@ export interface Extra { loc: boolean; log: (message: string) => void; preserveNodeMaps?: boolean; - program: null | Program; + programs: null | Program[]; projects: CanonicalPath[]; range: boolean; strict: boolean; @@ -171,11 +171,11 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { tsconfigRootDir?: string; /** - * Instance of a TypeScript Program object to be used for type information. + * An array of one or more instances of TypeScript Program objects 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. + * All linted files must be part of the provided program(s). */ - program?: Program; + programs?: Program[]; /** *************************************************************************************** diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 8d6d7df312f..7fe00818959 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -19,7 +19,7 @@ import { getCanonicalFileName, } from './create-program/shared'; import { Program } from 'typescript'; -import { useProvidedProgram } from './create-program/useProvidedProgram'; +import { useProvidedPrograms } from './create-program/useProvidedPrograms'; const log = debug('typescript-eslint:typescript-estree:parser'); @@ -57,18 +57,20 @@ function enforceString(code: unknown): string { /** * @param code The code of the file being linted + * @param programInstances One or more existing programs to use * @param shouldProvideParserServices True if the program should be attempted to be calculated from provided tsconfig files * @param shouldCreateDefaultProgram True if the program should be created from compiler host * @returns Returns a source file and program corresponding to the linted code */ function getProgramAndAST( code: string, - programInstance: Program | null, + programInstances: Program[] | null, shouldProvideParserServices: boolean, shouldCreateDefaultProgram: boolean, ): ASTAndProgram { return ( - (programInstance && useProvidedProgram(programInstance, extra)) || + (programInstances?.length && + useProvidedPrograms(programInstances, extra)) || (shouldProvideParserServices && createProjectProgram(code, shouldCreateDefaultProgram, extra)) || (shouldProvideParserServices && @@ -109,7 +111,7 @@ function resetExtra(): void { loc: false, log: console.log, // eslint-disable-line no-console preserveNodeMaps: true, - program: null, + programs: null, projects: [], range: false, strict: false, @@ -269,14 +271,19 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { // NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra extra.filePath = ensureAbsolutePath(extra.filePath, extra); - if (options.program && typeof options.program === 'object') { - extra.program = options.program; + if (Array.isArray(options.programs)) { + if (!options.programs.length) { + throw new Error( + `You have set parserOptions.programs to an empty array. This will cause all files to not be found in existing programs. Either provide one or more existing TypeScript Program instances in the array, or remove the parserOptions.programs setting.`, + ); + } + extra.programs = options.programs; log( - 'parserOptions.program was provided, so parserOptions.project will be ignored.', + 'parserOptions.programs was provided, so parserOptions.project will be ignored.', ); } - if (!extra.program) { + if (!extra.programs) { // providing a program overrides project resolution const projectFolderIgnoreList = ( options.projectFolderIgnoreList ?? ['**/node_modules/**'] @@ -464,10 +471,10 @@ 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 != null || (extra.projects && extra.projects.length > 0); + extra.programs != null || (extra.projects && extra.projects.length > 0); const { ast, program } = getProgramAndAST( code, - extra.program, + extra.programs, shouldProvideParserServices, extra.createDefaultProgram, )!; diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index fb6a4cf0e23..397fb4ddad4 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -293,37 +293,58 @@ describe('semanticInfo', () => { expect(parseResult.services.program).toBeDefined(); }); - it(`provided program instance is returned in result`, () => { + it('empty programs array should throw', () => { + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const badConfig = createOptions(fileName); + badConfig.programs = []; + expect(() => parseAndGenerateServices('const foo = 5;', badConfig)).toThrow( + 'You have set parserOptions.programs to an empty array. This will cause all files to not be found in existing programs. Either provide one or more existing TypeScript Program instances in the array, or remove the parserOptions.programs setting.', + ); + }); + + it(`first matching provided program instance is returned in result`, () => { const filename = testFiles[0]; - const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const program2 = createProgram(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, + programs: [program1, program2], project: './tsconfig.json', }; const parseResult = parseAndGenerateServices(code, optionsProjectString); - expect(parseResult.services.program).toBe(program); + expect(parseResult.services.program).toBe(program1); }); - it('file not in provided program instance', () => { - const filename = 'non-existant-file.ts'; - const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + it('file not in provided program instance(s)', () => { + const filename = 'non-existent-file.ts'; + const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); const options = createOptions(filename); - const optionsProjectString = { + const optionsWithSingleProgram = { + ...options, + programs: [program1], + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram), + ).toThrow( + `The file was not found in any of the provided program instance(s): ${filename}`, + ); + + const program2 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const optionsWithMultiplePrograms = { ...options, - program: program, + programs: [program1, program2], }; expect(() => - parseAndGenerateServices('const foo = 5;', optionsProjectString), + parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms), ).toThrow( - `The file was not found in the provided program instance: ${filename}`, + `The file was not found in any of the provided program instance(s): ${filename}`, ); }); - it('createProgram fails on non-existant file', () => { - expect(() => createProgram('tsconfig.non-existant.json')).toThrow(); + it('createProgram fails on non-existent file', () => { + expect(() => createProgram('tsconfig.non-existent.json')).toThrow(); }); it('createProgram fails on tsconfig with errors', () => { diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 0cfd463e6b2..0e8da2eba1c 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,6 +1,8 @@ { "compilerOptions": { - "types": ["@types/node"] + "types": ["@types/node"], + "noEmit": true, + "allowJs": true }, "extends": "./tsconfig.base.json", "include": ["tests/**/*.ts", "tools/**/*.ts", ".eslintrc.js"]