From d65cb21fcde8f365688db4196c5d89d7b6902c11 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 27 Jul 2018 15:35:13 -0700 Subject: [PATCH] Keep the original configured project alive created during findAllReferences --- src/server/editorServices.ts | 62 +++++++-- src/server/project.ts | 3 + .../unittests/tsserverProjectSystem.ts | 124 ++++++++++++++---- 3 files changed, 148 insertions(+), 41 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 00905a28579..3ea6b45047c 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -707,7 +707,7 @@ namespace ts.server { /* @internal */ private forEachProject(cb: (project: Project) => void) { - for (const p of this.inferredProjects) cb(p); + this.inferredProjects.forEach(cb); this.configuredProjects.forEach(cb); this.externalProjects.forEach(cb); } @@ -2110,19 +2110,34 @@ namespace ts.server { if (!originalLocation) return undefined; const { fileName } = originalLocation; - const originalScriptInfo = this.getScriptInfo(fileName); - if (originalScriptInfo && originalScriptInfo.containingProjects.length) { - return originalLocation; - } + if (!this.getScriptInfo(fileName) && !this.host.fileExists(fileName)) return undefined; - const info: OriginalFileInfo = { fileName: toNormalizedPath(fileName), path: this.toPath(fileName) }; - const configFileName = this.getConfigFileNameForFile(info); + const originalFileInfo: OriginalFileInfo = { fileName: toNormalizedPath(fileName), path: this.toPath(fileName) }; + const configFileName = this.getConfigFileNameForFile(originalFileInfo); if (!configFileName) return undefined; const configuredProject = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName); updateProjectIfDirty(configuredProject); + // Keep this configured project as referenced from project + addOriginalConfiguredProject(configuredProject); - return configuredProject.containsFile(info.fileName) ? originalLocation : undefined; + const originalScriptInfo = this.getScriptInfo(fileName); + if (!originalScriptInfo || !originalScriptInfo.containingProjects.length) return undefined; + + // Add configured projects as referenced + originalScriptInfo.containingProjects.forEach(project => { + if (project.projectKind === ProjectKind.Configured) { + addOriginalConfiguredProject(project as ConfiguredProject); + } + }); + return originalLocation; + + function addOriginalConfiguredProject(originalProject: ConfiguredProject) { + if (!project.originalConfiguredProjects) { + project.originalConfiguredProjects = createMap(); + } + project.originalConfiguredProjects.set(originalProject.canonicalConfigFilePath, true); + } } /** @internal */ @@ -2185,14 +2200,9 @@ namespace ts.server { } Debug.assert(!info.isOrphan()); - // Remove the configured projects that have zero references from open files. // This was postponed from closeOpenFile to after opening next file, // so that we can reuse the project if we need to right away - this.configuredProjects.forEach(project => { - if (!project.hasOpenRef()) { - this.removeProject(project); - } - }); + this.removeOrphanConfiguredProjects(); // Remove orphan inferred projects now that we have reused projects // We need to create a duplicate because we cant guarantee order after removal @@ -2220,6 +2230,30 @@ namespace ts.server { return { configFileName, configFileErrors }; } + private removeOrphanConfiguredProjects() { + const toRemoveConfiguredProjects = cloneMap(this.configuredProjects); + + // Do not remove configured projects that are used as original projects of other + this.inferredProjects.forEach(markOriginalProjectsAsUsed); + this.externalProjects.forEach(markOriginalProjectsAsUsed); + this.configuredProjects.forEach(project => { + // 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 + if (project.hasOpenRef()) { + toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath); + markOriginalProjectsAsUsed(project); + } + }); + + // Remove all the non marked projects + toRemoveConfiguredProjects.forEach(project => this.removeProject(project)); + + function markOriginalProjectsAsUsed(project: Project) { + if (!project.isOrphan() && project.originalConfiguredProjects) { + project.originalConfiguredProjects.forEach((_value, configuredProjectPath) => toRemoveConfiguredProjects.delete(configuredProjectPath)); + } + } + } + private telemetryOnOpenFile(scriptInfo: ScriptInfo): void { if (this.syntaxOnly || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) { return; diff --git a/src/server/project.ts b/src/server/project.ts index decb31097a9..d26ee5d79e5 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -158,6 +158,9 @@ namespace ts.server { /*@internal*/ typingFiles: SortedReadonlyArray = emptyArray; + /*@internal*/ + originalConfiguredProjects: Map | undefined; + private readonly cancellationToken: ThrottledCancellationToken; public isNonTsProject() { diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index d23c2f288f3..1eab4e38aeb 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -9110,7 +9110,7 @@ export function Test2() { }); }); - function makeSampleProjects() { + describe("tsserverProjectSystem project references", () => { const aTs: File = { path: "/a/a.ts", content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", @@ -9166,62 +9166,121 @@ export function Test2() { content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`, }; + const dummyFile: File = { + path: "/dummy/dummy.ts", + content: "let a = 10;" + }; + const userTs: File = { path: "/user/user.ts", content: 'import { fnA, instanceA } from "../a/bin/a";\nimport { fnB } from "../b/bin/b";\nexport function fnUser() { fnA(); fnB(); instanceA; }', }; - const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, userTs]); - const session = createSession(host); + function makeSampleProjects() { + const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, userTs, dummyFile]); + const session = createSession(host); - checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); - checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); - // Testing what happens if we delete the original sources. - host.removeFile(bTs.path); + // Testing what happens if we delete the original sources. + host.removeFile(bTs.path); - openFilesForSession([userTs], session); + openFilesForSession([userTs], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + return session; + } - return { session, aTs, bDts, userTs }; - } + function verifyInferredProjectUnchanged(session: TestSession) { + checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]); + } + + function verifyDummyProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]); + } + + function verifyOnlyOrphanInferredProject(session: TestSession) { + openFilesForSession([dummyFile], session); + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyDummyProject(session); + } + + function verifySingleInferredProject(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyInferredProjectUnchanged(session); + + // Close user file should close all the projects after opening dummy file + closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } + + function verifyATsConfigProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]); + } + + function verifyATsConfigOriginalProject(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + // Close user file should close all the projects + closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } + + function verifyATsConfigWhenOpened(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + + closeFilesForSession([userTs], session); + openFilesForSession([dummyFile], session); + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyDummyProject(session); + verifyATsConfigProject(session); // ATsConfig should still be alive + } - describe("tsserverProjectSystem project references", () => { it("goToDefinition", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.Definition, protocolFileLocationFromSubstring(userTs, "fnA()")); assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]); + verifySingleInferredProject(session); }); it("getDefinitionAndBoundSpan", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); assert.deepEqual(response, { textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA", { index: 1 }), definitions: [protocolFileSpanFromSubstring(aTs, "fnA")], }); + verifySingleInferredProject(session); }); it("goToType", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.TypeDefinition, protocolFileLocationFromSubstring(userTs, "instanceA")); assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "IfaceA")]); + verifySingleInferredProject(session); }); it("goToImplementation", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.Implementation, protocolFileLocationFromSubstring(userTs, "fnA()")); assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]); + verifySingleInferredProject(session); }); it("goToDefinition -- target does not exist", () => { - const { session, bDts, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, CommandNames.Definition, protocolFileLocationFromSubstring(userTs, "fnB()")); // bTs does not exist, so stick with bDts assert.deepEqual(response, [protocolFileSpanFromSubstring(bDts, "fnB")]); + verifySingleInferredProject(session); }); it("navigateTo", () => { - const { session, aTs, bDts, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" }); assert.deepEqual | undefined>(response, [ { @@ -9249,6 +9308,8 @@ export function Test2() { kindModifiers: "export", }, ]); + + verifyATsConfigOriginalProject(session); }); const referenceATs = (aTs: File): protocol.ReferencesResponseItem => makeReferenceItem(aTs, /*isDefinition*/ true, "fnA", "export function fnA() {}"); @@ -9258,7 +9319,7 @@ export function Test2() { ]; it("findAllReferences", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()")); assert.deepEqual(response, { @@ -9267,10 +9328,12 @@ export function Test2() { symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset, symbolDisplayString: "(alias) fnA(): void\nimport fnA", }); + + verifyATsConfigOriginalProject(session); }); it("findAllReferences -- starting at definition", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(aTs, "fnA")); assert.deepEqual(response, { @@ -9279,10 +9342,11 @@ export function Test2() { symbolStartOffset: protocolLocationFromSubstring(aTs.content, "fnA").offset, symbolDisplayString: "function fnA(): void", }); + verifyATsConfigWhenOpened(session); }); it("findAllReferencesFull", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); interface ReferencesFullRequest extends protocol.FileLocationRequest { command: protocol.CommandTypes.ReferencesFull; } interface ReferencesFullResponse extends protocol.Response { body: ReadonlyArray; } @@ -9339,10 +9403,11 @@ export function Test2() { ], } ]); + verifyATsConfigOriginalProject(session); }); it("findAllReferences -- target does not exist", () => { - const { session, bDts, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnB()")); assert.deepEqual(response, { @@ -9355,6 +9420,7 @@ export function Test2() { symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnB()").offset, symbolDisplayString: "(alias) fnB(): void\nimport fnB", }); + verifySingleInferredProject(session); }); const renameATs = (aTs: File): protocol.SpanGroup => ({ @@ -9370,7 +9436,7 @@ export function Test2() { }); it("renameLocations", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnA()")); assert.deepEqual(response, { info: { @@ -9383,10 +9449,11 @@ export function Test2() { }, locs: [renameUserTs(userTs), renameATs(aTs)], }); + verifyATsConfigOriginalProject(session); }); it("renameLocations -- starting at definition", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "fnA")); assert.deepEqual(response, { @@ -9400,20 +9467,22 @@ export function Test2() { }, locs: [renameATs(aTs), renameUserTs(userTs)], }); + verifyATsConfigWhenOpened(session); }); it("renameLocationsFull", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.RenameLocationsFull, protocolFileLocationFromSubstring(userTs, "fnA()")); assert.deepEqual>(response, [ documentSpanFromSubstring(userTs, "fnA"), documentSpanFromSubstring(userTs, "fnA", { index: 1 }), documentSpanFromSubstring(aTs, "fnA"), ]); + verifyATsConfigOriginalProject(session); }); it("renameLocations -- target does not exist", () => { - const { session, bDts, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnB()")); assert.deepEqual(response, { info: { @@ -9438,11 +9507,11 @@ export function Test2() { } ], }); - + verifySingleInferredProject(session); }); it("getEditsForFileRename", () => { - const { session, aTs, userTs } = makeSampleProjects(); + const session = makeSampleProjects(); const response = executeSessionRequest(session, protocol.CommandTypes.GetEditsForFileRename, { oldFilePath: aTs.path, newFilePath: "/a/aNew.ts", @@ -9455,6 +9524,7 @@ export function Test2() { ], }, ]); + verifySingleInferredProject(session); }); });