diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 669194b4e8981..8038e7276aef6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1775,11 +1775,9 @@ namespace ts.server { const project = configFileName && this.findConfiguredProjectByProjectName(configFileName); - return project?.isSolution() ? - project.getDefaultChildProjectFromSolution(info) : - project && projectContainsInfoDirectly(project, info) ? - project : - undefined; + return project && projectContainsInfoDirectly(project, info) ? + project : + project?.getDefaultChildProjectFromProjectWithReferences(info); } /** @@ -2826,8 +2824,8 @@ namespace ts.server { else { // reload from the disk this.reloadConfiguredProject(project, reason); - // If this is solution, reload the project till the reloaded project contains the script info directly - if (!project.containsScriptInfo(info) && project.isSolution()) { + // If this project does not contain this file directly, reload the project till the reloaded project contains the script info directly + if (!projectContainsInfoDirectly(project, info)) { const referencedProject = forEachResolvedProjectReferenceProject( project, child => { @@ -2936,14 +2934,18 @@ namespace ts.server { this.createAndLoadConfiguredProject(configFileName, `Creating project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}`); updateProjectIfDirty(configuredProject); - if (configuredProject.isSolution()) { + const projectContainsOriginalInfo = (project: ConfiguredProject) => { + const info = this.getScriptInfo(fileName); + return info && projectContainsInfoDirectly(project, info); + }; + + if (configuredProject.isSolution() || !projectContainsOriginalInfo(configuredProject)) { // Find the project that is referenced from this solution that contains the script info directly configuredProject = forEachResolvedProjectReferenceProject( configuredProject, child => { updateProjectIfDirty(child); - const info = this.getScriptInfo(fileName); - return info && projectContainsInfoDirectly(child, info) ? child : undefined; + return projectContainsOriginalInfo(child) ? child : undefined; }, configuredProject.getCompilerOptions().disableReferencedProjectLoad ? ProjectReferenceProjectLoadKind.Find : 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 : ""}` @@ -3026,7 +3028,7 @@ namespace ts.server { // If this configured project doesnt contain script info but // it is solution with project references, try those project references - if (project.isSolution()) { + if (!projectContainsInfoDirectly(project, info)) { forEachResolvedProjectReferenceProject( project, child => { @@ -3151,10 +3153,10 @@ namespace ts.server { if (forEachPotentialProjectReference( project, potentialRefPath => forProjects!.has(potentialRefPath) - ) || (project.isSolution() && forEachResolvedProjectReference( + ) || forEachResolvedProjectReference( project, (_ref, resolvedPath) => forProjects!.has(resolvedPath) - ))) { + )) { // Load children this.ensureProjectChildren(project, seenProjects); } diff --git a/src/server/project.ts b/src/server/project.ts index f87b67505c213..895ca04b4fc27 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -2225,9 +2225,8 @@ namespace ts.server { } /* @internal */ - /** Find the configured project from the project references in this solution which contains the info directly */ - getDefaultChildProjectFromSolution(info: ScriptInfo) { - Debug.assert(this.isSolution()); + /** Find the configured project from the project references in project which contains the info directly */ + getDefaultChildProjectFromProjectWithReferences(info: ScriptInfo) { return forEachResolvedProjectReferenceProject( this, child => projectContainsInfoDirectly(child, info) ? @@ -2257,8 +2256,6 @@ namespace ts.server { return !!configFileExistenceInfo.openFilesImpactedByConfigFile.size; } - const isSolution = this.isSolution(); - // If there is no pending update for this project, // We know exact set of open files that get impacted by this configured project as the files in the project // The project is referenced only if open files impacted by this project are present in this project @@ -2266,13 +2263,12 @@ namespace ts.server { configFileExistenceInfo.openFilesImpactedByConfigFile, (_value, infoPath) => { const info = this.projectService.getScriptInfoForPath(infoPath)!; - return isSolution ? + return this.containsScriptInfo(info) || !!forEachResolvedProjectReferenceProject( this, child => child.containsScriptInfo(info), ProjectReferenceProjectLoadKind.Find - ) : - this.containsScriptInfo(info); + ); } ) || false; } diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index c8629a743be6a..44ba7911a97df 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -1827,11 +1827,13 @@ bar(); describe("when default project is solution project", () => { interface Setup { solutionOptions?: CompilerOptions; + solutionFiles?: string[]; configRefs: string[]; additionalFiles: readonly File[]; expectedOpenEvents: protocol.Event[]; } interface VerifySolutionScenario extends Setup { + solutionProject?: readonly string[]; additionalProjects: readonly { projectName: string, files: readonly string[] }[]; expectedReloadEvents: protocol.Event[]; expectedReferences: protocol.ReferencesResponseBody; @@ -1876,12 +1878,13 @@ export { foo }; const fileResolvingToMainDts: File = { path: `${tscWatch.projectRoot}/indirect3/main.ts`, content: `import { foo } from 'main'; -foo;` +foo; +export function bar() {}` }; const tsconfigSrcPath = `${tscWatch.projectRoot}/tsconfig-src.json`; const tsconfigPath = `${tscWatch.projectRoot}/tsconfig.json`; const dummyFilePath = "/dummy/dummy.ts"; - function setup({ solutionOptions, configRefs, additionalFiles, expectedOpenEvents }: Setup) { + function setup({ solutionFiles, solutionOptions, configRefs, additionalFiles, expectedOpenEvents }: Setup) { const tsconfigSrc: File = { path: tsconfigSrcPath, content: JSON.stringify({ @@ -1898,7 +1901,7 @@ foo;` content: JSON.stringify({ ... (solutionOptions ? { compilerOptions: solutionOptions } : {}), references: configRefs.map(path => ({ path })), - files: [] + files: solutionFiles || [] }) }; const dummyFile: File = { @@ -1921,7 +1924,7 @@ foo;` function verifySolutionScenario(input: VerifySolutionScenario) { const { session, service, host, tsconfigSrc, tsconfig } = setup(input); const { - additionalProjects, expectedReloadEvents, + solutionProject, additionalProjects, expectedReloadEvents, expectedReferences, expectedReferencesFromDtsProject } = input; verifyProjects(/*includeConfigured*/ true, /*includeDummy*/ false); @@ -1981,7 +1984,7 @@ foo;` checkNumberOfProjects(service, { configuredProjects, inferredProjects }); if (includeConfigured) { checkProjectActualFiles(service.configuredProjects.get(tsconfigSrc.path)!, [tsconfigSrc.path, main.path, helper.path, libFile.path]); - checkProjectActualFiles(service.configuredProjects.get(tsconfig.path)!, [tsconfig.path]); + checkProjectActualFiles(service.configuredProjects.get(tsconfig.path)!, solutionProject || [tsconfig.path]); additionalProjects.forEach(({ projectName, files }) => checkProjectActualFiles(service.configuredProjects.get(projectName)!, files)); } @@ -2320,6 +2323,218 @@ foo;` ] }); }); + + describe("when solution is project that contains its own files", () => { + it("when the project found is not solution but references open file through project reference", () => { + const ownMain: File = { + path: `${tscWatch.projectRoot}/own/main.ts`, + content: fileResolvingToMainDts.content + }; + const { refs, ...rest } = expectedReferencesResponse(); + verifySolutionScenario({ + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./src/" + }, + solutionProject: [tsconfigPath, ownMain.path, main.path, libFile.path, helper.path], + configRefs: ["./tsconfig-src.json"], + additionalFiles: [ownMain], + additionalProjects: emptyArray, + expectedOpenEvents: [ + ...expectedSolutionLoadAndTelemetry(), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), + configFileDiagEvent(main.path, tsconfigSrcPath, []) + ], + expectedReloadEvents: [ + ...expectedReloadEvent(tsconfigPath), + ...expectedReloadEvent(tsconfigSrcPath), + ], + expectedReferences: { + refs: [ + ...refs, + ...expectedIndirectRefs(ownMain), + ], + ...rest + }, + expectedReferencesFromDtsProject: { + ...rest, + refs: [ + ...expectedIndirectRefs(fileResolvingToMainDts), + ...refs, + ...expectedIndirectRefs(ownMain), + ], + symbolDisplayString: "(alias) const foo: 1\nimport foo", + }, + }); + }); + + it("when project is indirectly referenced by solution", () => { + const ownMain: File = { + path: `${tscWatch.projectRoot}/own/main.ts`, + content: `import { bar } from 'main'; +bar;` + }; + const { tsconfigIndirect, indirect } = getIndirectProject("1"); + const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); + const { refs, ...rest } = expectedReferencesResponse(); + verifySolutionScenario({ + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./indirect1/" + }, + 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] + }], + expectedOpenEvents: [ + ...expectedSolutionLoadAndTelemetry(), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect.path), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), + configFileDiagEvent(main.path, tsconfigSrcPath, []) + ], + expectedReloadEvents: [ + ...expectedReloadEvent(tsconfigPath), + ...expectedReloadEvent(tsconfigIndirect.path), + ...expectedReloadEvent(tsconfigSrcPath), + ], + expectedReferences: { + refs: [ + ...refs, + ...expectedIndirectRefs(indirect), + ...expectedIndirectRefs(indirect2), + ], + ...rest + }, + expectedReferencesFromDtsProject: { + ...rest, + refs: [ + ...expectedIndirectRefs(fileResolvingToMainDts), + ...refs, + ...expectedIndirectRefs(indirect2), + ...expectedIndirectRefs(indirect), + ], + symbolDisplayString: "(alias) const foo: 1\nimport foo", + } + }); + }); + + it("disables looking into the child project if disableReferencedProjectLoad is set", () => { + const ownMain: File = { + path: `${tscWatch.projectRoot}/own/main.ts`, + content: fileResolvingToMainDts.content + }; + const expectedProjectsOnOpen: VerifyProjects = { + configuredProjects: [ + { projectName: tsconfigPath, files: [tsconfigPath, ownMain.path, main.path, libFile.path, helper.path] }, + ], + inferredProjects: emptyArray + }; + verifyDisableReferencedProjectLoad({ + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./src/", + disableReferencedProjectLoad: true + }, + configRefs: ["./tsconfig-src.json"], + additionalFiles: [ownMain], + expectedOpenEvents: [ + ...expectedSolutionLoadAndTelemetry(), + configFileDiagEvent(main.path, tsconfigPath, []) + ], + expectedDefaultProject: service => service.configuredProjects.get(tsconfigPath)!, + expectedDefaultConfiguredProject: returnUndefined, + expectedProjectsOnOpen, + expectedReloadEvents: expectedReloadEvent(tsconfigPath) + }); + }); + + it("disables looking into the child project if disableReferencedProjectLoad is set in indirect project", () => { + const ownMain: File = { + path: `${tscWatch.projectRoot}/own/main.ts`, + content: `import { bar } from 'main'; +bar;` + }; + const { tsconfigIndirect, indirect } = getIndirectProject("1", { disableReferencedProjectLoad: true }); + 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] }, + ], + inferredProjects: emptyArray + }; + verifyDisableReferencedProjectLoad({ + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./indirect1/", + }, + configRefs: ["./tsconfig-indirect1.json"], + additionalFiles: [tsconfigIndirect, indirect, ownMain], + expectedOpenEvents: [ + ...expectedSolutionLoadAndTelemetry(), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect.path), + configFileDiagEvent(main.path, tsconfigPath, []) + ], + expectedDefaultProject: service => service.configuredProjects.get(tsconfigPath)!, + expectedDefaultConfiguredProject: returnUndefined, + expectedProjectsOnOpen, + expectedReloadEvents: [ + ...expectedReloadEvent(tsconfigPath), + ...expectedReloadEvent(tsconfigIndirect.path), + ] + }); + }); + + it("disables looking into the child project if disableReferencedProjectLoad is set in first indirect project but not in another one", () => { + const ownMain: File = { + path: `${tscWatch.projectRoot}/own/main.ts`, + content: `import { bar } from 'main'; +bar;` + }; + const { tsconfigIndirect, indirect } = getIndirectProject("1", { disableReferencedProjectLoad: true }); + const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); + 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 + }; + verifyDisableReferencedProjectLoad({ + solutionFiles: [`./own/main.ts`], + solutionOptions: { + outDir: "./target/", + baseUrl: "./indirect1/", + }, + configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], + additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2, ownMain], + expectedOpenEvents: [ + ...expectedSolutionLoadAndTelemetry(), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect.path), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect2.path), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), + configFileDiagEvent(main.path, tsconfigSrcPath, []) + ], + expectedDefaultProject: service => service.configuredProjects.get(tsconfigSrcPath)!, + expectedDefaultConfiguredProject: service => service.configuredProjects.get(tsconfigSrcPath)!, + expectedProjectsOnOpen, + expectedReloadEvents: [ + ...expectedReloadEvent(tsconfigPath), + ...expectedReloadEvent(tsconfigIndirect.path), + ...expectedReloadEvent(tsconfigSrcPath), + ...expectedReloadEvent(tsconfigIndirect2.path), + ] + }); + }); + }); }); describe("auto import with referenced project", () => {