From 15cec9d1f7ef8043cf456fc9a288fc2b3f83f073 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 19 Oct 2020 12:59:59 -0700 Subject: [PATCH] Optimizes project loading in few scenarios (#41126) * Some refactoring of forEachResolvedProjectReference * More refactoring * Test before the change * When loading project tree, load projects that directly or indirectly reference the projects we are looking for * Optimize finding project in solution scenario by directly finding possible default project through projectReferenceRedirect This helps in avoiding loading indirect projects when solution indirectly referenced default project --- src/compiler/program.ts | 147 +++++++-------- src/compiler/types.ts | 4 +- src/server/editorServices.ts | 168 ++++++++++++------ src/server/project.ts | 20 ++- .../unittests/tsserver/projectReferences.ts | 100 ++++++++--- 5 files changed, 267 insertions(+), 172 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 1e41cdbf37a..2c8a2709fb7 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -530,6 +530,51 @@ namespace ts { return resolutions; } + /* @internal */ + export function forEachResolvedProjectReference( + 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)); + } + + function forEachProjectReference( + projectReferences: readonly ProjectReference[] | undefined, + resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, + cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, parent: ResolvedProjectReference | undefined, index: number) => T | undefined, + cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined + ): T | undefined { + let seenResolvedRefs: Set | undefined; + + return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined); + + function worker( + projectReferences: readonly ProjectReference[] | undefined, + resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, + parent: ResolvedProjectReference | undefined, + ): T | undefined { + + // Visit project references first + if (cbRef) { + 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); + }); + } + } + /* @internal */ export const inferredTypesContainingFile = "__inferred type names__.ts"; @@ -914,8 +959,8 @@ namespace ts { host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions(), !!getSourceFileByPath(oldSourceFile.path)); } } - oldProgram.forEachResolvedProjectReference((resolvedProjectReference, resolvedProjectReferencePath) => { - if (resolvedProjectReference && !getResolvedProjectReferenceByPath(resolvedProjectReferencePath)) { + oldProgram.forEachResolvedProjectReference(resolvedProjectReference => { + if (!getResolvedProjectReferenceByPath(resolvedProjectReference.sourceFile.path)) { host.onReleaseOldSourceFile!(resolvedProjectReference.sourceFile, oldProgram!.getCompilerOptions(), /*hasSourceFileByPath*/ false); } }); @@ -1038,7 +1083,6 @@ namespace ts { if (!source) return undefined; // Output of .d.ts file so return resolved ref that matches the out file name return forEachResolvedProjectReference(resolvedRef => { - if (!resolvedRef) return undefined; const out = outFile(resolvedRef.commandLine.options); if (!out) return undefined; return toPath(out) === filePath ? resolvedRef : undefined; @@ -1251,7 +1295,7 @@ namespace ts { return !forEachProjectReference( oldProgram!.getProjectReferences(), oldProgram!.getResolvedProjectReferences(), - (oldResolvedRef, index, parent) => { + (oldResolvedRef, parent, index) => { const newRef = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; const newResolvedRef = parseProjectReferenceConfigFile(newRef); if (oldResolvedRef) { @@ -2115,9 +2159,7 @@ namespace ts { if (!options.configFile) { return emptyArray; } let diagnostics = programDiagnostics.getDiagnostics(options.configFile.fileName); forEachResolvedProjectReference(resolvedRef => { - if (resolvedRef) { - diagnostics = concatenate(diagnostics, programDiagnostics.getDiagnostics(resolvedRef.sourceFile.fileName)); - } + diagnostics = concatenate(diagnostics, programDiagnostics.getDiagnostics(resolvedRef.sourceFile.fileName)); }); return diagnostics; } @@ -2597,12 +2639,11 @@ namespace ts { function getResolvedProjectReferenceToRedirect(fileName: string) { if (mapFromFileToProjectReferenceRedirects === undefined) { mapFromFileToProjectReferenceRedirects = new Map(); - forEachResolvedProjectReference((referencedProject, referenceProjectPath) => { + forEachResolvedProjectReference(referencedProject => { // not input file from the referenced project, ignore - if (referencedProject && - toPath(options.configFilePath!) !== referenceProjectPath) { + if (toPath(options.configFilePath!) !== referencedProject.sourceFile.path) { referencedProject.commandLine.fileNames.forEach(f => - mapFromFileToProjectReferenceRedirects!.set(toPath(f), referenceProjectPath)); + mapFromFileToProjectReferenceRedirects!.set(toPath(f), referencedProject.sourceFile.path)); } }); } @@ -2612,13 +2653,9 @@ namespace ts { } function forEachResolvedProjectReference( - cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined + cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined ): T | undefined { - return forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, index, parent) => { - const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; - const resolvedRefPath = toPath(resolveProjectReferencePath(ref)); - return cb(resolvedRef, resolvedRefPath); - }); + return ts.forEachResolvedProjectReference(resolvedProjectReferences, cb); } function getSourceOfProjectReferenceRedirect(file: string) { @@ -2626,21 +2663,19 @@ namespace ts { if (mapFromToProjectReferenceRedirectSource === undefined) { mapFromToProjectReferenceRedirectSource = new Map(); forEachResolvedProjectReference(resolvedRef => { - if (resolvedRef) { - const out = outFile(resolvedRef.commandLine.options); - if (out) { - // Dont know which source file it means so return true? - const outputDts = changeExtension(out, Extension.Dts); - mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); - } - else { - forEach(resolvedRef.commandLine.fileNames, fileName => { - if (!fileExtensionIs(fileName, Extension.Dts) && !fileExtensionIs(fileName, Extension.Json)) { - const outputDts = getOutputDeclarationFileName(fileName, resolvedRef.commandLine, host.useCaseSensitiveFileNames()); - mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); - } - }); - } + const out = outFile(resolvedRef.commandLine.options); + if (out) { + // Dont know which source file it means so return true? + const outputDts = changeExtension(out, Extension.Dts); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); + } + else { + forEach(resolvedRef.commandLine.fileNames, fileName => { + if (!fileExtensionIs(fileName, Extension.Dts) && !fileExtensionIs(fileName, Extension.Json)) { + const outputDts = getOutputDeclarationFileName(fileName, resolvedRef.commandLine, host.useCaseSensitiveFileNames()); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); + } + }); } }); } @@ -2651,49 +2686,6 @@ namespace ts { return useSourceOfProjectReferenceRedirect && !!getResolvedProjectReferenceToRedirect(fileName); } - function forEachProjectReference( - projectReferences: readonly ProjectReference[] | undefined, - resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, - cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, index: number, parent: ResolvedProjectReference | undefined) => T | undefined, - cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined - ): T | undefined { - let seenResolvedRefs: ResolvedProjectReference[] | undefined; - - return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined, cbResolvedRef, cbRef); - - function worker( - projectReferences: readonly ProjectReference[] | undefined, - resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, - parent: ResolvedProjectReference | undefined, - cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, index: number, parent: ResolvedProjectReference | undefined) => T | undefined, - cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined, - ): T | undefined { - - // Visit project references first - if (cbRef) { - const result = cbRef(projectReferences, parent); - if (result) { return result; } - } - - return forEach(resolvedProjectReferences, (resolvedRef, index) => { - if (contains(seenResolvedRefs, resolvedRef)) { - // ignore recursives - return undefined; - } - - const result = cbResolvedRef(resolvedRef, index, parent); - if (result) { - return result; - } - - if (!resolvedRef) return undefined; - - (seenResolvedRefs || (seenResolvedRefs = [])).push(resolvedRef); - return worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef, cbResolvedRef, cbRef); - }); - } - } - function getResolvedProjectReferenceByPath(projectReferencePath: Path): ResolvedProjectReference | undefined { if (!projectReferenceRedirects) { return undefined; @@ -3348,7 +3340,7 @@ namespace ts { function verifyProjectReferences() { const buildInfoPath = !options.suppressOutputPathCheck ? getTsBuildInfoEmitOutputFilePath(options) : undefined; - forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, index, parent) => { + forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, parent, index) => { const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; const parentFile = parent && parent.sourceFile as JsonSourceFile; if (!resolvedRef) { @@ -3546,7 +3538,7 @@ namespace ts { toPath(fileName: string): Path; getResolvedProjectReferences(): readonly (ResolvedProjectReference | undefined)[] | undefined; getSourceOfProjectReferenceRedirect(fileName: string): SourceOfProjectReferenceRedirect | undefined; - forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined; + forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined): T | undefined; } function updateHostForUseSourceOfProjectReferenceRedirect(host: HostForUseSourceOfProjectReferenceRedirect) { @@ -3576,7 +3568,6 @@ namespace ts { if (!setOfDeclarationDirectories) { setOfDeclarationDirectories = new Set(); host.forEachResolvedProjectReference(ref => { - if (!ref) return; const out = outFile(ref.commandLine.options); if (out) { setOfDeclarationDirectories!.add(getDirectoryPath(host.toPath(out))); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6ce8b89cca8..d72248ba8d5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3800,7 +3800,7 @@ namespace ts { getResolvedProjectReferences(): readonly (ResolvedProjectReference | undefined)[] | undefined; /*@internal*/ getProjectReferenceRedirect(fileName: string): string | undefined; /*@internal*/ getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined; - /*@internal*/ forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined; + /*@internal*/ forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined): T | undefined; /*@internal*/ getResolvedProjectReferenceByPath(projectReferencePath: Path): ResolvedProjectReference | undefined; /*@internal*/ isSourceOfProjectReferenceRedirect(fileName: string): boolean; /*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined; @@ -6333,7 +6333,7 @@ namespace ts { /*@internal*/ export interface ResolvedProjectReferenceCallbacks { getSourceOfProjectReferenceRedirect(fileName: string): SourceOfProjectReferenceRedirect | undefined; - forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined; + forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined): T | undefined; } /* @internal */ diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 03a7090a8a5..2747a6f03f6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -444,64 +444,105 @@ namespace ts.server { /*@internal*/ export function forEachResolvedProjectReferenceProject( project: ConfiguredProject, + fileName: string | undefined, cb: (child: ConfiguredProject) => T | undefined, projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind.Find | ProjectReferenceProjectLoadKind.FindCreate, ): T | undefined; /*@internal*/ export function forEachResolvedProjectReferenceProject( project: ConfiguredProject, + fileName: string | undefined, cb: (child: ConfiguredProject) => T | undefined, projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, reason: string ): T | undefined; export function forEachResolvedProjectReferenceProject( project: ConfiguredProject, + fileName: string | undefined, cb: (child: ConfiguredProject) => T | undefined, projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, reason?: string ): T | undefined { + const resolvedRefs = project.getCurrentProgram()?.getResolvedProjectReferences(); + if (!resolvedRefs) return undefined; let seenResolvedRefs: ESMap | undefined; - return worker(project.getCurrentProgram()?.getResolvedProjectReferences(), project.getCompilerOptions()); - - function worker(resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, parentOptions: CompilerOptions): T | undefined { - const loadKind = parentOptions.disableReferencedProjectLoad ? ProjectReferenceProjectLoadKind.Find : projectReferenceProjectLoadKind; - return forEach(resolvedProjectReferences, ref => { - if (!ref) return undefined; - - const configFileName = toNormalizedPath(ref.sourceFile.fileName); - const canonicalPath = project.projectService.toCanonicalFileName(configFileName); - const seenValue = seenResolvedRefs?.get(canonicalPath); - if (seenValue !== undefined && seenValue >= loadKind) { - return undefined; - } - const child = project.projectService.findConfiguredProjectByProjectName(configFileName) || ( - loadKind === ProjectReferenceProjectLoadKind.Find ? - undefined : - loadKind === ProjectReferenceProjectLoadKind.FindCreate ? - project.projectService.createConfiguredProject(configFileName) : - loadKind === ProjectReferenceProjectLoadKind.FindCreateLoad ? - project.projectService.createAndLoadConfiguredProject(configFileName, reason!) : - Debug.assertNever(loadKind) + const possibleDefaultRef = fileName ? project.getResolvedProjectReferenceToRedirect(fileName) : undefined; + if (possibleDefaultRef) { + // Try to find the name of the file directly through resolved project references + const configFileName = toNormalizedPath(possibleDefaultRef.sourceFile.fileName); + const child = project.projectService.findConfiguredProjectByProjectName(configFileName); + if (child) { + const result = cb(child); + if (result) return result; + } + else if (projectReferenceProjectLoadKind !== ProjectReferenceProjectLoadKind.Find) { + seenResolvedRefs = new Map(); + // Try to see if this project can be loaded + const result = forEachResolvedProjectReferenceProjectWorker( + resolvedRefs, + project.getCompilerOptions(), + (ref, loadKind) => possibleDefaultRef === ref ? callback(ref, loadKind) : undefined, + projectReferenceProjectLoadKind, + project.projectService, + seenResolvedRefs ); + if (result) return result; + // Cleanup seenResolvedRefs + seenResolvedRefs.clear(); + } + } - const result = child && cb(child); - if (result) { - return result; - } + return forEachResolvedProjectReferenceProjectWorker( + resolvedRefs, + project.getCompilerOptions(), + (ref, loadKind) => possibleDefaultRef !== ref ? callback(ref, loadKind) : undefined, + projectReferenceProjectLoadKind, + project.projectService, + seenResolvedRefs + ); - (seenResolvedRefs || (seenResolvedRefs = new Map())).set(canonicalPath, loadKind); - return worker(ref.references, ref.commandLine.options); - }); + function callback(ref: ResolvedProjectReference, loadKind: ProjectReferenceProjectLoadKind) { + const configFileName = toNormalizedPath(ref.sourceFile.fileName); + const child = project.projectService.findConfiguredProjectByProjectName(configFileName) || ( + loadKind === ProjectReferenceProjectLoadKind.Find ? + undefined : + loadKind === ProjectReferenceProjectLoadKind.FindCreate ? + project.projectService.createConfiguredProject(configFileName) : + loadKind === ProjectReferenceProjectLoadKind.FindCreateLoad ? + project.projectService.createAndLoadConfiguredProject(configFileName, reason!) : + Debug.assertNever(loadKind) + ); + + return child && cb(child); } } - /*@internal*/ - export function forEachResolvedProjectReference( - project: ConfiguredProject, - cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined + function forEachResolvedProjectReferenceProjectWorker( + resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[], + parentOptions: CompilerOptions, + cb: (resolvedRef: ResolvedProjectReference, loadKind: ProjectReferenceProjectLoadKind) => T | undefined, + projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, + projectService: ProjectService, + seenResolvedRefs: ESMap | undefined, ): T | undefined { - const program = project.getCurrentProgram(); - return program && program.forEachResolvedProjectReference(cb); + const loadKind = parentOptions.disableReferencedProjectLoad ? ProjectReferenceProjectLoadKind.Find : projectReferenceProjectLoadKind; + return forEach(resolvedProjectReferences, ref => { + if (!ref) return undefined; + + const configFileName = toNormalizedPath(ref.sourceFile.fileName); + const canonicalPath = projectService.toCanonicalFileName(configFileName); + const seenValue = seenResolvedRefs?.get(canonicalPath); + if (seenValue !== undefined && seenValue >= loadKind) { + return undefined; + } + const result = cb(ref, loadKind); + if (result) { + return result; + } + + (seenResolvedRefs || (seenResolvedRefs = new Map())).set(canonicalPath, loadKind); + return ref.references && forEachResolvedProjectReferenceProjectWorker(ref.references, ref.commandLine.options, cb, loadKind, projectService, seenResolvedRefs); + }); } function forEachPotentialProjectReference( @@ -514,12 +555,12 @@ namespace ts.server { function forEachAnyProjectReferenceKind( project: ConfiguredProject, - cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined, + cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined, cbProjectRef: (projectReference: ProjectReference) => T | undefined, cbPotentialProjectRef: (potentialProjectReference: Path) => T | undefined ): T | undefined { return project.getCurrentProgram() ? - forEachResolvedProjectReference(project, cb) : + project.forEachResolvedProjectReference(cb) : project.isInitialLoadPending() ? forEachPotentialProjectReference(project, cbPotentialProjectRef) : forEach(project.getProjectReferences(), cbProjectRef); @@ -540,8 +581,8 @@ namespace ts.server { ): T | undefined { return forEachAnyProjectReferenceKind( project, - resolvedRef => callbackRefProject(project, cb, resolvedRef && resolvedRef.sourceFile.path), - projectRef => callbackRefProject(project, cb, project.toPath(projectRef.path)), + resolvedRef => callbackRefProject(project, cb, resolvedRef.sourceFile.path), + projectRef => callbackRefProject(project, cb, project.toPath(resolveProjectReferencePath(projectRef))), potentialProjectRef => callbackRefProject(project, cb, potentialProjectRef) ); } @@ -2875,6 +2916,7 @@ namespace ts.server { if (!projectContainsInfoDirectly(project, info)) { const referencedProject = forEachResolvedProjectReferenceProject( project, + info.path, child => { reloadChildProject(child); return projectContainsInfoDirectly(child, info); @@ -2885,6 +2927,7 @@ namespace ts.server { // Reload the project's tree that is already present forEachResolvedProjectReferenceProject( project, + /*fileName*/ undefined, reloadChildProject, ProjectReferenceProjectLoadKind.Find ); @@ -2990,11 +3033,12 @@ namespace ts.server { // Find the project that is referenced from this solution that contains the script info directly configuredProject = forEachResolvedProjectReferenceProject( configuredProject, + fileName, child => { updateProjectIfDirty(child); return projectContainsOriginalInfo(child) ? child : undefined; }, - configuredProject.getCompilerOptions().disableReferencedProjectLoad ? ProjectReferenceProjectLoadKind.Find : ProjectReferenceProjectLoadKind.FindCreateLoad, + ProjectReferenceProjectLoadKind.FindCreateLoad, `Creating project referenced in solution ${configuredProject.projectName} to find possible configured project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}` ); if (!configuredProject) return undefined; @@ -3070,6 +3114,7 @@ namespace ts.server { if (!projectContainsInfoDirectly(project, info)) { forEachResolvedProjectReferenceProject( project, + info.path, child => { updateProjectIfDirty(child); // Retain these projects @@ -3184,32 +3229,37 @@ namespace ts.server { // Work on array copy as we could add more projects as part of callback for (const project of arrayFrom(this.configuredProjects.values())) { // If this project has potential project reference for any of the project we are loading ancestor tree for - // we need to load this project tree - if (forEachPotentialProjectReference( - project, - potentialRefPath => forProjects!.has(potentialRefPath) - ) || forEachResolvedProjectReference( - project, - (_ref, resolvedPath) => forProjects!.has(resolvedPath) - )) { - // Load children - this.ensureProjectChildren(project, seenProjects); + // load this project first + if (forEachPotentialProjectReference(project, potentialRefPath => forProjects!.has(potentialRefPath))) { + updateProjectIfDirty(project); } + this.ensureProjectChildren(project, forProjects, seenProjects); } } - private ensureProjectChildren(project: ConfiguredProject, seenProjects: Set) { + private ensureProjectChildren(project: ConfiguredProject, forProjects: ReadonlyCollection, seenProjects: Set) { if (!tryAddToSet(seenProjects, project.canonicalConfigFilePath)) return; - // Update the project - updateProjectIfDirty(project); - // Create tree because project is uptodate we only care of resolved references - forEachResolvedProjectReferenceProject( - project, - child => this.ensureProjectChildren(child, seenProjects), - ProjectReferenceProjectLoadKind.FindCreateLoad, - `Creating project for reference of project: ${project.projectName}` - ); + // If this project disables child load ignore it + if (project.getCompilerOptions().disableReferencedProjectLoad) return; + + const children = project.getCurrentProgram()?.getResolvedProjectReferences(); + if (!children) return; + + for (const child of children) { + if (!child) continue; + const referencedProject = forEachResolvedProjectReference(child.references, ref => forProjects.has(ref.sourceFile.path) ? ref : undefined); + if (!referencedProject) continue; + + // Load this project, + const configFileName = toNormalizedPath(child.sourceFile.fileName); + const childProject = project.projectService.findConfiguredProjectByProjectName(configFileName) || + project.projectService.createAndLoadConfiguredProject(configFileName, `Creating project referenced by : ${project.projectName} as it references project ${referencedProject.sourceFile.fileName}`); + updateProjectIfDirty(childProject); + + // Ensure children for this project + this.ensureProjectChildren(childProject, forProjects, seenProjects); + } } private cleanupAfterOpeningFile(toRetainConfigProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) { diff --git a/src/server/project.ts b/src/server/project.ts index d95e9e266dd..ee64647bdee 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -750,11 +750,8 @@ namespace ts.server { for (const f of this.program.getSourceFiles()) { this.detachScriptInfoIfNotRoot(f.fileName); } - this.program.forEachResolvedProjectReference(ref => { - if (ref) { - this.detachScriptInfoFromProject(ref.sourceFile.fileName); - } - }); + this.program.forEachResolvedProjectReference(ref => + this.detachScriptInfoFromProject(ref.sourceFile.fileName)); } // Release external files @@ -1099,8 +1096,8 @@ namespace ts.server { } } - oldProgram.forEachResolvedProjectReference((resolvedProjectReference, resolvedProjectReferencePath) => { - if (resolvedProjectReference && !this.program!.getResolvedProjectReferenceByPath(resolvedProjectReferencePath)) { + oldProgram.forEachResolvedProjectReference(resolvedProjectReference => { + if (!this.program!.getResolvedProjectReferenceByPath(resolvedProjectReference.sourceFile.path)) { this.detachScriptInfoFromProject(resolvedProjectReference.sourceFile.fileName); } }); @@ -2201,6 +2198,13 @@ namespace ts.server { return program && program.getResolvedProjectReferenceToRedirect(fileName); } + /*@internal*/ + forEachResolvedProjectReference( + cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined + ): T | undefined { + return this.getCurrentProgram()?.forEachResolvedProjectReference(cb); + } + /*@internal*/ enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: ESMap | undefined) { const host = this.projectService.host; @@ -2301,6 +2305,7 @@ namespace ts.server { getDefaultChildProjectFromProjectWithReferences(info: ScriptInfo) { return forEachResolvedProjectReferenceProject( this, + info.path, child => projectContainsInfoDirectly(child, info) ? child : undefined, @@ -2338,6 +2343,7 @@ namespace ts.server { return this.containsScriptInfo(info) || !!forEachResolvedProjectReferenceProject( this, + info.path, child => child.containsScriptInfo(info), ProjectReferenceProjectLoadKind.Find ); diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index 44ba7911a97..103143390fc 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -2189,19 +2189,14 @@ export function bar() {}` verifySolutionScenario({ configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2], - additionalProjects: [{ - projectName: tsconfigIndirect.path, - files: [tsconfigIndirect.path, main.path, helper.path, indirect.path, libFile.path] - }], + additionalProjects: emptyArray, expectedOpenEvents: [ ...expectedSolutionLoadAndTelemetry(), - ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect.path), ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), configFileDiagEvent(main.path, tsconfigSrcPath, []) ], expectedReloadEvents: [ ...expectedReloadEvent(tsconfigPath), - ...expectedReloadEvent(tsconfigIndirect.path), ...expectedReloadEvent(tsconfigSrcPath), ], expectedReferences: { @@ -2217,8 +2212,8 @@ export function bar() {}` refs: [ ...expectedIndirectRefs(fileResolvingToMainDts), ...refs, - ...expectedIndirectRefs(indirect2), ...expectedIndirectRefs(indirect), + ...expectedIndirectRefs(indirect2), ], symbolDisplayString: "(alias) const foo: 1\nimport foo", } @@ -2296,8 +2291,6 @@ export function bar() {}` const expectedProjectsOnOpen: VerifyProjects = { configuredProjects: [ { projectName: tsconfigPath, files: [tsconfigPath] }, - { projectName: tsconfigIndirect.path, files: [tsconfigIndirect.path, main.path, helper.path, indirect.path, libFile.path] }, - { projectName: tsconfigIndirect2.path, files: [tsconfigIndirect2.path, main.path, helper.path, indirect2.path, libFile.path] }, { projectName: tsconfigSrcPath, files: [tsconfigSrcPath, main.path, helper.path, libFile.path] }, ], inferredProjects: emptyArray @@ -2307,8 +2300,6 @@ export function bar() {}` additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2], expectedOpenEvents: [ ...expectedSolutionLoadAndTelemetry(), - ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect.path), - ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect2.path), ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), configFileDiagEvent(main.path, tsconfigSrcPath, []) ], @@ -2317,9 +2308,7 @@ export function bar() {}` expectedProjectsOnOpen, expectedReloadEvents: [ ...expectedReloadEvent(tsconfigPath), - ...expectedReloadEvent(tsconfigIndirect.path), ...expectedReloadEvent(tsconfigSrcPath), - ...expectedReloadEvent(tsconfigIndirect2.path), ] }); }); @@ -2387,19 +2376,14 @@ bar;` solutionProject: [tsconfigPath, indirect.path, ownMain.path, main.path, libFile.path, helper.path], configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2, ownMain], - additionalProjects: [{ - projectName: tsconfigIndirect.path, - files: [tsconfigIndirect.path, main.path, helper.path, indirect.path, libFile.path] - }], + additionalProjects: emptyArray, expectedOpenEvents: [ ...expectedSolutionLoadAndTelemetry(), - ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect.path), ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), configFileDiagEvent(main.path, tsconfigSrcPath, []) ], expectedReloadEvents: [ ...expectedReloadEvent(tsconfigPath), - ...expectedReloadEvent(tsconfigIndirect.path), ...expectedReloadEvent(tsconfigSrcPath), ], expectedReferences: { @@ -2415,8 +2399,8 @@ bar;` refs: [ ...expectedIndirectRefs(fileResolvingToMainDts), ...refs, - ...expectedIndirectRefs(indirect2), ...expectedIndirectRefs(indirect), + ...expectedIndirectRefs(indirect2), ], symbolDisplayString: "(alias) const foo: 1\nimport foo", } @@ -2502,8 +2486,6 @@ bar;` const expectedProjectsOnOpen: VerifyProjects = { configuredProjects: [ { projectName: tsconfigPath, files: [tsconfigPath, indirect.path, ownMain.path, main.path, libFile.path, helper.path] }, - { projectName: tsconfigIndirect.path, files: [tsconfigIndirect.path, main.path, helper.path, indirect.path, libFile.path] }, - { projectName: tsconfigIndirect2.path, files: [tsconfigIndirect2.path, main.path, helper.path, indirect2.path, libFile.path] }, { projectName: tsconfigSrcPath, files: [tsconfigSrcPath, main.path, helper.path, libFile.path] }, ], inferredProjects: emptyArray @@ -2518,8 +2500,6 @@ bar;` additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2, ownMain], expectedOpenEvents: [ ...expectedSolutionLoadAndTelemetry(), - ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect.path), - ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect2.path), ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), configFileDiagEvent(main.path, tsconfigSrcPath, []) ], @@ -2528,9 +2508,7 @@ bar;` expectedProjectsOnOpen, expectedReloadEvents: [ ...expectedReloadEvent(tsconfigPath), - ...expectedReloadEvent(tsconfigIndirect.path), ...expectedReloadEvent(tsconfigSrcPath), - ...expectedReloadEvent(tsconfigIndirect2.path), ] }); }); @@ -2645,5 +2623,75 @@ bar;` verifyAutoImport(/*built*/ true, /*disableSourceOfProjectReferenceRedirect*/ true); }); }); + + it("when files from two projects are open and one project references", () => { + function getPackageAndFile(packageName: string, references?: string[], optionsToExtend?: CompilerOptions): [file: File, config: File] { + const file: File = { + path: `${tscWatch.projectRoot}/${packageName}/src/file1.ts`, + content: `export const ${packageName}Const = 10;` + }; + const config: File = { + path: `${tscWatch.projectRoot}/${packageName}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, ...optionsToExtend || {} }, + references: references?.map(path => ({ path: `../${path}` })) + }) + }; + return [file, config]; + } + const [mainFile, mainConfig] = getPackageAndFile("main", ["core", "indirect", "noCoreRef1", "indirectDisabledChildLoad1", "indirectDisabledChildLoad2", "refToCoreRef3", "indirectNoCoreRef"]); + const [coreFile, coreConfig] = getPackageAndFile("core"); + const [noCoreRef1File, noCoreRef1Config] = getPackageAndFile("noCoreRef1"); + const [indirectFile, indirectConfig] = getPackageAndFile("indirect", ["coreRef1"]); + const [coreRef1File, coreRef1Config] = getPackageAndFile("coreRef1", ["core"]); + const [indirectDisabledChildLoad1File, indirectDisabledChildLoad1Config] = getPackageAndFile("indirectDisabledChildLoad1", ["coreRef2"], { disableReferencedProjectLoad: true }); + const [coreRef2File, coreRef2Config] = getPackageAndFile("coreRef2", ["core"]); + const [indirectDisabledChildLoad2File, indirectDisabledChildLoad2Config] = getPackageAndFile("indirectDisabledChildLoad2", ["coreRef3"], { disableReferencedProjectLoad: true }); + const [coreRef3File, coreRef3Config] = getPackageAndFile("coreRef3", ["core"]); + const [refToCoreRef3File, refToCoreRef3Config] = getPackageAndFile("refToCoreRef3", ["coreRef3"]); + const [indirectNoCoreRefFile, indirectNoCoreRefConfig] = getPackageAndFile("indirectNoCoreRef", ["noCoreRef2"]); + const [noCoreRef2File, noCoreRef2Config] = getPackageAndFile("noCoreRef2"); + + const host = createServerHost([ + libFile, mainFile, mainConfig, coreFile, coreConfig, noCoreRef1File, noCoreRef1Config, + indirectFile, indirectConfig, coreRef1File, coreRef1Config, + indirectDisabledChildLoad1File, indirectDisabledChildLoad1Config, coreRef2File, coreRef2Config, + indirectDisabledChildLoad2File, indirectDisabledChildLoad2Config, coreRef3File, coreRef3Config, + refToCoreRef3File, refToCoreRef3Config, + indirectNoCoreRefFile, indirectNoCoreRefConfig, noCoreRef2File, noCoreRef2Config + ], { useCaseSensitiveFileNames: true }); + const session = createSession(host); + const service = session.getProjectService(); + openFilesForSession([mainFile, coreFile], session); + + verifyProject(mainConfig); + verifyProject(coreConfig); + + // Find all refs in coreFile + session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: protocolFileLocationFromSubstring(coreFile, `coreConst`) + }); + verifyProject(mainConfig); + verifyProject(coreConfig); + verifyNoProject(noCoreRef1Config); // Should not be loaded + verifyProject(indirectConfig); + verifyProject(coreRef1Config); + verifyProject(indirectDisabledChildLoad1Config); + verifyNoProject(coreRef2Config); // Should not be loaded + verifyProject(indirectDisabledChildLoad2Config); + verifyProject(coreRef3Config); + verifyProject(refToCoreRef3Config); + verifyNoProject(indirectNoCoreRefConfig); // Should not be loaded + verifyNoProject(noCoreRef2Config); // Should not be loaded + + function verifyProject(config: File) { + assert.isDefined(service.configuredProjects.get(config.path), `Expected to find ${config.path}`); + } + + function verifyNoProject(config: File) { + assert.isUndefined(service.configuredProjects.get(config.path), `Expected to not find ${config.path}`); + } + }); }); }