From 6fed39692f3469bc4f4cc7516da493852771199a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 15 Oct 2023 10:09:15 -0400 Subject: [PATCH 01/12] fix(typescript-estree): allow project service for unknown client file --- .../typescript-estree/src/useProgramFromProjectService.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 16a7933a671c..6a233e63e319 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -10,15 +10,12 @@ export function useProgramFromProjectService( projectService: server.ProjectService, parseSettings: Readonly, ): ASTAndDefiniteProgram | undefined { - const opened = projectService.openClientFile( + projectService.openClientFile( absolutify(parseSettings.filePath), parseSettings.codeFullText, /* scriptKind */ undefined, parseSettings.tsconfigRootDir, ); - if (!opened.configFileName) { - return undefined; - } const scriptInfo = projectService.getScriptInfo(parseSettings.filePath); const program = projectService From f8e275ded2e158faca4a51326de9d7d35c9e9605 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 15 Oct 2023 11:22:09 -0400 Subject: [PATCH 02/12] Fixed up project throwing tests --- .../tests/lib/semanticInfo.test.ts | 93 ++++++++++++++----- 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 83569479085c..6ba1361b3733 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -335,33 +335,76 @@ describe('semanticInfo', () => { const parseResult = parseAndGenerateServices(code, optionsProjectString); expect(parseResult.services.program).toBe(program1); }); - } - 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 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}`, - ); + it('file not in single provided program instance should throw', () => { + const filename = 'non-existent-file.ts'; + const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + const optionsWithSingleProgram = { + ...options, + programs: [program], + }; + 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, - programs: [program1, program2], - }; - expect(() => - parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms), - ).toThrow( - `The file was not found in any of the provided program instance(s): ${filename}`, - ); - }); + it('file not in multiple provided program instances should throw', () => { + const filename = 'non-existent-file.ts'; + const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + 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, + programs: [program1, program2], + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms), + ).toThrow( + `The file was not found in any of the provided program instance(s): ${filename}`, + ); + }); + } else { + it('file not in single provided program instance should not throw', () => { + const filename = 'non-existent-file.ts'; + const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + const optionsWithSingleProgram = { + ...options, + programs: [program], + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram), + ).not.toThrow(); + }); + + it('file not in multiple provided program instances should not throw', () => { + const filename = 'non-existent-file.ts'; + const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + + const program2 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const optionsWithMultiplePrograms = { + ...options, + programs: [program1, program2], + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms), + ).not.toThrow(); + }); + } it('createProgram fails on non-existent file', () => { expect(() => createProgram('tsconfig.non-existent.json')).toThrow(); From 3d083d651eb37a7a977af1e1fa04638aede19376 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 7 Nov 2023 23:41:53 +0000 Subject: [PATCH 03/12] Added allowDefaultProjectForFiles as nested option --- .../create-program/createProjectService.ts | 27 +++++++++++++++++-- .../src/parseSettings/createParseSettings.ts | 11 +++++--- .../src/parseSettings/index.ts | 6 ++--- .../typescript-estree/src/parser-options.ts | 12 ++++++++- .../src/useProgramFromProjectService.ts | 24 ++++++++++++----- 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 333d221f85b1..6bbd9523f646 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -1,6 +1,10 @@ /* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ +import globby from 'globby'; +import * as path from 'path'; import type * as ts from 'typescript/lib/tsserverlibrary'; +import type { ProjectServiceOptions } from '../parser-options'; + const doNothing = (): void => {}; const createStubFileWatcher = (): ts.FileWatcher => ({ @@ -9,7 +13,15 @@ const createStubFileWatcher = (): ts.FileWatcher => ({ export type TypeScriptProjectService = ts.server.ProjectService; -export function createProjectService(): TypeScriptProjectService { +export interface ProjectServiceSettings { + allowDefaultProjectForFiles: ReadonlySet; + service: TypeScriptProjectService; +} + +export function createProjectService( + options: boolean | ProjectServiceOptions | undefined, + tsconfigRootDir: string, +): ProjectServiceSettings { // We import this lazily to avoid its cost for users who don't use the service const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts; @@ -27,7 +39,7 @@ export function createProjectService(): TypeScriptProjectService { watchFile: createStubFileWatcher, }; - return new tsserver.server.ProjectService({ + const service = new tsserver.server.ProjectService({ host: system, cancellationToken: { isCancellationRequested: (): boolean => false }, useSingleInferredProject: false, @@ -45,5 +57,16 @@ export function createProjectService(): TypeScriptProjectService { }, session: undefined, }); + + return { + allowDefaultProjectForFiles: new Set( + typeof options === 'object' + ? options.allowDefaultProjectForFiles + ?.flatMap(pattern => globby.sync(pattern)) + .map(filePath => path.join(tsconfigRootDir, filePath)) + : undefined, + ), + service, + }; } /* eslint-enable @typescript-eslint/no-empty-function */ diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 3062a6164b32..28684b793aed 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,7 +1,7 @@ import debug from 'debug'; import type * as ts from 'typescript'; -import type { TypeScriptProjectService } from '../create-program/createProjectService'; +import type { ProjectServiceSettings } from '../create-program/createProjectService'; import { createProjectService } from '../create-program/createProjectService'; import { ensureAbsolutePath } from '../create-program/shared'; import type { TSESTreeOptions } from '../parser-options'; @@ -21,7 +21,7 @@ const log = debug( ); let TSCONFIG_MATCH_CACHE: ExpiringCache | null; -let TSSERVER_PROJECT_SERVICE: TypeScriptProjectService | null = null; +let TSSERVER_PROJECT_SERVICE: ProjectServiceSettings | null = null; export function createParseSettings( code: ts.SourceFile | string, @@ -51,11 +51,14 @@ export function createParseSettings( errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: options.errorOnUnknownASTType === true, EXPERIMENTAL_projectService: - (options.EXPERIMENTAL_useProjectService === true && + (options.EXPERIMENTAL_useProjectService && process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'false') || (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' && options.EXPERIMENTAL_useProjectService !== false) - ? (TSSERVER_PROJECT_SERVICE ??= createProjectService()) + ? (TSSERVER_PROJECT_SERVICE ??= createProjectService( + options.EXPERIMENTAL_useProjectService, + tsconfigRootDir, + )) : undefined, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index da093dedfed9..e533088afa0c 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -1,6 +1,6 @@ import type * as ts from 'typescript'; -import type * as tsserverlibrary from 'typescript/lib/tsserverlibrary'; +import type { ProjectServiceSettings } from '../create-program/createProjectService'; import type { CanonicalPath } from '../create-program/shared'; import type { TSESTree } from '../ts-estree'; import type { CacheLike } from './ExpiringCache'; @@ -61,9 +61,7 @@ export interface MutableParseSettings { /** * Experimental: TypeScript server to power program creation. */ - EXPERIMENTAL_projectService: - | tsserverlibrary.server.ProjectService - | undefined; + EXPERIMENTAL_projectService: ProjectServiceSettings | undefined; /** * Whether TS should use the source files for referenced projects instead of the compiled .d.ts files. diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 711ef4bca34b..959b376fa8db 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -88,6 +88,16 @@ interface ParseOptions { suppressDeprecatedPropertyWarnings?: boolean; } +/** + * Granular options to configure the project service. + */ +export interface ProjectServiceOptions { + /** + * Files to allow running with the default inferred project settings. + */ + allowDefaultProjectForFiles?: string[]; +} + interface ParseAndGenerateServicesOptions extends ParseOptions { /** * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. @@ -101,7 +111,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { * * @see https://github.com/typescript-eslint/typescript-eslint/issues/6575 */ - EXPERIMENTAL_useProjectService?: boolean; + EXPERIMENTAL_useProjectService?: boolean | ProjectServiceOptions; /** * ***EXPERIMENTAL FLAG*** - Use this at your own risk. diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 6a233e63e319..2dd5eaeb8a53 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,24 +1,36 @@ import path from 'path'; import type * as ts from 'typescript'; -import type { server } from 'typescript/lib/tsserverlibrary'; import { createProjectProgram } from './create-program/createProjectProgram'; +import type { ProjectServiceSettings } from './create-program/createProjectService'; import { type ASTAndDefiniteProgram } from './create-program/shared'; import type { MutableParseSettings } from './parseSettings'; export function useProgramFromProjectService( - projectService: server.ProjectService, + { allowDefaultProjectForFiles, service }: ProjectServiceSettings, parseSettings: Readonly, ): ASTAndDefiniteProgram | undefined { - projectService.openClientFile( + const opened = service.openClientFile( absolutify(parseSettings.filePath), parseSettings.codeFullText, /* scriptKind */ undefined, parseSettings.tsconfigRootDir, ); - const scriptInfo = projectService.getScriptInfo(parseSettings.filePath); - const program = projectService + if (opened.configFileName) { + if (allowDefaultProjectForFiles.has(parseSettings.filePath)) { + throw new Error( + `${parseSettings.filePath} was included by allowDefaultProjectForFiles but also was found in the project service. Consider removing it from allowDefaultProjectForFiles.`, + ); + } + } else if (!allowDefaultProjectForFiles.has(parseSettings.filePath)) { + throw new Error( + `${parseSettings.filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.`, + ); + } + + const scriptInfo = service.getScriptInfo(parseSettings.filePath); + const program = service .getDefaultProjectForFile(scriptInfo!.fileName, true)! .getLanguageService(/*ensureSynchronized*/ true) .getProgram(); @@ -32,6 +44,6 @@ export function useProgramFromProjectService( function absolutify(filePath: string): string { return path.isAbsolute(filePath) ? filePath - : path.join(projectService.host.getCurrentDirectory(), filePath); + : path.join(service.host.getCurrentDirectory(), filePath); } } From 7130d6c3cf2c1b205c70956ddee6be380b0b9c75 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 12 Nov 2023 20:32:50 -0500 Subject: [PATCH 04/12] Added a bit more testing --- .../tests/lib/createProjectService.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index f6f6d117e3bc..e797f3d9daa1 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -1,7 +1,19 @@ import { createProjectService } from '../../src/create-program/createProjectService'; describe('createProjectService', () => { - it('does not crash', () => { - createProjectService(); + it('sets allowDefaultProjectForFiles to a populated Set when options.allowDefaultProjectForFiles is defined', () => { + const allowDefaultProjectForFiles = ['./*.js']; + const settings = createProjectService( + { allowDefaultProjectForFiles }, + __dirname, + ); + + expect(settings.allowDefaultProjectForFiles.size).toBe(1); + }); + + it('sets allowDefaultProjectForFiles to an empty Set when options.allowDefaultProjectForFiles is not defined', () => { + const settings = createProjectService(undefined, __dirname); + + expect(settings.allowDefaultProjectForFiles.size).toBe(0); }); }); From bf1d15042e916624c434435149e0f55ab1db79f0 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 12 Nov 2023 21:26:18 -0500 Subject: [PATCH 05/12] fix: only throw on missing file path if hasFullTypeInformation --- packages/typescript-estree/src/parser.ts | 1 + .../src/useProgramFromProjectService.ts | 17 +-- .../typescript-estree/tests/lib/parse.test.ts | 2 +- .../tests/lib/semanticInfo.test.ts | 112 +++++++----------- 4 files changed, 57 insertions(+), 75 deletions(-) diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index aeef8d1e45ff..08c29892e220 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -53,6 +53,7 @@ function getProgramAndAST( const fromProjectService = useProgramFromProjectService( parseSettings.EXPERIMENTAL_projectService, parseSettings, + hasFullTypeInformation, ); if (fromProjectService) { return fromProjectService; diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 2dd5eaeb8a53..23b86309efe6 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -9,6 +9,7 @@ import type { MutableParseSettings } from './parseSettings'; export function useProgramFromProjectService( { allowDefaultProjectForFiles, service }: ProjectServiceSettings, parseSettings: Readonly, + hasFullTypeInformation: boolean, ): ASTAndDefiniteProgram | undefined { const opened = service.openClientFile( absolutify(parseSettings.filePath), @@ -17,16 +18,18 @@ export function useProgramFromProjectService( parseSettings.tsconfigRootDir, ); - if (opened.configFileName) { - if (allowDefaultProjectForFiles.has(parseSettings.filePath)) { + if (hasFullTypeInformation) { + if (opened.configFileName) { + if (allowDefaultProjectForFiles.has(parseSettings.filePath)) { + throw new Error( + `${parseSettings.filePath} was included by allowDefaultProjectForFiles but also was found in the project service. Consider removing it from allowDefaultProjectForFiles.`, + ); + } + } else if (!allowDefaultProjectForFiles.has(parseSettings.filePath)) { throw new Error( - `${parseSettings.filePath} was included by allowDefaultProjectForFiles but also was found in the project service. Consider removing it from allowDefaultProjectForFiles.`, + `${parseSettings.filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.`, ); } - } else if (!allowDefaultProjectForFiles.has(parseSettings.filePath)) { - throw new Error( - `${parseSettings.filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.`, - ); } const scriptInfo = service.getScriptInfo(parseSettings.filePath); diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index 95ef77de154e..0bde0a555713 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -505,7 +505,7 @@ describe('parseAndGenerateServices', () => { expect(testParse('ts/notIncluded0j1.ts')) .toThrowErrorMatchingInlineSnapshot(` - "ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: + "ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: - /tsconfig.json - /tsconfig.extra.json However, none of those TSConfigs include this file. Either: diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 53870cd8bbe3..d580249079ab 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -335,76 +335,54 @@ describe('semanticInfo', () => { const parseResult = parseAndGenerateServices(code, optionsProjectString); expect(parseResult.services.program).toBe(program1); }); + } - it('file not in single provided program instance should throw', () => { - const filename = 'non-existent-file.ts'; - const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); - const options = createOptions(filename); - const optionsWithSingleProgram = { - ...options, - programs: [program], - }; - expect(() => - parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram), - ).toThrow( - `The file was not found in any of the provided program instance(s): ${filename}`, - ); - }); - - it('file not in multiple provided program instances should throw', () => { - const filename = 'non-existent-file.ts'; - const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); - const options = createOptions(filename); - 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, - programs: [program1, program2], - }; - expect(() => - parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms), - ).toThrow( - `The file was not found in any of the provided program instance(s): ${filename}`, - ); - }); - } else { - it('file not in single provided program instance should not throw', () => { - const filename = 'non-existent-file.ts'; - const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); - const options = createOptions(filename); - const optionsWithSingleProgram = { - ...options, - programs: [program], - }; - expect(() => - parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram), - ).not.toThrow(); - }); + it('file not in single provided program instance should throw', () => { + const filename = 'non-existent-file.ts'; + const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + const optionsWithSingleProgram = { + ...options, + programs: [program], + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram), + ).toThrow( + process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' + ? `${filename} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.` + : `The file was not found in any of the provided program instance(s): ${filename}`, + ); + }); - it('file not in multiple provided program instances should not throw', () => { - const filename = 'non-existent-file.ts'; - const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); - const options = createOptions(filename); + it('file not in multiple provided program instances should throw a program instance error', () => { + const filename = 'non-existent-file.ts'; + const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + const optionsWithSingleProgram = { + ...options, + programs: [program1], + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram), + ).toThrow( + process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' + ? `${filename} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.` + : `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, - programs: [program1, program2], - }; - expect(() => - parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms), - ).not.toThrow(); - }); - } + const program2 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const optionsWithMultiplePrograms = { + ...options, + programs: [program1, program2], + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms), + ).toThrow( + process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' + ? `${filename} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.` + : `The file was not found in any of the provided program instance(s): ${filename}`, + ); + }); it('createProgram fails on non-existent file', () => { expect(() => createProgram('tsconfig.non-existent.json')).toThrow(); From 0cf53d3ebe80a24b48c8f301317a4cc2598d1eef Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 14 Nov 2023 10:19:08 -0500 Subject: [PATCH 06/12] Fix test snapshot --- packages/typescript-estree/tests/lib/parse.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index 0bde0a555713..95ef77de154e 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -505,7 +505,7 @@ describe('parseAndGenerateServices', () => { expect(testParse('ts/notIncluded0j1.ts')) .toThrowErrorMatchingInlineSnapshot(` - "ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: + "ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: - /tsconfig.json - /tsconfig.extra.json However, none of those TSConfigs include this file. Either: From 4766f7633705ea59f5d9058b1fa6751a935540a1 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 17 Nov 2023 08:37:37 -0500 Subject: [PATCH 07/12] Remove unnecessary assertion --- packages/typescript-estree/src/useProgramFromProjectService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 23b86309efe6..15fc83bdba57 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,5 +1,4 @@ import path from 'path'; -import type * as ts from 'typescript'; import { createProjectProgram } from './create-program/createProjectProgram'; import type { ProjectServiceSettings } from './create-program/createProjectService'; @@ -42,7 +41,7 @@ export function useProgramFromProjectService( return undefined; } - return createProjectProgram(parseSettings, [program as ts.Program]); + return createProjectProgram(parseSettings, [program]); function absolutify(filePath: string): string { return path.isAbsolute(filePath) From e9e475f36efde671253af9ba84c26afe37b326ef Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 6 Dec 2023 09:45:08 -0500 Subject: [PATCH 08/12] TypeScript_ESTree.mdx docs --- docs/packages/TypeScript_ESTree.mdx | 12 +++++++++++- packages/typescript-estree/src/parser-options.ts | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index 610f641d384d..27122374bd18 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -167,7 +167,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { * * @see https://github.com/typescript-eslint/typescript-eslint/issues/6575 */ - EXPERIMENTAL_useProjectService?: boolean; + EXPERIMENTAL_useProjectService?: boolean | ProjectServiceOptions; /** * ***EXPERIMENTAL FLAG*** - Use this at your own risk. @@ -270,6 +270,16 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { }; } +/** + * Granular options to configure the project service. + */ +interface ProjectServiceOptions { + /** + * Globs of files to allow running with the default inferred project settings. + */ + allowDefaultProjectForFiles?: string[]; +} + interface ParserServices { program: ts.Program; esTreeNodeToTSNodeMap: WeakMap; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 2b5833e9c090..e86be6d50996 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -106,7 +106,7 @@ interface ParseOptions { */ export interface ProjectServiceOptions { /** - * Files to allow running with the default inferred project settings. + * Globs of files to allow running with the default inferred project settings. */ allowDefaultProjectForFiles?: string[]; } From 23a32b12eae56bc0670f795355c9a50eb532c727 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 6 Dec 2023 09:49:37 -0500 Subject: [PATCH 09/12] Absolute and canonical file paths --- .../create-program/createProjectService.ts | 5 +++- .../src/useProgramFromProjectService.ts | 28 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 1ffea19ecc1c..0aaf115bfe42 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { ProjectServiceOptions } from '../parser-options'; +import { getCanonicalFileName } from './shared'; const doNothing = (): void => {}; @@ -66,7 +67,9 @@ export function createProjectService( typeof options === 'object' ? options.allowDefaultProjectForFiles ?.flatMap(pattern => globby.sync(pattern)) - .map(filePath => path.join(tsconfigRootDir, filePath)) + .map(filePath => + getCanonicalFileName(path.join(tsconfigRootDir, filePath)), + ) : undefined, ), service, diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 15fc83bdba57..bf427f27c163 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,8 +1,10 @@ -import path from 'path'; - import { createProjectProgram } from './create-program/createProjectProgram'; import type { ProjectServiceSettings } from './create-program/createProjectService'; -import { type ASTAndDefiniteProgram } from './create-program/shared'; +import { + type ASTAndDefiniteProgram, + ensureAbsolutePath, + getCanonicalFileName, +} from './create-program/shared'; import type { MutableParseSettings } from './parseSettings'; export function useProgramFromProjectService( @@ -10,8 +12,10 @@ export function useProgramFromProjectService( parseSettings: Readonly, hasFullTypeInformation: boolean, ): ASTAndDefiniteProgram | undefined { + const filePath = getCanonicalFileName(parseSettings.filePath); + const opened = service.openClientFile( - absolutify(parseSettings.filePath), + ensureAbsolutePath(filePath, service.host.getCurrentDirectory()), parseSettings.codeFullText, /* scriptKind */ undefined, parseSettings.tsconfigRootDir, @@ -19,19 +23,19 @@ export function useProgramFromProjectService( if (hasFullTypeInformation) { if (opened.configFileName) { - if (allowDefaultProjectForFiles.has(parseSettings.filePath)) { + if (allowDefaultProjectForFiles.has(filePath)) { throw new Error( - `${parseSettings.filePath} was included by allowDefaultProjectForFiles but also was found in the project service. Consider removing it from allowDefaultProjectForFiles.`, + `${filePath} was included by allowDefaultProjectForFiles but also was found in the project service. Consider removing it from allowDefaultProjectForFiles.`, ); } - } else if (!allowDefaultProjectForFiles.has(parseSettings.filePath)) { + } else if (!allowDefaultProjectForFiles.has(filePath)) { throw new Error( - `${parseSettings.filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.`, + `${filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.`, ); } } - const scriptInfo = service.getScriptInfo(parseSettings.filePath); + const scriptInfo = service.getScriptInfo(filePath); const program = service .getDefaultProjectForFile(scriptInfo!.fileName, true)! .getLanguageService(/*ensureSynchronized*/ true) @@ -42,10 +46,4 @@ export function useProgramFromProjectService( } return createProjectProgram(parseSettings, [program]); - - function absolutify(filePath: string): string { - return path.isAbsolute(filePath) - ? filePath - : path.join(service.host.getCurrentDirectory(), filePath); - } } From 294a957e01a578b70212717666b47ba1a329f1db Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 24 Dec 2023 00:21:27 -0500 Subject: [PATCH 10/12] Use minimatch instead of fs globbing --- packages/typescript-estree/package.json | 1 + .../create-program/createProjectService.ts | 13 ++----------- .../src/parseSettings/createParseSettings.ts | 1 - .../src/useProgramFromProjectService.ts | 14 ++++++++++++-- .../tests/lib/createProjectService.test.ts | 13 +++++++------ yarn.lock | 19 ++++++++++--------- 6 files changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 2415d15a39b3..47e139f56951 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -57,6 +57,7 @@ "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 0aaf115bfe42..1cf7f9d82992 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -1,10 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ -import globby from 'globby'; -import * as path from 'path'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { ProjectServiceOptions } from '../parser-options'; -import { getCanonicalFileName } from './shared'; const doNothing = (): void => {}; @@ -15,14 +12,13 @@ const createStubFileWatcher = (): ts.FileWatcher => ({ export type TypeScriptProjectService = ts.server.ProjectService; export interface ProjectServiceSettings { - allowDefaultProjectForFiles: ReadonlySet; + allowDefaultProjectForFiles: string[] | undefined; service: TypeScriptProjectService; } export function createProjectService( options: boolean | ProjectServiceOptions | undefined, jsDocParsingMode: ts.JSDocParsingMode | undefined, - tsconfigRootDir: string, ): ProjectServiceSettings { // We import this lazily to avoid its cost for users who don't use the service // TODO: Once we drop support for TS<5.3 we can import from "typescript" directly @@ -63,15 +59,10 @@ export function createProjectService( }); return { - allowDefaultProjectForFiles: new Set( + allowDefaultProjectForFiles: typeof options === 'object' ? options.allowDefaultProjectForFiles - ?.flatMap(pattern => globby.sync(pattern)) - .map(filePath => - getCanonicalFileName(path.join(tsconfigRootDir, filePath)), - ) : undefined, - ), service, }; } diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index b72280747468..a30942aa0d5b 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -87,7 +87,6 @@ export function createParseSettings( ? (TSSERVER_PROJECT_SERVICE ??= createProjectService( options.EXPERIMENTAL_useProjectService, jsDocParsingMode, - tsconfigRootDir, )) : undefined, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index bf427f27c163..2a4e8caf9fd7 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,3 +1,4 @@ +import { minimatch } from 'minimatch'; import { createProjectProgram } from './create-program/createProjectProgram'; import type { ProjectServiceSettings } from './create-program/createProjectService'; import { @@ -23,12 +24,12 @@ export function useProgramFromProjectService( if (hasFullTypeInformation) { if (opened.configFileName) { - if (allowDefaultProjectForFiles.has(filePath)) { + if (filePathMatchedBy(filePath, allowDefaultProjectForFiles)) { throw new Error( `${filePath} was included by allowDefaultProjectForFiles but also was found in the project service. Consider removing it from allowDefaultProjectForFiles.`, ); } - } else if (!allowDefaultProjectForFiles.has(filePath)) { + } else if (!filePathMatchedBy(filePath, allowDefaultProjectForFiles)) { throw new Error( `${filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.`, ); @@ -47,3 +48,12 @@ export function useProgramFromProjectService( return createProjectProgram(parseSettings, [program]); } + +function filePathMatchedBy( + filePath: string, + allowDefaultProjectForFiles: string[] | undefined, +) { + return !!allowDefaultProjectForFiles?.some(pattern => + minimatch(filePath, pattern), + ); +} diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index dc98fb3b8509..9541dcd43942 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -1,20 +1,21 @@ import { createProjectService } from '../../src/create-program/createProjectService'; describe('createProjectService', () => { - it('sets allowDefaultProjectForFiles to a populated Set when options.allowDefaultProjectForFiles is defined', () => { + it('sets allowDefaultProjectForFiles when options.allowDefaultProjectForFiles is defined', () => { const allowDefaultProjectForFiles = ['./*.js']; const settings = createProjectService( { allowDefaultProjectForFiles }, undefined, - __dirname, ); - expect(settings.allowDefaultProjectForFiles.size).toBe(1); + expect(settings.allowDefaultProjectForFiles).toBe( + allowDefaultProjectForFiles, + ); }); - it('sets allowDefaultProjectForFiles to an empty Set when options.allowDefaultProjectForFiles is not defined', () => { - const settings = createProjectService(undefined, undefined, __dirname); + it('does not set allowDefaultProjectForFiles when options.allowDefaultProjectForFiles is not defined', () => { + const settings = createProjectService(undefined, undefined); - expect(settings.allowDefaultProjectForFiles.size).toBe(0); + expect(settings.allowDefaultProjectForFiles).toBeUndefined(); }); }); diff --git a/yarn.lock b/yarn.lock index e74659c009be..6c6cab78689f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6130,6 +6130,7 @@ __metadata: jest: 29.7.0 jest-specific-snapshot: ^8.0.0 make-dir: "*" + minimatch: 9.0.3 prettier: ^3.0.3 rimraf: "*" semver: ^7.5.4 @@ -14845,6 +14846,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:~9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -14881,15 +14891,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:~9.0.3": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" - dependencies: - brace-expansion: ^2.0.1 - checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 - languageName: node - linkType: hard - "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" From 3f1eea1cd71abbc4b4ca2f14ee95438e17563a3f Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 24 Dec 2023 00:22:01 -0500 Subject: [PATCH 11/12] lint: import sorting, missing return type --- packages/typescript-estree/src/useProgramFromProjectService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 2a4e8caf9fd7..d49acd55e095 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,4 +1,5 @@ import { minimatch } from 'minimatch'; + import { createProjectProgram } from './create-program/createProjectProgram'; import type { ProjectServiceSettings } from './create-program/createProjectService'; import { @@ -52,7 +53,7 @@ export function useProgramFromProjectService( function filePathMatchedBy( filePath: string, allowDefaultProjectForFiles: string[] | undefined, -) { +): boolean { return !!allowDefaultProjectForFiles?.some(pattern => minimatch(filePath, pattern), ); From f3700a49774119992b5ace4ecd385c761bbc5326 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 24 Dec 2023 01:19:11 -0500 Subject: [PATCH 12/12] Ignore some tests --- .../eslint-plugin-tslint/tests/index.spec.ts | 126 +++++++++--------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/packages/eslint-plugin-tslint/tests/index.spec.ts b/packages/eslint-plugin-tslint/tests/index.spec.ts index 59f84be30770..bbe479c832b8 100644 --- a/packages/eslint-plugin-tslint/tests/index.spec.ts +++ b/packages/eslint-plugin-tslint/tests/index.spec.ts @@ -166,71 +166,73 @@ ruleTester.run('tslint/config', rule, { ], }); -describe('tslint/error', () => { - function testOutput(code: string, config: ClassicConfig.Config): void { - const linter = new TSESLint.Linter(); - linter.defineRule('tslint/config', rule); - linter.defineParser('@typescript-eslint/parser', parser); - - expect(() => linter.verify(code, config)).toThrow( - 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', - ); - } - - it('should error on missing project', () => { - testOutput('foo;', { - rules: { - 'tslint/config': [2, tslintRulesConfig], - }, - parser: '@typescript-eslint/parser', +if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + describe('tslint/error', () => { + function testOutput(code: string, config: ClassicConfig.Config): void { + const linter = new TSESLint.Linter(); + linter.defineRule('tslint/config', rule); + linter.defineParser('@typescript-eslint/parser', parser); + + expect(() => linter.verify(code, config)).toThrow( + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', + ); + } + + it('should error on missing project', () => { + testOutput('foo;', { + rules: { + 'tslint/config': [2, tslintRulesConfig], + }, + parser: '@typescript-eslint/parser', + }); }); - }); - it('should error on default parser', () => { - testOutput('foo;', { - parserOptions: { - project: TEST_PROJECT_PATH, - }, - rules: { - 'tslint/config': [2, tslintRulesConfig], - }, + it('should error on default parser', () => { + testOutput('foo;', { + parserOptions: { + project: TEST_PROJECT_PATH, + }, + rules: { + 'tslint/config': [2, tslintRulesConfig], + }, + }); }); - }); - it('should not crash if there are no tslint rules specified', () => { - const linter = new TSESLint.Linter(); - jest.spyOn(console, 'warn').mockImplementation(); - linter.defineRule('tslint/config', rule); - linter.defineParser('@typescript-eslint/parser', parser); - - const filePath = path.resolve( - __dirname, - 'fixtures', - 'test-project', - 'extra.ts', - ); - - expect(() => - linter.verify( - 'foo;', - { - parserOptions: { - project: TEST_PROJECT_PATH, - }, - rules: { - 'tslint/config': [2, {}], + it('should not crash if there are no tslint rules specified', () => { + const linter = new TSESLint.Linter(); + jest.spyOn(console, 'warn').mockImplementation(); + linter.defineRule('tslint/config', rule); + linter.defineParser('@typescript-eslint/parser', parser); + + const filePath = path.resolve( + __dirname, + 'fixtures', + 'test-project', + 'extra.ts', + ); + + expect(() => + linter.verify( + 'foo;', + { + parserOptions: { + project: TEST_PROJECT_PATH, + }, + rules: { + 'tslint/config': [2, {}], + }, + parser: '@typescript-eslint/parser', }, - parser: '@typescript-eslint/parser', - }, - filePath, - ), - ).not.toThrow(); - - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining( - `Tried to lint ${filePath} but found no valid, enabled rules for this file type and file path in the resolved configuration.`, - ), - ); - jest.resetAllMocks(); + filePath, + ), + ).not.toThrow(); + + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining( + `Tried to lint ${filePath} but found no valid, enabled rules for this file type and file path in the resolved configuration.`, + ), + ); + jest.resetAllMocks(); + }); }); -}); +}