From b1662e2af7196f76392be0d7d9a8617ed1732ef0 Mon Sep 17 00:00:00 2001 From: Victor Lin Date: Tue, 30 Jan 2024 13:10:07 -0800 Subject: [PATCH 1/6] speed up non-type-aware linting with project service --- .../src/parseSettings/createParseSettings.ts | 1 + .../src/parseSettings/index.ts | 5 ++ packages/typescript-estree/src/parser.ts | 3 +- .../src/useProgramFromProjectService.ts | 53 +++++++++++++++---- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index a30942aa0d5b..686595edf278 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -127,6 +127,7 @@ export function createParseSettings( DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS, )), tsconfigRootDir, + typeAware: !!options.programs || !!options.project, }; // debug doesn't support multiple `enable` calls, so have to do it all at once diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 6aca7a863aae..6f39d6a17b91 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -154,6 +154,11 @@ export interface MutableParseSettings { * The absolute path to the root directory for all provided `project`s. */ tsconfigRootDir: string; + + /** + * Whether this parse is type-aware + */ + typeAware: boolean; } export type ParseSettings = Readonly; diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 08c29892e220..c5c6d4524248 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -207,8 +207,7 @@ function parseAndGenerateServices( /** * Generate a full ts.Program or offer provided instances in order to be able to provide parser services, such as type-checking */ - const hasFullTypeInformation = - parseSettings.programs != null || parseSettings.projects.length > 0; + const hasFullTypeInformation = parseSettings.typeAware; if ( typeof options.errorOnTypeScriptSyntacticAndSemanticIssues === 'boolean' && diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index d49acd55e095..4582af761541 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -2,28 +2,47 @@ import { minimatch } from 'minimatch'; import { createProjectProgram } from './create-program/createProjectProgram'; import type { ProjectServiceSettings } from './create-program/createProjectService'; +import { createNoProgram } from './create-program/createSourceFile'; +import type { + ASTAndDefiniteProgram, + ASTAndNoProgram, + ASTAndProgram, +} from './create-program/shared'; import { - type ASTAndDefiniteProgram, ensureAbsolutePath, getCanonicalFileName, } from './create-program/shared'; import type { MutableParseSettings } from './parseSettings'; +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: boolean, +): ASTAndProgram | undefined; +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: true, +): ASTAndDefiniteProgram | undefined; +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: false, +): ASTAndNoProgram | undefined; export function useProgramFromProjectService( { allowDefaultProjectForFiles, service }: ProjectServiceSettings, parseSettings: Readonly, hasFullTypeInformation: boolean, -): ASTAndDefiniteProgram | undefined { +): ASTAndDefiniteProgram | ASTAndNoProgram | undefined { const filePath = getCanonicalFileName(parseSettings.filePath); - const opened = service.openClientFile( - ensureAbsolutePath(filePath, service.host.getCurrentDirectory()), - parseSettings.codeFullText, - /* scriptKind */ undefined, - parseSettings.tsconfigRootDir, - ); - if (hasFullTypeInformation) { + const opened = service.openClientFile( + ensureAbsolutePath(filePath, service.host.getCurrentDirectory()), + parseSettings.codeFullText, + /* scriptKind */ undefined, + parseSettings.tsconfigRootDir, + ); if (opened.configFileName) { if (filePathMatchedBy(filePath, allowDefaultProjectForFiles)) { throw new Error( @@ -35,6 +54,22 @@ export function useProgramFromProjectService( `${filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.`, ); } + } else { + // Type-aware linting is disabled for this file. However, type aware lint rules might still rely on the contents of this file. + // We should check if the project service knows about this file, and open it (to inform it of changes) + // This ensures that: + // - if the file is not part of a project, we don't waste time creating a program (fast non-type-aware linting) + // - otherwise, we refresh the file in the project service (moderately fast, since the project is already loaded) + if (service.getScriptInfo(filePath)) { + service.openClientFile( + ensureAbsolutePath(filePath, service.host.getCurrentDirectory()), + parseSettings.codeFullText, + /* scriptKind */ undefined, + parseSettings.tsconfigRootDir, + ); + } + // don't bother creating a program + return createNoProgram(parseSettings); } const scriptInfo = service.getScriptInfo(filePath); From 06075cc38adce5a440d4c355561703575b8fce51 Mon Sep 17 00:00:00 2001 From: Victor Lin Date: Tue, 30 Jan 2024 13:56:23 -0800 Subject: [PATCH 2/6] fix ts error --- packages/website/src/components/linter/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index 8262487369c1..8bea715c95ad 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -32,6 +32,7 @@ export const defaultParseSettings: ParseSettings = { tokens: [], tsconfigMatchCache: new Map(), tsconfigRootDir: '/', + typeAware: false, }; export const defaultEslintConfig: ClassicConfig.Config = { From dd18a1d7660ed0256ea77b2b4b643151b64b40c2 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 19 Jul 2024 18:22:53 -0400 Subject: [PATCH 3/6] fix: account for the default project --- .../create-program/createProjectProgram.ts | 4 +- .../src/parseSettings/createParseSettings.ts | 1 - .../src/parseSettings/index.ts | 5 - packages/typescript-estree/src/parser.ts | 8 +- .../src/useProgramFromProjectService.ts | 251 +++++++++++------- .../typescript-estree/tests/lib/parse.test.ts | 29 +- .../lib/useProgramFromProjectService.test.ts | 69 ++++- 7 files changed, 229 insertions(+), 138 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index af4e6378972e..4a9fc7824115 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -52,10 +52,10 @@ function createProjectProgram( ); const describedPrograms = relativeProjects.length === 1 - ? relativeProjects[0] + ? ` ${relativeProjects[0]}` : `\n${relativeProjects.map(project => `- ${project}`).join('\n')}`; const errorLines = [ - `ESLint was configured to run on \`${describedFilePath}\` using \`parserOptions.project\`: ${describedPrograms}`, + `ESLint was configured to run on \`${describedFilePath}\` using \`parserOptions.project\`:${describedPrograms}`, ]; let hasMatchedAnError = false; diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 5f902201d520..98e42d8af36c 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -125,7 +125,6 @@ export function createParseSettings( DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS, )), tsconfigRootDir, - typeAware: !!tsestreeOptions.programs || !!tsestreeOptions.project, }; // debug doesn't support multiple `enable` calls, so have to do it all at once diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index dd18cd28a4b7..41a21c044946 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -143,11 +143,6 @@ export interface MutableParseSettings { * The absolute path to the root directory for all provided `project`s. */ tsconfigRootDir: string; - - /** - * Whether this parse is type-aware - */ - typeAware: boolean; } export type ParseSettings = Readonly; diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index c54b62af75fc..45e5a0a92b87 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -195,10 +195,10 @@ function parseAndGenerateServices( }; } - /** - * Generate a full ts.Program or offer provided instances in order to be able to provide parser services, such as type-checking - */ - const hasFullTypeInformation = parseSettings.typeAware; + const hasFullTypeInformation = + parseSettings.programs != null || + parseSettings.projects.size > 0 || + !!parseSettings.projectService; if ( typeof tsestreeOptions.errorOnTypeScriptSyntacticAndSemanticIssues === diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 8e1e5b0b4c79..fdb438930df1 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -47,107 +47,107 @@ const updateExtraFileExtensions = ( } }; -export function useProgramFromProjectService( - settings: ProjectServiceSettings, - parseSettings: Readonly, - hasFullTypeInformation: boolean, +function openClientFileFromProjectService( defaultProjectMatchedFiles: Set, -): ASTAndProgram | undefined; -export function useProgramFromProjectService( - settings: ProjectServiceSettings, + isDefaultProjectAllowed: boolean, + filePathAbsolute: string, parseSettings: Readonly, - hasFullTypeInformation: true, - defaultProjectMatchedFiles: Set, -): ASTAndDefiniteProgram | undefined; -export function useProgramFromProjectService( - settings: ProjectServiceSettings, - parseSettings: Readonly, - hasFullTypeInformation: false, - defaultProjectMatchedFiles: Set, -): ASTAndNoProgram | undefined; -export function useProgramFromProjectService( - { - allowDefaultProject, - maximumDefaultProjectFileMatchCount, - service, - }: ProjectServiceSettings, - parseSettings: Readonly, - hasFullTypeInformation: boolean, - defaultProjectMatchedFiles: Set, -): ASTAndProgram | undefined { - // NOTE: triggers a full project reload when changes are detected - updateExtraFileExtensions(service, parseSettings.extraFileExtensions); - - // We don't canonicalize the filename because it caused a performance regression. - // See https://github.com/typescript-eslint/typescript-eslint/issues/8519 - const filePathAbsolute = absolutify(parseSettings.filePath); - log( - 'Opening project service file for: %s at absolute path %s', - parseSettings.filePath, - filePathAbsolute, - ); - - // Type-aware linting is disabled for this file. However, type aware lint rules might still rely on the contents of this file. - // We should check if the project service knows about this file, and open it (to inform it of changes) - // This ensures that: - // - if the file is not part of a project, we don't waste time creating a program (fast non-type-aware linting) - // - otherwise, we refresh the file in the project service (moderately fast, since the project is already loaded) - // TODO: account for the default project (defaultProjectMatchedFile) - if (!hasFullTypeInformation) { - if (service.getScriptInfo(filePathAbsolute)) { - service.openClientFile( - filePathAbsolute, - parseSettings.codeFullText, - /* scriptKind */ undefined, - parseSettings.tsconfigRootDir, - ); - } - - // don't bother creating a program - return createNoProgram(parseSettings); - } - - const opened = service.openClientFile( + serviceSettings: ProjectServiceSettings, +): ts.server.OpenConfiguredProjectResult { + const opened = serviceSettings.service.openClientFile( filePathAbsolute, parseSettings.codeFullText, /* scriptKind */ undefined, parseSettings.tsconfigRootDir, ); - log('Opened project service file: %o', opened); - log( 'Project service type information enabled; checking for file path match on: %o', - allowDefaultProject, - ); - const isDefaultProjectAllowedPath = filePathMatchedBy( - parseSettings.filePath, - allowDefaultProject, + serviceSettings.allowDefaultProject, ); log( 'Default project allowed path: %s, based on config file: %s', - isDefaultProjectAllowedPath, + isDefaultProjectAllowed, opened.configFileName, ); if (opened.configFileName) { - if (isDefaultProjectAllowedPath) { + if (isDefaultProjectAllowed) { throw new Error( `${parseSettings.filePath} was included by allowDefaultProject but also was found in the project service. Consider removing it from allowDefaultProject.`, ); } - } else if (!isDefaultProjectAllowedPath) { + } else if (!isDefaultProjectAllowed) { throw new Error( `${parseSettings.filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.`, ); } + // No a configFileName indicates this file wasn't included in a TSConfig. + // That means it must get its type information from the default project. + if (!opened.configFileName) { + defaultProjectMatchedFiles.add(filePathAbsolute); + if ( + defaultProjectMatchedFiles.size > + serviceSettings.maximumDefaultProjectFileMatchCount + ) { + const filePrintLimit = 20; + const filesToPrint = Array.from(defaultProjectMatchedFiles).slice( + 0, + filePrintLimit, + ); + const truncatedFileCount = + defaultProjectMatchedFiles.size - filesToPrint.length; + + throw new Error( + `Too many files (>${serviceSettings.maximumDefaultProjectFileMatchCount}) have matched the default project.${DEFAULT_PROJECT_FILES_ERROR_EXPLANATION} +Matching files: +${filesToPrint.map(file => `- ${file}`).join('\n')} +${truncatedFileCount ? `...and ${truncatedFileCount} more files\n` : ''} +If you absolutely need more files included, set parserOptions.projectService.maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING to a larger value. +`, + ); + } + } + + return opened; +} + +function createNoProgramWithProjectService( + filePathAbsolute: string, + parseSettings: Readonly, + service: ts.server.ProjectService, +): ASTAndNoProgram { + log('No project service information available. Creating no program.'); + + // If the project service knows about this file, this informs if of changes. + // Doing so ensures that: + // - if the file is not part of a project, we don't waste time creating a program (fast non-type-aware linting) + // - otherwise, we refresh the file in the project service (moderately fast, since the project is already loaded) + if (service.getScriptInfo(filePathAbsolute)) { + log('Script info available. Opening client file in project service.'); + service.openClientFile( + filePathAbsolute, + parseSettings.codeFullText, + /* scriptKind */ undefined, + parseSettings.tsconfigRootDir, + ); + } + + return createNoProgram(parseSettings); +} + +function retrieveASTAndProgramFor( + filePathAbsolute: string, + parseSettings: Readonly, + serviceSettings: ProjectServiceSettings, +): ASTAndDefiniteProgram | undefined { log('Retrieving script info and then program for: %s', filePathAbsolute); - const scriptInfo = service.getScriptInfo(filePathAbsolute); + const scriptInfo = serviceSettings.service.getScriptInfo(filePathAbsolute); /* eslint-disable @typescript-eslint/no-non-null-assertion */ - const program = service + const program = serviceSettings.service .getDefaultProjectForFile(scriptInfo!.fileName, true)! .getLanguageService(/*ensureSynchronized*/ true) .getProgram(); @@ -158,37 +158,96 @@ export function useProgramFromProjectService( return undefined; } - if (!opened.configFileName) { - defaultProjectMatchedFiles.add(filePathAbsolute); - } - if (defaultProjectMatchedFiles.size > maximumDefaultProjectFileMatchCount) { - const filePrintLimit = 20; - const filesToPrint = Array.from(defaultProjectMatchedFiles).slice( - 0, - filePrintLimit, - ); - const truncatedFileCount = - defaultProjectMatchedFiles.size - filesToPrint.length; + log('Found project service program for: %s', filePathAbsolute); - throw new Error( - `Too many files (>${maximumDefaultProjectFileMatchCount}) have matched the default project.${DEFAULT_PROJECT_FILES_ERROR_EXPLANATION} -Matching files: -${filesToPrint.map(file => `- ${file}`).join('\n')} -${truncatedFileCount ? `...and ${truncatedFileCount} more files\n` : ''} -If you absolutely need more files included, set parserOptions.projectService.maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING to a larger value. -`, + return createProjectProgram(parseSettings, [program]); +} + +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: boolean, + defaultProjectMatchedFiles: Set, +): ASTAndProgram | undefined; +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: true, + defaultProjectMatchedFiles: Set, +): ASTAndDefiniteProgram | undefined; +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: false, + defaultProjectMatchedFiles: Set, +): ASTAndNoProgram | undefined; +export function useProgramFromProjectService( + serviceSettings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: boolean, + defaultProjectMatchedFiles: Set, +): ASTAndProgram | undefined { + // NOTE: triggers a full project reload when changes are detected + updateExtraFileExtensions( + serviceSettings.service, + parseSettings.extraFileExtensions, + ); + + // We don't canonicalize the filename because it caused a performance regression. + // See https://github.com/typescript-eslint/typescript-eslint/issues/8519 + const filePathAbsolute = absolutify(parseSettings.filePath, serviceSettings); + log( + 'Opening project service file for: %s at absolute path %s', + parseSettings.filePath, + filePathAbsolute, + ); + + const isDefaultProjectAllowed = filePathMatchedBy( + parseSettings.filePath, + serviceSettings.allowDefaultProject, + ); + + // Type-aware linting is disabled for this file. + // However, type-aware lint rules might still rely on its contents. + if (!hasFullTypeInformation && !isDefaultProjectAllowed) { + return createNoProgramWithProjectService( + filePathAbsolute, + parseSettings, + serviceSettings.service, ); } - log('Found project service program for: %s', filePathAbsolute); + // If type info was requested, we attempt to open it in the project service. + // By now, the file is known to be one of: + // - in the project service (valid configuration) + // - allowlisted in the default project (valid configuration) + // - neither, which openClientFileFromProjectService will throw an error for + const opened = + hasFullTypeInformation && + openClientFileFromProjectService( + defaultProjectMatchedFiles, + isDefaultProjectAllowed, + filePathAbsolute, + parseSettings, + serviceSettings, + ); - return createProjectProgram(parseSettings, [program]); + log('Opened project service file: %o', opened); - function absolutify(filePath: string): string { - return path.isAbsolute(filePath) - ? filePath - : path.join(service.host.getCurrentDirectory(), filePath); - } + return retrieveASTAndProgramFor( + filePathAbsolute, + parseSettings, + serviceSettings, + ); +} + +function absolutify( + filePath: string, + serviceSettings: ProjectServiceSettings, +): string { + return path.isAbsolute(filePath) + ? filePath + : path.join(serviceSettings.service.host.getCurrentDirectory(), filePath); } function filePathMatchedBy( diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index 0b976af5b7a8..1be10f4d5eca 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -369,17 +369,12 @@ describe('parseAndGenerateServices', () => { }; const testExtraFileExtensions = (filePath: string, extraFileExtensions: string[]) => (): void => { - const result = parser.parseAndGenerateServices(code, { + parser.parseAndGenerateServices(code, { ...config, extraFileExtensions, filePath: join(PROJECT_DIR, filePath), projectService: true, }); - const compilerOptions = result.services.program?.getCompilerOptions(); - - if (!compilerOptions?.configFilePath) { - throw new Error('No config file found, using inferred project'); - } }; describe('project includes', () => { @@ -505,35 +500,31 @@ describe('parseAndGenerateServices', () => { it("the file isn't included", () => { expect( testExtraFileExtensions('other/notIncluded.vue', ['.vue']), - ).toThrowErrorMatchingInlineSnapshot( - `"No config file found, using inferred project"`, - ); + ).toThrow(/notIncluded\.vue was not found by the project service/); }); it('duplicate extension', () => { expect( testExtraFileExtensions('ts/notIncluded.ts', ['.ts']), - ).toThrowErrorMatchingInlineSnapshot( - `"No config file found, using inferred project"`, - ); + ).toThrow(/notIncluded\.ts was not found by the project service/); }); }); - it('invalid extension', () => { + it('extension matching the file name but not a file on disk', () => { expect( testExtraFileExtensions('other/unknownFileType.unknown', [ 'unknown', ]), - ).toThrowErrorMatchingInlineSnapshot( - `"No config file found, using inferred project"`, + ).toThrow( + /unknownFileType\.unknown was not found by the project service/, ); }); - it('the extension does not match', () => { + it('the extension does not match the file name', () => { expect( testExtraFileExtensions('other/unknownFileType.unknown', ['.vue']), - ).toThrowErrorMatchingInlineSnapshot( - `"No config file found, using inferred project"`, + ).toThrow( + /unknownFileType\.unknown was not found by the project service/, ); }); }); @@ -566,7 +557,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/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index 03940dda400e..528f91bbc35a 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type -- Fancy mocks */ import path from 'path'; -import { ScriptKind } from 'typescript'; +import * as ts from 'typescript'; import type { ProjectServiceSettings, @@ -9,6 +9,14 @@ import type { import type { ParseSettings } from '../../src/parseSettings'; import { useProgramFromProjectService } from '../../src/useProgramFromProjectService'; +const mockCreateNoProgram = jest.fn(); + +jest.mock('../../src/create-program/createSourceFile', () => ({ + get createNoProgram() { + return mockCreateNoProgram; + }, +})); + const mockCreateProjectProgram = jest.fn(); jest.mock('../../src/create-program/createProjectProgram', () => ({ @@ -44,8 +52,10 @@ function createMockProjectService() { }; } +const mockFileName = 'camelCaseFile.ts'; + const mockParseSettings = { - filePath: 'path/PascalCaseDirectory/camelCaseFile.ts', + filePath: `path/PascalCaseDirectory/${mockFileName}`, extraFileExtensions: [] as readonly string[], } as ParseSettings; @@ -58,7 +68,44 @@ const createProjectServiceSettings = < ...settings, }); +// if hasFullTypeInformation is true: +// do openClientFileFromProjectService +// ✅ did use the isDefaultProjectAllowed boolean - in openClientFileFromProjectService, to make sure not double-included + +// if hasFullTypeInformation is false and isDefaultProjectAllowed is true: +// don't openClientFileFromProjectService +// ✅ did use the isDefaultProjectAllowed boolean - in the if (!... && !...) { createNoProgramWithProjectService check +// do + +// if hasFullTypeInformation is false and isDefaultProjectAllowed is false: +// ✅ did use the isDefaultProjectAllowed boolean - in the if (!... && !...) { createNoProgramWithProjectService check +// do createNoProgramWithProjectService +// - which still opens the file if service.getScriptInfo knows of it + describe('useProgramFromProjectService', () => { + it('creates a standalone AST with no program when hasFullTypeInformation is false', () => { + const { service } = createMockProjectService(); + + const stubASTAndNoProgram = { + ast: ts.createSourceFile(mockFileName, '', ts.ScriptTarget.Latest), + program: null, + }; + + mockCreateNoProgram.mockReturnValueOnce(stubASTAndNoProgram); + + const actual = useProgramFromProjectService( + createProjectServiceSettings({ + allowDefaultProject: undefined, + service, + }), + mockParseSettings, + false, + new Set(), + ); + + expect(actual).toBe(stubASTAndNoProgram); + }); + it('passes an absolute, case-matching file path to service.openClientFile', () => { const { service } = createMockProjectService(); @@ -123,7 +170,7 @@ describe('useProgramFromProjectService', () => { ); }); - it('throws an error when called more than the maximum allowed file count', () => { + it('throws an error when more than the maximum allowed file count is matched to the default project', () => { const { service } = createMockProjectService(); const program = { getSourceFile: jest.fn() }; @@ -255,7 +302,7 @@ If you absolutely need more files included, set parserOptions.projectService.max expect(actual).toBe(program); }); - it('returns a created program when hasFullTypeInformation is disabled, the file is neither in the project service nor allowDefaultProject, and the service has a matching program', () => { + it('returns undefined when hasFullTypeInformation is disabled, the file is neither in the project service nor allowDefaultProject, and the service has a matching program', () => { const { service } = createMockProjectService(); const program = { getSourceFile: jest.fn() }; @@ -274,10 +321,10 @@ If you absolutely need more files included, set parserOptions.projectService.max new Set(), ); - expect(actual).toBe(program); + expect(actual).toBeUndefined(); }); - it('returns a created program when hasFullTypeInformation is disabled, the file is in the project service, the service has a matching program, and no out-of-project files are allowed', () => { + it('returns undefined when hasFullTypeInformation is disabled, the file is in the project service, the service has a matching program, and no out', () => { const { service } = createMockProjectService(); const program = { getSourceFile: jest.fn() }; @@ -299,7 +346,7 @@ If you absolutely need more files included, set parserOptions.projectService.max new Set(), ); - expect(actual).toBe(program); + expect(actual).toBeUndefined(); }); it('does not call setHostConfiguration on the service with default extensions if extraFileExtensions are not provided', () => { @@ -358,7 +405,7 @@ If you absolutely need more files included, set parserOptions.projectService.max { extension: '.vue', isMixedContent: false, - scriptKind: ScriptKind.Deferred, + scriptKind: ts.ScriptKind.Deferred, }, ], }); @@ -387,7 +434,7 @@ If you absolutely need more files included, set parserOptions.projectService.max { extension: '.vue', isMixedContent: false, - scriptKind: ScriptKind.Deferred, + scriptKind: ts.ScriptKind.Deferred, }, ], }); @@ -427,7 +474,7 @@ If you absolutely need more files included, set parserOptions.projectService.max { extension: '.vue', isMixedContent: false, - scriptKind: ScriptKind.Deferred, + scriptKind: ts.ScriptKind.Deferred, }, ], }); @@ -471,7 +518,7 @@ If you absolutely need more files included, set parserOptions.projectService.max { extension: '.vue', isMixedContent: false, - scriptKind: ScriptKind.Deferred, + scriptKind: ts.ScriptKind.Deferred, }, ], }); From 6d5ceb0128610db62b13b292392058c22420aac0 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 19 Jul 2024 18:23:08 -0400 Subject: [PATCH 4/6] fix: account for the default project in website --- packages/website/src/components/linter/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index 0582d8820b16..b1fa526b9d97 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -30,7 +30,6 @@ export const defaultParseSettings: ParseSettings = { tokens: [], tsconfigMatchCache: new Map(), tsconfigRootDir: '/', - typeAware: false, }; export const defaultEslintConfig: ClassicConfig.Config = { From eadbfb5ac8f3ca3c5273c4761edf69c714d46981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Fri, 19 Jul 2024 18:30:46 -0400 Subject: [PATCH 5/6] Apply suggestions from code review --- .../lib/useProgramFromProjectService.test.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index 528f91bbc35a..967b82013d08 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -68,22 +68,9 @@ const createProjectServiceSettings = < ...settings, }); -// if hasFullTypeInformation is true: -// do openClientFileFromProjectService -// ✅ did use the isDefaultProjectAllowed boolean - in openClientFileFromProjectService, to make sure not double-included - -// if hasFullTypeInformation is false and isDefaultProjectAllowed is true: -// don't openClientFileFromProjectService -// ✅ did use the isDefaultProjectAllowed boolean - in the if (!... && !...) { createNoProgramWithProjectService check -// do - -// if hasFullTypeInformation is false and isDefaultProjectAllowed is false: -// ✅ did use the isDefaultProjectAllowed boolean - in the if (!... && !...) { createNoProgramWithProjectService check -// do createNoProgramWithProjectService -// - which still opens the file if service.getScriptInfo knows of it describe('useProgramFromProjectService', () => { - it('creates a standalone AST with no program when hasFullTypeInformation is false', () => { + it('creates a standalone AST with no program when hasFullTypeInformation is false and allowDefaultProject is falsy', () => { const { service } = createMockProjectService(); const stubASTAndNoProgram = { From 80dbb380d08cf2079216af4f8718df12def60993 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 19 Jul 2024 18:31:23 -0400 Subject: [PATCH 6/6] chore: formatting --- .../tests/lib/useProgramFromProjectService.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index 967b82013d08..b3a8e4e4e4e4 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -68,7 +68,6 @@ const createProjectServiceSettings = < ...settings, }); - describe('useProgramFromProjectService', () => { it('creates a standalone AST with no program when hasFullTypeInformation is false and allowDefaultProject is falsy', () => { const { service } = createMockProjectService();