Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle if project for open file will get recollected because of pending cleanup from closed script info #50908

Merged
merged 2 commits into from Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/server/editorServices.ts
Expand Up @@ -1167,20 +1167,22 @@ namespace ts.server {
}

/* @internal */
tryGetDefaultProjectForFile(fileName: NormalizedPath): Project | undefined {
const scriptInfo = this.getScriptInfoForNormalizedPath(fileName);
tryGetDefaultProjectForFile(fileNameOrScriptInfo: NormalizedPath | ScriptInfo): Project | undefined {
const scriptInfo = isString(fileNameOrScriptInfo) ? this.getScriptInfoForNormalizedPath(fileNameOrScriptInfo) : fileNameOrScriptInfo;
return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : undefined;
}

/* @internal */
ensureDefaultProjectForFile(fileName: NormalizedPath): Project {
return this.tryGetDefaultProjectForFile(fileName) || this.doEnsureDefaultProjectForFile(fileName);
ensureDefaultProjectForFile(fileNameOrScriptInfo: NormalizedPath | ScriptInfo): Project {
return this.tryGetDefaultProjectForFile(fileNameOrScriptInfo) || this.doEnsureDefaultProjectForFile(fileNameOrScriptInfo);
}

private doEnsureDefaultProjectForFile(fileName: NormalizedPath): Project {
private doEnsureDefaultProjectForFile(fileNameOrScriptInfo: NormalizedPath | ScriptInfo): Project {
this.ensureProjectStructuresUptoDate();
const scriptInfo = this.getScriptInfoForNormalizedPath(fileName);
return scriptInfo ? scriptInfo.getDefaultProject() : (this.logErrorForScriptInfoNotFound(fileName), Errors.ThrowNoProject());
const scriptInfo = isString(fileNameOrScriptInfo) ? this.getScriptInfoForNormalizedPath(fileNameOrScriptInfo) : fileNameOrScriptInfo;
return scriptInfo ?
scriptInfo.getDefaultProject() :
(this.logErrorForScriptInfoNotFound(isString(fileNameOrScriptInfo) ? fileNameOrScriptInfo : fileNameOrScriptInfo.fileName), Errors.ThrowNoProject());
}

getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) {
Expand Down Expand Up @@ -3648,7 +3650,7 @@ namespace ts.server {
return;
}

const project = scriptInfo.getDefaultProject();
const project = this.ensureDefaultProjectForFile(scriptInfo);
if (!project.languageServiceEnabled) {
return;
}
Expand Down
12 changes: 5 additions & 7 deletions src/server/session.ts
Expand Up @@ -1719,6 +1719,10 @@ namespace ts.server {
this.projectService.logErrorForScriptInfoNotFound(args.file);
return Errors.ThrowNoProject();
}
else if (!getScriptInfoEnsuringProjectsUptoDate) {
// Ensure there are containing projects are present
this.projectService.ensureDefaultProjectForFile(scriptInfo);
}
projects = scriptInfo.containingProjects;
symLinkedProjects = this.projectService.getSymlinkedProjects(scriptInfo);
}
Expand Down Expand Up @@ -1867,13 +1871,7 @@ namespace ts.server {
}

private getFileAndLanguageServiceForSyntacticOperation(args: protocol.FileRequestArgs) {
// Since this is syntactic operation, there should always be project for the file
// throw if we dont get project
const file = toNormalizedPath(args.file);
const project = this.getProject(args.projectFileName) || this.projectService.ensureDefaultProjectForFile(file);
if (!project) {
return Errors.ThrowNoProject();
}
const { file, project } = this.getFileAndProject(args);
return {
file,
languageService: project.getLanguageService(/*ensureSynchronized*/ false)
Expand Down
1 change: 1 addition & 0 deletions src/testRunner/unittests/tsserver/helpers.ts
Expand Up @@ -95,6 +95,7 @@ namespace ts.projectSystem {
function msg(s: string, type = server.Msg.Err, write: (s: string) => void) {
s = `[${nowString()}] ${s}`;
if (!inGroup || firstInGroup) s = padStringRight(type + " " + seq.toString(), " ") + s;
if (Debug.isDebugging) console.log(s);
write(s);
if (!inGroup) seq++;
}
Expand Down
110 changes: 79 additions & 31 deletions src/testRunner/unittests/tsserver/projects.ts
Expand Up @@ -1610,39 +1610,87 @@ namespace ts.projectSystem {
checkNumberOfInferredProjects(projectService, 0);
});

it("file opened is in configured project that will be removed", () => {
const testsConfig: File = {
path: `${tscWatch.projectRoot}/playground/tsconfig.json`,
content: "{}"
};
const testsFile: File = {
path: `${tscWatch.projectRoot}/playground/tests.ts`,
content: `export function foo() {}`
};
const innerFile: File = {
path: `${tscWatch.projectRoot}/playground/tsconfig-json/tests/spec.ts`,
content: `export function bar() { }`
};
const innerConfig: File = {
path: `${tscWatch.projectRoot}/playground/tsconfig-json/tsconfig.json`,
content: JSON.stringify({
include: ["./src"]
describe("file opened is in configured project that will be removed", () => {
function runOnTs<T extends server.protocol.Request>(scenario: string, getRequest: (innerFile: File) => Partial<T>) {
it(scenario, () => {
const testsConfig: File = {
path: `${tscWatch.projectRoot}/playground/tsconfig.json`,
content: "{}"
};
const testsFile: File = {
path: `${tscWatch.projectRoot}/playground/tests.ts`,
content: `export function foo() {}`
};
const innerFile: File = {
path: `${tscWatch.projectRoot}/playground/tsconfig-json/tests/spec.ts`,
content: `export function bar() { }`
};
const innerConfig: File = {
path: `${tscWatch.projectRoot}/playground/tsconfig-json/tsconfig.json`,
content: JSON.stringify({
include: ["./src"]
})
};
const innerSrcFile: File = {
path: `${tscWatch.projectRoot}/playground/tsconfig-json/src/src.ts`,
content: `export function foobar() { }`
};
const host = createServerHost([testsConfig, testsFile, innerFile, innerConfig, innerSrcFile, libFile]);
const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
openFilesForSession([testsFile], session);
closeFilesForSession([testsFile], session);
openFilesForSession([innerFile], session);
session.executeCommandSeq(getRequest(innerFile));
baselineTsserverLogs("projects", scenario, session);
});
}
runOnTs<protocol.OutliningSpansRequest>(
"file opened is in configured project that will be removed",
innerFile => ({
command: protocol.CommandTypes.GetOutliningSpans,
arguments: { file: innerFile.path }
})
};
const innerSrcFile: File = {
path: `${tscWatch.projectRoot}/playground/tsconfig-json/src/src.ts`,
content: `export function foobar() { }`
};
const host = createServerHost([testsConfig, testsFile, innerFile, innerConfig, innerSrcFile, libFile]);
const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
openFilesForSession([testsFile], session);
closeFilesForSession([testsFile], session);
openFilesForSession([innerFile], session);
session.executeCommandSeq<protocol.OutliningSpansRequest>({
command: protocol.CommandTypes.GetOutliningSpans,
arguments: { file: innerFile.path }
);

runOnTs<protocol.ReferencesRequest>(
"references on file opened is in configured project that will be removed",
innerFile => ({
command: protocol.CommandTypes.References,
arguments: protocolFileLocationFromSubstring(innerFile, "bar")
})
);

it("js file opened is in configured project that will be removed", () => {
const rootConfig: File = {
path: `${tscWatch.projectRoot}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { allowJs: true } })
};
const mocksFile: File = {
path: `${tscWatch.projectRoot}/mocks/cssMock.js`,
content: `function foo() { }`
};
const innerFile: File = {
path: `${tscWatch.projectRoot}/apps/editor/scripts/createConfigVariable.js`,
content: `function bar() { }`
};
const innerConfig: File = {
path: `${tscWatch.projectRoot}/apps/editor/tsconfig.json`,
content: JSON.stringify({
extends: "../../tsconfig.json",
include: ["./src"],
})
};
const innerSrcFile: File = {
path: `${tscWatch.projectRoot}/apps/editor/src/src.js`,
content: `function fooBar() { }`
};
const host = createServerHost([rootConfig, mocksFile, innerFile, innerConfig, innerSrcFile, libFile]);
const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) });
openFilesForSession([mocksFile], session);
closeFilesForSession([mocksFile], session);
openFilesForSession([innerFile], session);
baselineTsserverLogs("projects", "js file opened is in configured project that will be removed", session);
});
baselineTsserverLogs("projects", "file opened is in configured project that will be removed", session);
});
});
}