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(); + }); }); -}); +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy