Dont open composite projects to determine if script info is part of project (#59688)

This commit is contained in:
Sheetal Nandi
2024-09-18 10:35:22 -07:00
committed by GitHub
parent 8230bc66a7
commit 02b07a1788
1079 changed files with 31109 additions and 25547 deletions
+16 -3
View File
@@ -3250,13 +3250,25 @@ function shouldReportNoInputFiles(fileNames: string[], canJsonReportNoInutFiles:
return fileNames.length === 0 && canJsonReportNoInutFiles && (!resolutionStack || resolutionStack.length === 0);
}
/** @internal */
export function isSolutionConfig(config: ParsedCommandLine): boolean {
return !config.fileNames.length &&
hasProperty(config.raw, "references");
}
/** @internal */
export function canJsonReportNoInputFiles(raw: any): boolean {
return !hasProperty(raw, "files") && !hasProperty(raw, "references");
}
/** @internal */
export function updateErrorForNoInputFiles(fileNames: string[], configFileName: string, configFileSpecs: ConfigFileSpecs, configParseDiagnostics: Diagnostic[], canJsonReportNoInutFiles: boolean): boolean {
export function updateErrorForNoInputFiles(
fileNames: string[],
configFileName: string,
configFileSpecs: ConfigFileSpecs,
configParseDiagnostics: Diagnostic[],
canJsonReportNoInutFiles: boolean,
): boolean {
const existingErrors = configParseDiagnostics.length;
if (shouldReportNoInputFiles(fileNames, canJsonReportNoInutFiles)) {
configParseDiagnostics.push(getErrorForNoInputFiles(configFileSpecs, configFileName));
@@ -3960,13 +3972,14 @@ export function matchesExclude(
);
}
function matchesExcludeWorker(
/** @internal */
export function matchesExcludeWorker(
pathToCheck: string,
excludeSpecs: readonly string[] | undefined,
useCaseSensitiveFileNames: boolean,
currentDirectory: string,
basePath?: string,
) {
): boolean {
const excludePattern = getRegularExpressionForWildcard(excludeSpecs, combinePaths(normalizePath(currentDirectory), basePath), "exclude");
const excludeRegex = excludePattern && getRegexFromPattern(excludePattern, useCaseSensitiveFileNames);
if (!excludeRegex) return false;
+66 -38
View File
@@ -1150,7 +1150,11 @@ export function forEachResolvedProjectReference<T>(
resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined,
cb: (resolvedProjectReference: ResolvedProjectReference, parent: ResolvedProjectReference | undefined) => T | undefined,
): T | undefined {
return forEachProjectReference(/*projectReferences*/ undefined, resolvedProjectReferences, (resolvedRef, parent) => resolvedRef && cb(resolvedRef, parent));
return forEachProjectReference(
/*projectReferences*/ undefined,
resolvedProjectReferences,
(resolvedRef, parent) => resolvedRef && cb(resolvedRef, parent),
);
}
function forEachProjectReference<T>(
@@ -1160,7 +1164,6 @@ function forEachProjectReference<T>(
cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined,
): T | undefined {
let seenResolvedRefs: Set<Path> | undefined;
return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined);
function worker(
@@ -1173,19 +1176,26 @@ function forEachProjectReference<T>(
const result = cbRef(projectReferences, parent);
if (result) return result;
}
return forEach(resolvedProjectReferences, (resolvedRef, index) => {
if (resolvedRef && seenResolvedRefs?.has(resolvedRef.sourceFile.path)) {
// ignore recursives
return undefined;
}
const result = cbResolvedRef(resolvedRef, parent, index);
if (result || !resolvedRef) return result;
(seenResolvedRefs ||= new Set()).add(resolvedRef.sourceFile.path);
return worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef);
});
let skipChildren: Set<ResolvedProjectReference> | undefined;
return forEach(
resolvedProjectReferences,
(resolvedRef, index) => {
if (resolvedRef && seenResolvedRefs?.has(resolvedRef.sourceFile.path)) {
(skipChildren ??= new Set()).add(resolvedRef);
// ignore recursives
return undefined;
}
const result = cbResolvedRef(resolvedRef, parent, index);
if (result || !resolvedRef) return result;
(seenResolvedRefs ||= new Set()).add(resolvedRef.sourceFile.path);
},
) || forEach(
resolvedProjectReferences,
resolvedRef =>
resolvedRef && !skipChildren?.has(resolvedRef) ?
worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef) :
undefined,
);
}
}
@@ -1367,7 +1377,14 @@ export function isProgramUptoDate(
(seenResolvedRefs || (seenResolvedRefs = [])).push(oldResolvedRef);
// If child project references are upto date, this project reference is uptodate
return !forEach(oldResolvedRef.references, (childResolvedRef, index) => !resolvedProjectReferenceUptoDate(childResolvedRef, oldResolvedRef.commandLine.projectReferences![index]));
return !forEach(
oldResolvedRef.references,
(childResolvedRef, index) =>
!resolvedProjectReferenceUptoDate(
childResolvedRef,
oldResolvedRef.commandLine.projectReferences![index],
),
);
}
// In old program, not able to resolve project reference path,
@@ -4905,7 +4922,14 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
case FileIncludeKind.SourceFromProjectReference:
case FileIncludeKind.OutputFromProjectReference:
const referencedResolvedRef = Debug.checkDefined(resolvedProjectReferences?.[reason.index]);
const referenceInfo = forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, parent, index) => resolvedRef === referencedResolvedRef ? { sourceFile: parent?.sourceFile || options.configFile!, index } : undefined);
const referenceInfo = forEachProjectReference(
projectReferences,
resolvedProjectReferences,
(resolvedRef, parent, index) =>
resolvedRef === referencedResolvedRef ?
{ sourceFile: parent?.sourceFile || options.configFile!, index } :
undefined,
);
if (!referenceInfo) return undefined;
const { sourceFile, index } = referenceInfo;
const referencesSyntax = forEachTsConfigPropArray(sourceFile as TsConfigSourceFile, "references", property => isArrayLiteralExpression(property.initializer) ? property.initializer : undefined);
@@ -4945,28 +4969,32 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
function verifyProjectReferences() {
const buildInfoPath = !options.suppressOutputPathCheck ? getTsBuildInfoEmitOutputFilePath(options) : undefined;
forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, parent, index) => {
const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index];
const parentFile = parent && parent.sourceFile as JsonSourceFile;
verifyDeprecatedProjectReference(ref, parentFile, index);
if (!resolvedRef) {
createDiagnosticForReference(parentFile, index, Diagnostics.File_0_not_found, ref.path);
return;
}
const options = resolvedRef.commandLine.options;
if (!options.composite || options.noEmit) {
// ok to not have composite if the current program is container only
const inputs = parent ? parent.commandLine.fileNames : rootNames;
if (inputs.length) {
if (!options.composite) createDiagnosticForReference(parentFile, index, Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true, ref.path);
if (options.noEmit) createDiagnosticForReference(parentFile, index, Diagnostics.Referenced_project_0_may_not_disable_emit, ref.path);
forEachProjectReference(
projectReferences,
resolvedProjectReferences,
(resolvedRef, parent, index) => {
const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index];
const parentFile = parent && parent.sourceFile as JsonSourceFile;
verifyDeprecatedProjectReference(ref, parentFile, index);
if (!resolvedRef) {
createDiagnosticForReference(parentFile, index, Diagnostics.File_0_not_found, ref.path);
return;
}
}
if (!parent && buildInfoPath && buildInfoPath === getTsBuildInfoEmitOutputFilePath(options)) {
createDiagnosticForReference(parentFile, index, Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, buildInfoPath, ref.path);
hasEmitBlockingDiagnostics.set(toPath(buildInfoPath), true);
}
});
const options = resolvedRef.commandLine.options;
if (!options.composite || options.noEmit) {
// ok to not have composite if the current program is container only
const inputs = parent ? parent.commandLine.fileNames : rootNames;
if (inputs.length) {
if (!options.composite) createDiagnosticForReference(parentFile, index, Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true, ref.path);
if (options.noEmit) createDiagnosticForReference(parentFile, index, Diagnostics.Referenced_project_0_may_not_disable_emit, ref.path);
}
}
if (!parent && buildInfoPath && buildInfoPath === getTsBuildInfoEmitOutputFilePath(options)) {
createDiagnosticForReference(parentFile, index, Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, buildInfoPath, ref.path);
hasEmitBlockingDiagnostics.set(toPath(buildInfoPath), true);
}
},
);
}
function createDiagnosticForOptionPathKeyValue(key: string, valueIndex: number, message: DiagnosticMessage, ...args: DiagnosticArguments) {
+9 -6
View File
@@ -80,6 +80,7 @@ import {
isIncrementalBuildInfo,
isIncrementalCompilation,
isPackageJsonInfo,
isSolutionConfig,
loadWithModeAwareCache,
maybeBind,
missingFileModifiedTime,
@@ -1236,7 +1237,13 @@ function getNextInvalidatedProjectCreateInfo<T extends BuilderProgram>(
else if (updateLevel === ProgramUpdateLevel.RootNamesAndUpdate) {
// Update file names
config.fileNames = getFileNamesFromConfigSpecs(config.options.configFile!.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost);
updateErrorForNoInputFiles(config.fileNames, project, config.options.configFile!.configFileSpecs!, config.errors, canJsonReportNoInputFiles(config.raw));
updateErrorForNoInputFiles(
config.fileNames,
project,
config.options.configFile!.configFileSpecs!,
config.errors,
canJsonReportNoInputFiles(config.raw),
);
watchInputFiles(state, project, projectPath, config);
watchPackageJsonFiles(state, project, projectPath, config);
}
@@ -1461,11 +1468,7 @@ function checkConfigFileUpToDateStatus<T extends BuilderProgram>(state: Solution
function getUpToDateStatusWorker<T extends BuilderProgram>(state: SolutionBuilderState<T>, project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus {
// Container if no files are specified in the project
if (!project.fileNames.length && !canJsonReportNoInputFiles(project.raw)) {
return {
type: UpToDateStatusType.ContainerOnly,
};
}
if (isSolutionConfig(project)) return { type: UpToDateStatusType.ContainerOnly };
// Fast check to see if reference projects are upto date and error free
let referenceStatuses;
+9 -1
View File
@@ -925,7 +925,15 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
updateLevel = ProgramUpdateLevel.Update;
rootFileNames = getFileNamesFromConfigSpecs(compilerOptions.configFile!.configFileSpecs!, getNormalizedAbsolutePath(getDirectoryPath(configFileName), currentDirectory), compilerOptions, parseConfigFileHost, extraFileExtensions);
if (updateErrorForNoInputFiles(rootFileNames, getNormalizedAbsolutePath(configFileName, currentDirectory), compilerOptions.configFile!.configFileSpecs!, configFileParsingDiagnostics!, canConfigFileJsonReportNoInputFiles)) {
if (
updateErrorForNoInputFiles(
rootFileNames,
getNormalizedAbsolutePath(configFileName, currentDirectory),
compilerOptions.configFile!.configFileSpecs!,
configFileParsingDiagnostics!,
canConfigFileJsonReportNoInputFiles,
)
) {
hasChangedConfigFileParsingErrors = true;
}
+2
View File
@@ -82,6 +82,7 @@ import { protocol } from "./_namespaces/ts.server.js";
export interface SessionClientHost extends LanguageServiceHost {
writeMessage(message: string): void;
openFile(fileName: string): void;
}
interface RenameEntry {
@@ -480,6 +481,7 @@ export class SessionClient implements LanguageService {
}
getFileReferences(fileName: string): ReferenceEntry[] {
this.host.openFile(fileName);
const request = this.processRequest<protocol.FileReferencesRequest>(protocol.CommandTypes.FileReferences, { file: fileName });
const response = this.processResponse<protocol.FileReferencesResponse>(request);
+3
View File
@@ -31,6 +31,7 @@ interface ProjectData {
projectStateVersion: Project["projectStateVersion"];
projectProgramVersion: Project["projectProgramVersion"];
dirty: Project["dirty"];
initialLoadPending: Project["initialLoadPending"];
isClosed: ReturnType<Project["isClosed"]>;
isOrphan: ReturnType<Project["isOrphan"]>;
noOpenRef: boolean;
@@ -123,6 +124,7 @@ export function patchServiceForStateBaseline(service: ProjectService): void {
projectDiff = printProperty(PrintPropertyWhen.Always, data, "projectStateVersion", project.projectStateVersion, projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.Always, data, "projectProgramVersion", project.projectProgramVersion, projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "dirty", project.dirty, projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "initialLoadPending", project.initialLoadPending, projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isClosed", project.isClosed(), projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isOrphan", !isBackgroundProject(project) && project.isOrphan(), projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "noOpenRef", noOpenRef(project), projectDiff, projectPropertyLogs);
@@ -154,6 +156,7 @@ export function patchServiceForStateBaseline(service: ProjectService): void {
projectStateVersion: project.projectStateVersion,
projectProgramVersion: project.projectProgramVersion,
dirty: project.dirty,
initialLoadPending: project.initialLoadPending,
isClosed: project.isClosed(),
isOrphan: !isBackgroundProject(project) && project.isOrphan(),
noOpenRef: noOpenRef(project),
File diff suppressed because it is too large Load Diff
+22 -18
View File
@@ -8,6 +8,7 @@ import {
arrayToMap,
BuilderState,
CachedDirectoryStructureHost,
canJsonReportNoInputFiles,
canWatchDirectoryOrFilePath,
changeExtension,
changesAffectModuleResolution,
@@ -111,7 +112,6 @@ import {
ResolvedTypeReferenceDirectiveWithFailedLookupLocations,
resolvePackageNameToPackageJson,
returnFalse,
returnTrue,
ScriptKind,
some,
sortAndDeduplicate,
@@ -439,7 +439,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
protected projectErrors: Diagnostic[] | undefined;
protected isInitialLoadPending: () => boolean = returnFalse;
/** @internal */
initialLoadPending = false;
/** @internal */
dirty = false;
@@ -1915,7 +1916,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}
private filesToStringWorker(writeProjectFileNames: boolean, writeFileExplaination: boolean, writeFileVersionAndText: boolean) {
if (this.isInitialLoadPending()) return "\tFiles (0) InitialLoadPending\n";
if (this.initialLoadPending) return "\tFiles (0) InitialLoadPending\n";
if (!this.program) return "\tFiles (0) NoProgram\n";
const sourceFiles = this.program.getSourceFiles();
let strBuilder = `\tFiles (${sourceFiles.length})\n`;
@@ -1995,7 +1996,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
: (files: Map<string, boolean>) => arrayFrom(files.keys());
// Update the graph only if initial configured project load is not pending
if (!this.isInitialLoadPending()) {
if (!this.initialLoadPending) {
updateProjectIfDirty(this);
}
@@ -2871,7 +2872,9 @@ export class ConfiguredProject extends Project {
openFileWatchTriggered: Map<string, ProgramUpdateLevel> = new Map();
/** @internal */
canConfigFileJsonReportNoInputFiles = false;
parsedCommandLine: ParsedCommandLine | undefined;
/** @internal */
resolvedChildConfigs?: Set<NormalizedPath>;
private projectReferences: readonly ProjectReference[] | undefined;
@@ -2886,7 +2889,7 @@ export class ConfiguredProject extends Project {
projectOptions?: ProjectOptions | true;
/** @internal */
override isInitialLoadPending: () => boolean = returnTrue;
override initialLoadPending = true;
/** @internal */
sendLoadingProjectFinish = false;
@@ -2943,7 +2946,7 @@ export class ConfiguredProject extends Project {
/** @internal */
override getParsedCommandLine(fileName: string): ParsedCommandLine | undefined {
const configFileName = asNormalizedPath(normalizePath(fileName));
const configFileName = toNormalizedPath(fileName);
const canonicalConfigFilePath = asNormalizedPath(this.projectService.toCanonicalFileName(configFileName));
// Ensure the config file existience info is cached
let configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(canonicalConfigFilePath);
@@ -2961,7 +2964,7 @@ export class ConfiguredProject extends Project {
/** @internal */
onReleaseParsedCommandLine(fileName: string): void {
this.releaseParsedConfig(asNormalizedPath(this.projectService.toCanonicalFileName(asNormalizedPath(normalizePath(fileName)))));
this.releaseParsedConfig(asNormalizedPath(this.projectService.toCanonicalFileName(toNormalizedPath(fileName))));
}
private releaseParsedConfig(canonicalConfigFilePath: NormalizedPath) {
@@ -2976,7 +2979,7 @@ export class ConfiguredProject extends Project {
override updateGraph(): boolean {
if (this.deferredClose) return false;
const isDirty = this.dirty;
this.isInitialLoadPending = returnFalse;
this.initialLoadPending = false;
const updateLevel = this.pendingUpdateLevel;
this.pendingUpdateLevel = ProgramUpdateLevel.Update;
let result: boolean;
@@ -3036,7 +3039,7 @@ export class ConfiguredProject extends Project {
/** @internal */
setPotentialProjectReference(canonicalConfigPath: NormalizedPath): void {
Debug.assert(this.isInitialLoadPending());
Debug.assert(this.initialLoadPending);
(this.potentialProjectReferences || (this.potentialProjectReferences = new Set())).add(canonicalConfigPath);
}
@@ -3112,12 +3115,6 @@ export class ConfiguredProject extends Project {
super.markAsDirty();
}
/** @internal */
isSolution(): boolean {
return this.getRootFilesMap().size === 0 &&
!this.canConfigFileJsonReportNoInputFiles;
}
/** @internal */
override isOrphan(): boolean {
return !!this.deferredClose;
@@ -3128,8 +3125,15 @@ export class ConfiguredProject extends Project {
}
/** @internal */
updateErrorOnNoInputFiles(fileNames: string[]): void {
updateErrorForNoInputFiles(fileNames, this.getConfigFilePath(), this.getCompilerOptions().configFile!.configFileSpecs!, this.projectErrors!, this.canConfigFileJsonReportNoInputFiles);
updateErrorOnNoInputFiles(parsedCommandLine: ParsedCommandLine): void {
this.parsedCommandLine = parsedCommandLine;
updateErrorForNoInputFiles(
parsedCommandLine.fileNames,
this.getConfigFilePath(),
this.getCompilerOptions().configFile!.configFileSpecs!,
this.projectErrors!,
canJsonReportNoInputFiles(parsedCommandLine.raw),
);
}
}
+20
View File
@@ -498,6 +498,10 @@ export interface ProjectInfoRequestArgs extends FileRequestArgs {
* Indicate if the file name list of the project is needed
*/
needFileNameList: boolean;
/**
* if true returns details about default configured project calculation
*/
needDefaultConfiguredProjectInfo?: boolean;
}
/**
@@ -525,6 +529,18 @@ export interface CompilerOptionsDiagnosticsRequestArgs {
projectFileName: string;
}
/**
* Details about the default project for the file if tsconfig file is found
*/
export interface DefaultConfiguredProjectInfo {
/** List of config files looked and did not match because file was not part of root file names */
notMatchedByConfig?: readonly string[];
/** List of projects which were loaded but file was not part of the project or is file from referenced project */
notInProject?: readonly string[];
/** Configured project used as default */
defaultProject?: string;
}
/**
* Response message body for "projectInfo" request
*/
@@ -542,6 +558,10 @@ export interface ProjectInfo {
* Indicates if the project has a active language service instance
*/
languageServiceDisabled?: boolean;
/**
* Information about default project
*/
configuredProjectInfo?: DefaultConfiguredProjectInfo;
}
/**
+16 -8
View File
@@ -688,14 +688,6 @@ export class ScriptInfo {
return this.deferredDelete || !forEach(this.containingProjects, p => !p.isOrphan());
}
/** @internal */
isContainedByBackgroundProject(): boolean {
return some(
this.containingProjects,
isBackgroundProject,
);
}
/**
* @param line 1 based index
*/
@@ -746,3 +738,19 @@ function failIfInvalidLocation(location: protocol.Location) {
Debug.assert(location.line > 0, `Expected line to be non-${location.line === 0 ? "zero" : "negative"}`);
Debug.assert(location.offset > 0, `Expected offset to be non-${location.offset === 0 ? "zero" : "negative"}`);
}
/** @internal */
export function scriptInfoIsContainedByBackgroundProject(info: ScriptInfo): boolean {
return some(
info.containingProjects,
isBackgroundProject,
);
}
/** @internal */
export function scriptInfoIsContainedByDeferredClosedProject(info: ScriptInfo): boolean {
return some(
info.containingProjects,
isProjectDeferredClose,
);
}
+108 -33
View File
@@ -143,6 +143,7 @@ import {
CloseFileWatcherEvent,
ConfigFileDiagEvent,
ConfiguredProject,
ConfiguredProjectLoadKind,
convertFormatOptions,
convertScriptKindName,
convertUserPreferences,
@@ -524,7 +525,8 @@ function getRenameLocationsWorker(
projects,
defaultProject,
initialLocation,
/*isForRename*/ true,
getDefinitionLocation(defaultProject, initialLocation, /*isForRename*/ true),
mapDefinitionInProject,
(project, position) => project.getLanguageService().findRenameLocations(position.fileName, position.pos, findInStrings, findInComments, preferences),
(renameLocation, cb) => cb(documentSpanLocation(renameLocation)),
);
@@ -569,7 +571,8 @@ function getReferencesWorker(
projects,
defaultProject,
initialLocation,
/*isForRename*/ false,
getDefinitionLocation(defaultProject, initialLocation, /*isForRename*/ false),
mapDefinitionInProject,
(project, position) => {
logger.info(`Finding references to ${position.fileName} position ${position.pos} in project ${project.getProjectName()}`);
return project.getLanguageService().findReferences(position.fileName, position.pos);
@@ -709,9 +712,15 @@ function getPerProjectReferences<TResult>(
projects: Projects,
defaultProject: Project,
initialLocation: DocumentPosition,
isForRename: boolean,
defaultDefinition: DocumentPosition | undefined,
mapDefinitionInProject: (
definition: DocumentPosition,
project: Project,
getGeneratedDefinition: () => DocumentPosition | undefined,
getSourceDefinition: () => DocumentPosition | undefined,
) => DocumentPosition | undefined,
getResultsForPosition: (project: Project, location: DocumentPosition) => readonly TResult[] | undefined,
forPositionInResult: (result: TResult, cb: (location: DocumentPosition) => void) => void,
forPositionInResult?: (result: TResult, cb: (location: DocumentPosition) => void) => void,
): readonly TResult[] | Map<Project, readonly TResult[]> {
// If `getResultsForPosition` returns results for a project, they go in here
const resultsMap = new Map<Project, readonly TResult[]>();
@@ -733,8 +742,6 @@ function getPerProjectReferences<TResult>(
const projectService = defaultProject.projectService;
const cancellationToken = defaultProject.getCancellationToken();
const defaultDefinition = getDefinitionLocation(defaultProject, initialLocation, isForRename);
// Don't call these unless !!defaultDefinition
const getGeneratedDefinition = memoize(() =>
defaultProject.isSourceOfProjectReferenceRedirect(defaultDefinition!.fileName) ?
@@ -800,7 +807,7 @@ function getPerProjectReferences<TResult>(
function searchPosition(project: Project, location: DocumentPosition): readonly TResult[] | undefined {
const projectResults = getResultsForPosition(project, location);
if (!projectResults) return undefined;
if (!projectResults || !forPositionInResult) return projectResults;
for (const result of projectResults) {
forPositionInResult(result, position => {
@@ -833,12 +840,10 @@ function getPerProjectReferences<TResult>(
}
}
function mapDefinitionInProject(
function mapDefinitionInProjectIfFileInProject(
definition: DocumentPosition,
project: Project,
getGeneratedDefinition: () => DocumentPosition | undefined,
getSourceDefinition: () => DocumentPosition | undefined,
): DocumentPosition | undefined {
) {
// If the definition is actually from the project, definition is correct as is
if (
project.containsFile(toNormalizedPath(definition.fileName)) &&
@@ -846,13 +851,24 @@ function mapDefinitionInProject(
) {
return definition;
}
}
function mapDefinitionInProject(
definition: DocumentPosition,
project: Project,
getGeneratedDefinition: () => DocumentPosition | undefined,
getSourceDefinition: () => DocumentPosition | undefined,
): DocumentPosition | undefined {
// If the definition is actually from the project, definition is correct as is
const result = mapDefinitionInProjectIfFileInProject(definition, project);
if (result) return result;
const generatedDefinition = getGeneratedDefinition();
if (generatedDefinition && project.containsFile(toNormalizedPath(generatedDefinition.fileName))) return generatedDefinition;
const sourceDefinition = getSourceDefinition();
return sourceDefinition && project.containsFile(toNormalizedPath(sourceDefinition.fileName)) ? sourceDefinition : undefined;
}
function isLocationProjectReferenceRedirect(project: Project, location: DocumentPosition | undefined) {
function isLocationProjectReferenceRedirect(project: Project, location: Pick<DocumentPosition, "fileName"> | undefined) {
if (!location) return false;
const program = project.getLanguageService().getProgram();
if (!program) return false;
@@ -2039,20 +2055,63 @@ export class Session<TMessage = string> implements EventSender {
}
private getProjectInfo(args: protocol.ProjectInfoRequestArgs): protocol.ProjectInfo {
return this.getProjectInfoWorker(args.file, args.projectFileName, args.needFileNameList, /*excludeConfigFiles*/ false);
return this.getProjectInfoWorker(
args.file,
args.projectFileName,
args.needFileNameList,
args.needDefaultConfiguredProjectInfo,
/*excludeConfigFiles*/ false,
);
}
private getProjectInfoWorker(uncheckedFileName: string, projectFileName: string | undefined, needFileNameList: boolean, excludeConfigFiles: boolean) {
private getProjectInfoWorker(
uncheckedFileName: string,
projectFileName: string | undefined,
needFileNameList: boolean,
needDefaultConfiguredProjectInfo: boolean | undefined,
excludeConfigFiles: boolean,
): Omit<protocol.ProjectInfo, "fileNames"> & { fileNames?: NormalizedPath[]; } {
const { project } = this.getFileAndProjectWorker(uncheckedFileName, projectFileName);
updateProjectIfDirty(project);
const projectInfo = {
configFileName: project.getProjectName(),
languageServiceDisabled: !project.languageServiceEnabled,
fileNames: needFileNameList ? project.getFileNames(/*excludeFilesFromExternalLibraries*/ false, excludeConfigFiles) : undefined,
configuredProjectInfo: needDefaultConfiguredProjectInfo ? this.getDefaultConfiguredProjectInfo(uncheckedFileName) : undefined,
};
return projectInfo;
}
private getDefaultConfiguredProjectInfo(uncheckedFileName: string): protocol.DefaultConfiguredProjectInfo | undefined {
const info = this.projectService.getScriptInfo(uncheckedFileName);
if (!info) return;
// Find default project for the info
const result = this.projectService.findDefaultConfiguredProjectWorker(
info,
ConfiguredProjectLoadKind.CreateReplay,
);
if (!result) return undefined;
let notMatchedByConfig: NormalizedPath[] | undefined;
let notInProject: NormalizedPath[] | undefined;
result.seenProjects.forEach((kind, project) => {
if (project !== result.defaultProject) {
if (kind !== ConfiguredProjectLoadKind.CreateReplay) {
(notMatchedByConfig ??= []).push(toNormalizedPath(project.getConfigFilePath()));
}
else {
(notInProject ??= []).push(toNormalizedPath(project.getConfigFilePath()));
}
}
});
result.seenConfigs?.forEach(config => (notMatchedByConfig ??= []).push(config));
return {
notMatchedByConfig,
notInProject,
defaultProject: result.defaultProject && toNormalizedPath(result.defaultProject.getConfigFilePath()),
};
}
private getRenameInfo(args: protocol.FileLocationRequestArgs): RenameInfo {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
@@ -2187,25 +2246,38 @@ export class Session<TMessage = string> implements EventSender {
private getFileReferences(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.FileReferencesResponseBody | readonly ReferenceEntry[] {
const projects = this.getProjects(args);
const fileName = args.file;
const preferences = this.getPreferences(toNormalizedPath(fileName));
const fileName = toNormalizedPath(args.file);
const preferences = this.getPreferences(fileName);
const initialLocation: DocumentPosition = { fileName, pos: 0 };
const perProjectResults = getPerProjectReferences(
projects,
this.getDefaultProject(args),
initialLocation,
initialLocation,
mapDefinitionInProjectIfFileInProject,
project => {
this.logger.info(`Finding references to file ${fileName} in project ${project.getProjectName()}`);
return project.getLanguageService().getFileReferences(fileName);
},
);
const references: ReferenceEntry[] = [];
const seen = createDocumentSpanSet(this.host.useCaseSensitiveFileNames);
forEachProjectInProjects(projects, /*path*/ undefined, project => {
if (project.getCancellationToken().isCancellationRequested()) return;
const projectOutputs = project.getLanguageService().getFileReferences(fileName);
if (projectOutputs) {
// No re-mapping or isDefinition updatses are required if there's exactly one project
let references: ReferenceEntry[];
if (isArray(perProjectResults)) {
references = perProjectResults as ReferenceEntry[];
}
else {
references = [];
const seen = createDocumentSpanSet(this.host.useCaseSensitiveFileNames);
perProjectResults.forEach(projectOutputs => {
for (const referenceEntry of projectOutputs) {
if (!seen.has(referenceEntry)) {
references.push(referenceEntry);
seen.add(referenceEntry);
}
}
}
});
});
}
if (!simplifiedResult) return references;
const refs = references.map(entry => referenceEntryToReferencesResponseItem(this.projectService, entry, preferences));
@@ -3091,16 +3163,19 @@ export class Session<TMessage = string> implements EventSender {
return;
}
const { fileNames, languageServiceDisabled } = this.getProjectInfoWorker(fileName, /*projectFileName*/ undefined, /*needFileNameList*/ true, /*excludeConfigFiles*/ true);
if (languageServiceDisabled) {
return;
}
const { fileNames, languageServiceDisabled } = this.getProjectInfoWorker(
fileName,
/*projectFileName*/ undefined,
/*needFileNameList*/ true,
/*needDefaultConfiguredProjectInfo*/ undefined,
/*excludeConfigFiles*/ true,
);
if (languageServiceDisabled) return;
// No need to analyze lib.d.ts
const fileNamesInProject = fileNames!.filter(value => !value.includes("lib.d.ts")); // TODO: GH#18217
if (fileNamesInProject.length === 0) {
return;
}
if (fileNamesInProject.length === 0) return;
// Sort the file name list to make the recently touched files come first
const highPriorityFiles: NormalizedPath[] = [];
@@ -462,6 +462,20 @@ export function openFilesForSession(
}
}
export function projectInfoForSession(
session: TestSession,
file: string | File,
) {
return session.executeCommandSeq<ts.server.protocol.ProjectInfoRequest>({
command: ts.server.protocol.CommandTypes.ProjectInfo,
arguments: {
file: ts.isString(file) ? file : file.path,
needFileNameList: false,
needDefaultConfiguredProjectInfo: true,
},
}).response as ts.server.protocol.ProjectInfo;
}
export function closeFilesForSession(files: readonly (File | string)[], session: TestSession): void {
for (const file of files) {
session.executeCommandSeq<ts.server.protocol.CloseRequest>({
@@ -46,7 +46,7 @@ describe("unittests:: tsserver:: auxiliaryProject::", () => {
// project should throw.
const bJsScriptInfo = ts.Debug.checkDefined(session.getProjectService().getScriptInfo(bJs.path));
assert(bJsScriptInfo.isOrphan());
assert(bJsScriptInfo.isContainedByBackgroundProject());
assert(ts.server.scriptInfoIsContainedByBackgroundProject(bJsScriptInfo));
assert.deepEqual(bJsScriptInfo.containingProjects, [auxProject]);
assert.throws(() => bJsScriptInfo.getDefaultProject());
@@ -54,7 +54,7 @@ describe("unittests:: tsserver:: auxiliaryProject::", () => {
// even though it's still contained by the AuxiliaryProject.
openFilesForSession([bJs], session);
assert(!bJsScriptInfo.isOrphan());
assert(bJsScriptInfo.isContainedByBackgroundProject());
assert(ts.server.scriptInfoIsContainedByBackgroundProject(bJsScriptInfo));
assert.equal(bJsScriptInfo.getDefaultProject().projectKind, ts.server.ProjectKind.Inferred);
baselineTsserverLogs("auxiliaryProject", "does not remove scrips from InferredProject", session);
});
@@ -2,6 +2,7 @@ import * as ts from "../../_namespaces/ts.js";
import { jsonToReadableText } from "../helpers.js";
import {
baselineTsserverLogs,
projectInfoForSession,
TestSession,
TestSessionRequest,
} from "../helpers/tsserver.js";
@@ -88,10 +89,7 @@ describe("unittests:: tsserver:: cancellationToken::", () => {
});
// run new request
session.executeCommandSeq<ts.server.protocol.ProjectInfoRequest>({
command: ts.server.protocol.CommandTypes.ProjectInfo,
arguments: { file: f1.path, needFileNameList: false },
});
projectInfoForSession(session, f1);
// cancel previously issued Geterr
session.serverCancellationToken.setRequestToCancel(getErrId);
@@ -6,6 +6,7 @@ import {
baselineTsserverLogs,
closeFilesForSession,
openFilesForSession,
projectInfoForSession,
TestSession,
verifyGetErrRequest,
} from "../helpers/tsserver.js";
@@ -152,6 +153,7 @@ describe("unittests:: tsserver:: configuredProjects::", () => {
const { host, session, commonFile1, commonFile2, configFile } = setup(parentOrSiblingConfigFile);
openFilesForSession([commonFile1], session);
projectInfoForSession(session, commonFile1);
session.logger.log("1: When config file is deleted and then another file is opened");
// remove the tsconfig file
@@ -236,6 +238,27 @@ describe("unittests:: tsserver:: configuredProjects::", () => {
openFilesForSession([commonFile1, commonFile2], session);
closeFilesForSession([commonFile2], session);
openFilesForSession([{ file: "/user/username/projects/random/random.ts", content: "export const y = 10;" }], session);
closeFilesForSession(["/user/username/projects/random/random.ts"], session);
session.logger.log("7: When config file is deleted and then another file is opened and projectInfo");
// remove the tsconfig file
host.deleteFile(configFile.path);
openFilesForSession([commonFile2], session);
projectInfoForSession(session, commonFile1);
projectInfoForSession(session, commonFile2);
// Add a tsconfig file
host.writeFile(configFile.path, configFile.content);
host.runQueuedTimeoutCallbacks();
session.logger.log("8: When both files are open and config file is deleted and projectInfo");
// remove the tsconfig file
host.deleteFile(configFile.path);
host.runQueuedTimeoutCallbacks();
projectInfoForSession(session, commonFile1);
projectInfoForSession(session, commonFile2);
baselineTsserverLogs("configuredProjects", `add and then remove a config file ${scenario}`, session);
});
@@ -243,6 +266,7 @@ describe("unittests:: tsserver:: configuredProjects::", () => {
it(`add and then remove a config file ${scenario} and file from first config is not open`, () => {
const { host, session, commonFile2, configFile } = setup(parentOrSiblingConfigFile);
openFilesForSession([commonFile2], session);
projectInfoForSession(session, commonFile2);
session.logger.log("1: When config file is deleted");
// remove the tsconfig file
@@ -271,6 +295,27 @@ describe("unittests:: tsserver:: configuredProjects::", () => {
closeFilesForSession([commonFile2], session);
openFilesForSession([{ file: "/user/username/projects/random/random.ts", content: "export const y = 10;" }], session);
// Add a tsconfig file
host.writeFile(configFile.path, configFile.content);
host.runQueuedTimeoutCallbacks();
closeFilesForSession(["/user/username/projects/random/random.ts"], session);
openFilesForSession([commonFile2], session);
session.logger.log("3: When config file is deleted and projectInfo");
// remove the tsconfig file
host.deleteFile(configFile.path);
host.runQueuedTimeoutCallbacks();
projectInfoForSession(session, commonFile2);
// Add a tsconfig file
host.writeFile(configFile.path, configFile.content);
host.runQueuedTimeoutCallbacks();
session.logger.log("4: Check when file is closed when config file is deleted and projectInfo");
// remove the tsconfig file
host.deleteFile(configFile.path);
projectInfoForSession(session, commonFile2);
baselineTsserverLogs("configuredProjects", `add and then remove a config file ${scenario} and file from first config is not open`, session);
});
}
@@ -995,7 +1040,7 @@ foo();`,
endOffset: 1,
},
});
session.logger.log(`Default project for file: ${fooDts}: ${session.getProjectService().tryGetDefaultProjectForFile(ts.server.toNormalizedPath(fooDts))?.projectName}`);
projectInfoForSession(session, fooDts);
baselineTsserverLogs("configuredProjects", "when default configured project does not contain the file", session);
});
@@ -7,6 +7,7 @@ import {
closeFilesForSession,
createHostWithSolutionBuild,
openFilesForSession,
projectInfoForSession,
protocolFileLocationFromSubstring,
protocolLocationFromSubstring,
TestSession,
@@ -20,10 +21,10 @@ import {
function logDefaultProjectAndDefaultConfiguredProject(session: TestSession, file: File) {
const info = session.getProjectService().getScriptInfo(file.path);
const defaultProject = session.getProjectService().tryGetDefaultProjectForFile(file.path as ts.server.NormalizedPath);
const defaultConfiguredProject = info && session.getProjectService().findDefaultConfiguredProject(info);
session.logger.info(`File: ${file.path}:\n\tgetDefaultProjectForFile:\n\t\t${defaultProject?.projectName}\n\tfindDefaultConfiguredProject:\n\t\t${defaultConfiguredProject?.projectName}`);
return { defaultProject, defaultConfiguredProject };
if (info) {
const projectInfo = projectInfoForSession(session, file);
return session.getProjectService().findProject(projectInfo.configFileName);
}
}
describe("unittests:: tsserver:: with projectReferences:: and tsbuild", () => {
@@ -581,7 +582,7 @@ testCompositeFunction('why hello there', 42);`,
baselineTsserverLogs("projectReferences", `when the referenced projects have allowJs and emitDeclarationOnly`, session);
});
it("when finding local reference doesnt load ancestor/sibling projects", () => {
it("when finding local reference doesnt load ancestor sibling projects", () => {
const solutionLocation = "/user/username/projects/solution";
const solution: File = {
path: `${solutionLocation}/tsconfig.json`,
@@ -661,7 +662,7 @@ testCompositeFunction('why hello there', 42);`,
command: ts.server.protocol.CommandTypes.References,
arguments: protocolFileLocationFromSubstring(programFile, "getSourceFiles"),
});
baselineTsserverLogs("projectReferences", `finding local reference doesnt load ancestor/sibling projects`, session);
baselineTsserverLogs("projectReferences", "finding local reference doesnt load ancestor sibling projects", session);
});
it("when finding references in overlapping projects", () => {
@@ -1080,7 +1081,7 @@ export function bar() {}`,
function verifySolutionScenario(input: Setup) {
const { session, host } = setup(input);
const { defaultProject } = logDefaultProjectAndDefaultConfiguredProject(session, main);
const defaultProject = logDefaultProjectAndDefaultConfiguredProject(session, main);
// Verify errors
verifyGetErrRequest({ session, files: [main] });