-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat: speed up non-type-aware linting with project service #8322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b1662e2
06075cc
667fd5b
dd18a1d
6d5ceb0
eadbfb5
80dbb38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -195,11 +195,10 @@ function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>( | |
}; | ||
} | ||
|
||
/** | ||
* 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.size > 0; | ||
parseSettings.programs != null || | ||
parseSettings.projects.size > 0 || | ||
!!parseSettings.projectService; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yepitschunked following up on |
||
|
||
if ( | ||
typeof tsestreeOptions.errorOnTypeScriptSyntacticAndSemanticIssues === | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,12 @@ import * as ts from 'typescript'; | |
|
||
import { createProjectProgram } from './create-program/createProjectProgram'; | ||
import type { ProjectServiceSettings } from './create-program/createProjectService'; | ||
import type { ASTAndDefiniteProgram } from './create-program/shared'; | ||
import { createNoProgram } from './create-program/createSourceFile'; | ||
import type { | ||
ASTAndDefiniteProgram, | ||
ASTAndNoProgram, | ||
ASTAndProgram, | ||
} from './create-program/shared'; | ||
import { DEFAULT_PROJECT_FILES_ERROR_EXPLANATION } from './create-program/validateDefaultProjectForFilesGlob'; | ||
import type { MutableParseSettings } from './parseSettings'; | ||
|
||
|
@@ -42,70 +47,107 @@ const updateExtraFileExtensions = ( | |
} | ||
}; | ||
|
||
export function useProgramFromProjectService( | ||
{ | ||
allowDefaultProject, | ||
maximumDefaultProjectFileMatchCount, | ||
service, | ||
}: ProjectServiceSettings, | ||
parseSettings: Readonly<MutableParseSettings>, | ||
hasFullTypeInformation: boolean, | ||
function openClientFileFromProjectService( | ||
defaultProjectMatchedFiles: Set<string>, | ||
): ASTAndDefiniteProgram | 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, | ||
); | ||
|
||
const opened = service.openClientFile( | ||
isDefaultProjectAllowed: boolean, | ||
filePathAbsolute: string, | ||
parseSettings: Readonly<MutableParseSettings>, | ||
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', | ||
serviceSettings.allowDefaultProject, | ||
); | ||
|
||
if (hasFullTypeInformation) { | ||
log( | ||
'Project service type information enabled; checking for file path match on: %o', | ||
allowDefaultProject, | ||
); | ||
const isDefaultProjectAllowedPath = filePathMatchedBy( | ||
parseSettings.filePath, | ||
allowDefaultProject, | ||
); | ||
log( | ||
'Default project allowed path: %s, based on config file: %s', | ||
isDefaultProjectAllowed, | ||
opened.configFileName, | ||
); | ||
|
||
log( | ||
'Default project allowed path: %s, based on config file: %s', | ||
isDefaultProjectAllowedPath, | ||
opened.configFileName, | ||
if (opened.configFileName) { | ||
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 (!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; | ||
|
||
if (opened.configFileName) { | ||
if (isDefaultProjectAllowedPath) { | ||
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) { | ||
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.`, | ||
`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. | ||
`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that I've moved this "Too many files ..." notice to inside the |
||
); | ||
} | ||
} | ||
|
||
return opened; | ||
} | ||
|
||
function createNoProgramWithProjectService( | ||
filePathAbsolute: string, | ||
parseSettings: Readonly<MutableParseSettings>, | ||
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<MutableParseSettings>, | ||
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(); | ||
|
@@ -116,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<MutableParseSettings>, | ||
hasFullTypeInformation: boolean, | ||
defaultProjectMatchedFiles: Set<string>, | ||
): ASTAndProgram | undefined; | ||
export function useProgramFromProjectService( | ||
settings: ProjectServiceSettings, | ||
parseSettings: Readonly<MutableParseSettings>, | ||
hasFullTypeInformation: true, | ||
defaultProjectMatchedFiles: Set<string>, | ||
): ASTAndDefiniteProgram | undefined; | ||
export function useProgramFromProjectService( | ||
settings: ProjectServiceSettings, | ||
parseSettings: Readonly<MutableParseSettings>, | ||
hasFullTypeInformation: false, | ||
defaultProjectMatchedFiles: Set<string>, | ||
): ASTAndNoProgram | undefined; | ||
export function useProgramFromProjectService( | ||
serviceSettings: ProjectServiceSettings, | ||
parseSettings: Readonly<MutableParseSettings>, | ||
hasFullTypeInformation: boolean, | ||
defaultProjectMatchedFiles: Set<string>, | ||
): 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( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not strictly needed for this PR, but it was so annoying having Prettier auto-save the snapshot to no longer have a trailing slash...