Skip to content

Commit b8c8e55

Browse files
authored
Merge pull request microsoft#26032 from Microsoft/projectReferences
Keep the configured projects referenced during original location and project get, alive till the referencing project is alive
2 parents 1345a35 + d65cb21 commit b8c8e55

File tree

4 files changed

+197
-74
lines changed

4 files changed

+197
-74
lines changed

src/server/editorServices.ts

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ namespace ts.server {
326326
syntaxOnly?: boolean;
327327
}
328328

329+
interface OriginalFileInfo { fileName: NormalizedPath; path: Path; }
330+
type OpenScriptInfoOrClosedFileInfo = ScriptInfo | OriginalFileInfo;
331+
332+
function isOpenScriptInfo(infoOrFileName: OpenScriptInfoOrClosedFileInfo): infoOrFileName is ScriptInfo {
333+
return !!(infoOrFileName as ScriptInfo).containingProjects;
334+
}
335+
329336
function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) {
330337
return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`;
331338
}
@@ -700,7 +707,7 @@ namespace ts.server {
700707

701708
/* @internal */
702709
private forEachProject(cb: (project: Project) => void) {
703-
for (const p of this.inferredProjects) cb(p);
710+
this.inferredProjects.forEach(cb);
704711
this.configuredProjects.forEach(cb);
705712
this.externalProjects.forEach(cb);
706713
}
@@ -1044,12 +1051,12 @@ namespace ts.server {
10441051
}
10451052
}
10461053

1047-
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) {
1054+
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: OpenScriptInfoOrClosedFileInfo) {
10481055
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
10491056
if (configFileExistenceInfo) {
10501057
// By default the info would get impacted by presence of config file since its in the detection path
10511058
// Only adding the info as a root to inferred project will need the existence to be watched by file watcher
1052-
if (!configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) {
1059+
if (isOpenScriptInfo(info) && !configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) {
10531060
configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false);
10541061
this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd);
10551062
}
@@ -1066,9 +1073,11 @@ namespace ts.server {
10661073
// Or the whole chain of config files for the roots of the inferred projects
10671074

10681075
// Cache the host value of file exists and add the info to map of open files impacted by this config file
1069-
const openFilesImpactedByConfigFile = createMap<boolean>();
1070-
openFilesImpactedByConfigFile.set(info.path, false);
10711076
const exists = this.host.fileExists(configFileName);
1077+
const openFilesImpactedByConfigFile = createMap<boolean>();
1078+
if (isOpenScriptInfo(info)) {
1079+
openFilesImpactedByConfigFile.set(info.path, false);
1080+
}
10721081
configFileExistenceInfo = { exists, openFilesImpactedByConfigFile };
10731082
this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo);
10741083
this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd);
@@ -1180,7 +1189,7 @@ namespace ts.server {
11801189
*/
11811190
private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) {
11821191
Debug.assert(!info.isScriptOpen());
1183-
this.forEachConfigFileLocation(info, /*infoShouldBeOpen*/ true, (configFileName, canonicalConfigFilePath) => {
1192+
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
11841193
const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
11851194
if (configFileExistenceInfo) {
11861195
const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile.get(info.path);
@@ -1214,7 +1223,7 @@ namespace ts.server {
12141223
/* @internal */
12151224
startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
12161225
Debug.assert(info.isScriptOpen());
1217-
this.forEachConfigFileLocation(info, /*infoShouldBeOpen*/ true, (configFileName, canonicalConfigFilePath) => {
1226+
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
12181227
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
12191228
if (!configFileExistenceInfo) {
12201229
// Create the cache
@@ -1242,7 +1251,7 @@ namespace ts.server {
12421251
*/
12431252
/* @internal */
12441253
stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
1245-
this.forEachConfigFileLocation(info, /*infoShouldBeOpen*/ true, (configFileName, canonicalConfigFilePath) => {
1254+
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
12461255
const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
12471256
if (configFileExistenceInfo && configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) {
12481257
Debug.assert(info.isScriptOpen());
@@ -1265,12 +1274,12 @@ namespace ts.server {
12651274
* The server must start searching from the directory containing
12661275
* the newly opened file.
12671276
*/
1268-
private forEachConfigFileLocation(info: ScriptInfo, infoShouldBeOpen: boolean, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
1277+
private forEachConfigFileLocation(info: OpenScriptInfoOrClosedFileInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
12691278
if (this.syntaxOnly) {
12701279
return undefined;
12711280
}
12721281

1273-
Debug.assert(!infoShouldBeOpen || this.openFiles.has(info.path));
1282+
Debug.assert(!isOpenScriptInfo(info) || this.openFiles.has(info.path));
12741283
const projectRootPath = this.openFiles.get(info.path);
12751284

12761285
let searchPath = asNormalizedPath(getDirectoryPath(info.fileName));
@@ -1309,11 +1318,13 @@ namespace ts.server {
13091318
* current directory (the directory in which tsc was invoked).
13101319
* The server must start searching from the directory containing
13111320
* the newly opened file.
1321+
* If script info is passed in, it is asserted to be open script info
1322+
* otherwise just file name
13121323
*/
1313-
private getConfigFileNameForFile(info: ScriptInfo, infoShouldBeOpen: boolean) {
1314-
if (infoShouldBeOpen) Debug.assert(info.isScriptOpen());
1324+
private getConfigFileNameForFile(info: OpenScriptInfoOrClosedFileInfo) {
1325+
if (isOpenScriptInfo(info)) Debug.assert(info.isScriptOpen());
13151326
this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`);
1316-
const configFileName = this.forEachConfigFileLocation(info, infoShouldBeOpen, (configFileName, canonicalConfigFilePath) =>
1327+
const configFileName = this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) =>
13171328
this.configFileExists(configFileName, canonicalConfigFilePath, info));
13181329
if (configFileName) {
13191330
this.logger.info(`For info: ${info.fileName} :: Config file name: ${configFileName}`);
@@ -2005,7 +2016,7 @@ namespace ts.server {
20052016
// we first detect if there is already a configured project created for it: if so,
20062017
// we re- read the tsconfig file content and update the project only if we havent already done so
20072018
// otherwise we create a new one.
2008-
const configFileName = this.getConfigFileNameForFile(info, /*infoShouldBeOpen*/ true);
2019+
const configFileName = this.getConfigFileNameForFile(info);
20092020
if (configFileName) {
20102021
const project = this.findConfiguredProjectByProjectName(configFileName);
20112022
if (!project) {
@@ -2093,17 +2104,40 @@ namespace ts.server {
20932104
return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath ? toNormalizedPath(projectRootPath) : undefined);
20942105
}
20952106

2096-
/** @internal */
2097-
getProjectForFileWithoutOpening(fileName: NormalizedPath): { readonly scriptInfo: ScriptInfo, readonly projects: ReadonlyArray<Project> } | undefined {
2098-
const scriptInfo = this.filenameToScriptInfo.get(fileName) ||
2099-
this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName, this.currentDirectory, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined);
2100-
if (!scriptInfo) return undefined;
2101-
if (scriptInfo.containingProjects.length) {
2102-
return { scriptInfo, projects: scriptInfo.containingProjects };
2107+
/*@internal*/
2108+
getOriginalLocationEnsuringConfiguredProject(project: Project, location: sourcemaps.SourceMappableLocation): sourcemaps.SourceMappableLocation | undefined {
2109+
const originalLocation = project.getSourceMapper().tryGetOriginalLocation(location);
2110+
if (!originalLocation) return undefined;
2111+
2112+
const { fileName } = originalLocation;
2113+
if (!this.getScriptInfo(fileName) && !this.host.fileExists(fileName)) return undefined;
2114+
2115+
const originalFileInfo: OriginalFileInfo = { fileName: toNormalizedPath(fileName), path: this.toPath(fileName) };
2116+
const configFileName = this.getConfigFileNameForFile(originalFileInfo);
2117+
if (!configFileName) return undefined;
2118+
2119+
const configuredProject = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName);
2120+
updateProjectIfDirty(configuredProject);
2121+
// Keep this configured project as referenced from project
2122+
addOriginalConfiguredProject(configuredProject);
2123+
2124+
const originalScriptInfo = this.getScriptInfo(fileName);
2125+
if (!originalScriptInfo || !originalScriptInfo.containingProjects.length) return undefined;
2126+
2127+
// Add configured projects as referenced
2128+
originalScriptInfo.containingProjects.forEach(project => {
2129+
if (project.projectKind === ProjectKind.Configured) {
2130+
addOriginalConfiguredProject(project as ConfiguredProject);
2131+
}
2132+
});
2133+
return originalLocation;
2134+
2135+
function addOriginalConfiguredProject(originalProject: ConfiguredProject) {
2136+
if (!project.originalConfiguredProjects) {
2137+
project.originalConfiguredProjects = createMap<true>();
2138+
}
2139+
project.originalConfiguredProjects.set(originalProject.canonicalConfigFilePath, true);
21032140
}
2104-
const configFileName = this.getConfigFileNameForFile(scriptInfo, /*infoShouldBeOpen*/ false);
2105-
const project = configFileName === undefined ? undefined : this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName);
2106-
return project && project.containsScriptInfo(scriptInfo) ? { scriptInfo, projects: [project] } : undefined;
21072141
}
21082142

21092143
/** @internal */
@@ -2128,7 +2162,7 @@ namespace ts.server {
21282162
this.openFiles.set(info.path, projectRootPath);
21292163
let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info);
21302164
if (!project && !this.syntaxOnly) { // Checking syntaxOnly is an optimization
2131-
configFileName = this.getConfigFileNameForFile(info, /*infoShouldBeOpen*/ true);
2165+
configFileName = this.getConfigFileNameForFile(info);
21322166
if (configFileName) {
21332167
project = this.findConfiguredProjectByProjectName(configFileName);
21342168
if (!project) {
@@ -2166,14 +2200,9 @@ namespace ts.server {
21662200
}
21672201
Debug.assert(!info.isOrphan());
21682202

2169-
// Remove the configured projects that have zero references from open files.
21702203
// This was postponed from closeOpenFile to after opening next file,
21712204
// so that we can reuse the project if we need to right away
2172-
this.configuredProjects.forEach(project => {
2173-
if (!project.hasOpenRef()) {
2174-
this.removeProject(project);
2175-
}
2176-
});
2205+
this.removeOrphanConfiguredProjects();
21772206

21782207
// Remove orphan inferred projects now that we have reused projects
21792208
// We need to create a duplicate because we cant guarantee order after removal
@@ -2201,6 +2230,30 @@ namespace ts.server {
22012230
return { configFileName, configFileErrors };
22022231
}
22032232

2233+
private removeOrphanConfiguredProjects() {
2234+
const toRemoveConfiguredProjects = cloneMap(this.configuredProjects);
2235+
2236+
// Do not remove configured projects that are used as original projects of other
2237+
this.inferredProjects.forEach(markOriginalProjectsAsUsed);
2238+
this.externalProjects.forEach(markOriginalProjectsAsUsed);
2239+
this.configuredProjects.forEach(project => {
2240+
// If project has open ref (there are more than zero references from external project/open file), keep it alive as well as any project it references
2241+
if (project.hasOpenRef()) {
2242+
toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath);
2243+
markOriginalProjectsAsUsed(project);
2244+
}
2245+
});
2246+
2247+
// Remove all the non marked projects
2248+
toRemoveConfiguredProjects.forEach(project => this.removeProject(project));
2249+
2250+
function markOriginalProjectsAsUsed(project: Project) {
2251+
if (!project.isOrphan() && project.originalConfiguredProjects) {
2252+
project.originalConfiguredProjects.forEach((_value, configuredProjectPath) => toRemoveConfiguredProjects.delete(configuredProjectPath));
2253+
}
2254+
}
2255+
}
2256+
22042257
private telemetryOnOpenFile(scriptInfo: ScriptInfo): void {
22052258
if (this.syntaxOnly || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) {
22062259
return;

src/server/project.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ namespace ts.server {
158158
/*@internal*/
159159
typingFiles: SortedReadonlyArray<string> = emptyArray;
160160

161+
/*@internal*/
162+
originalConfiguredProjects: Map<true> | undefined;
163+
161164
private readonly cancellationToken: ThrottledCancellationToken;
162165

163166
public isNonTsProject() {
@@ -711,7 +714,7 @@ namespace ts.server {
711714
}
712715

713716
containsFile(filename: NormalizedPath, requireOpen?: boolean): boolean {
714-
const info = this.projectService.getScriptInfoForPath(this.toPath(filename));
717+
const info = this.projectService.getScriptInfoForNormalizedPath(filename);
715718
if (info && (info.isScriptOpen() || !requireOpen)) {
716719
return this.containsScriptInfo(info);
717720
}

src/server/session.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -427,23 +427,20 @@ namespace ts.server {
427427
if (projectAndLocation.project.getCancellationToken().isCancellationRequested()) return undefined; // Skip rest of toDo if cancelled
428428
cb(projectAndLocation, (project, location) => {
429429
seenProjects.set(projectAndLocation.project.projectName, true);
430-
const originalLocation = project.getSourceMapper().tryGetOriginalLocation(location);
430+
const originalLocation = projectService.getOriginalLocationEnsuringConfiguredProject(project, location);
431431
if (!originalLocation) return false;
432-
const originalProjectAndScriptInfo = projectService.getProjectForFileWithoutOpening(toNormalizedPath(originalLocation.fileName));
433-
if (!originalProjectAndScriptInfo) return false;
434432

435-
if (originalProjectAndScriptInfo) {
436-
toDo = toDo || [];
433+
const originalScriptInfo = projectService.getScriptInfo(originalLocation.fileName)!;
434+
toDo = toDo || [];
437435

438-
for (const project of originalProjectAndScriptInfo.projects) {
439-
addToTodo({ project, location: originalLocation as TLocation }, toDo, seenProjects);
440-
}
441-
const symlinkedProjectsMap = projectService.getSymlinkedProjects(originalProjectAndScriptInfo.scriptInfo);
442-
if (symlinkedProjectsMap) {
443-
symlinkedProjectsMap.forEach((symlinkedProjects) => {
444-
for (const symlinkedProject of symlinkedProjects) addToTodo({ project: symlinkedProject, location: originalLocation as TLocation }, toDo!, seenProjects);
445-
});
446-
}
436+
for (const project of originalScriptInfo.containingProjects) {
437+
addToTodo({ project, location: originalLocation as TLocation }, toDo, seenProjects);
438+
}
439+
const symlinkedProjectsMap = projectService.getSymlinkedProjects(originalScriptInfo);
440+
if (symlinkedProjectsMap) {
441+
symlinkedProjectsMap.forEach((symlinkedProjects) => {
442+
for (const symlinkedProject of symlinkedProjects) addToTodo({ project: symlinkedProject, location: originalLocation as TLocation }, toDo!, seenProjects);
443+
});
447444
}
448445
return true;
449446
});

0 commit comments

Comments
 (0)