Add separate flag serverMode for server mode (#39735)

* Add separate flag serverMode for server mode to allow back compatibility

* Addressed code review feedback.

Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>
This commit is contained in:
Sheetal Nandi
2020-08-02 18:18:26 -07:00
committed by GitHub
parent 86a87c493c
commit aee78acedb
12 changed files with 470 additions and 104 deletions

View File

@@ -145,6 +145,7 @@
"unittests/tscWatch/watchApi.ts",
"unittests/tscWatch/watchEnvironment.ts",
"unittests/tsserver/applyChangesToOpenFiles.ts",
"unittests/tsserver/approximateSemanticOnlyServer.ts",
"unittests/tsserver/autoImportProvider.ts",
"unittests/tsserver/cachingFileSystemInformation.ts",
"unittests/tsserver/cancellationToken.ts",

View File

@@ -0,0 +1,217 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: Semantic operations on Approximate Semantic only server", () => {
function setup() {
const file1: File = {
path: `${tscWatch.projectRoot}/a.ts`,
content: `import { y, cc } from "./b";
import { something } from "something";
class c { prop = "hello"; foo() { return this.prop; } }`
};
const file2: File = {
path: `${tscWatch.projectRoot}/b.ts`,
content: `export { cc } from "./c";
import { something } from "something";
export const y = 10;`
};
const file3: File = {
path: `${tscWatch.projectRoot}/c.ts`,
content: `export const cc = 10;`
};
const something: File = {
path: `${tscWatch.projectRoot}/node_modules/something/index.d.ts`,
content: "export const something = 10;"
};
const configFile: File = {
path: `${tscWatch.projectRoot}/tsconfig.json`,
content: "{}"
};
const host = createServerHost([file1, file2, file3, something, libFile, configFile]);
const session = createSession(host, { serverMode: LanguageServiceMode.ApproximateSemanticOnly, useSingleInferredProject: true });
return { host, session, file1, file2, file3, something, configFile };
}
it("open files are added to inferred project even if config file is present and semantic operations succeed", () => {
const { host, session, file1, file2, file3, something } = setup();
const service = session.getProjectService();
openFilesForSession([file1], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]); // Relative import from open file is resolves but not non relative
verifyCompletions();
verifyGoToDefToB();
openFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path]);
verifyCompletions();
verifyGoToDefToB();
verifyGoToDefToC();
openFilesForSession([file3], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path]);
openFilesForSession([something], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
// Close open files and verify resolutions
closeFilesForSession([file3], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
closeFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
function verifyCompletions() {
assert.isTrue(project.languageServiceEnabled);
checkWatchedFiles(host, emptyArray);
checkWatchedDirectories(host, emptyArray, /*recursive*/ true);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
const response = session.executeCommandSeq<protocol.CompletionsRequest>({
command: protocol.CommandTypes.Completions,
arguments: protocolFileLocationFromSubstring(file1, "prop", { index: 1 })
}).response as protocol.CompletionEntry[];
assert.deepEqual(response, [
completionEntry("foo", ScriptElementKind.memberFunctionElement),
completionEntry("prop", ScriptElementKind.memberVariableElement),
]);
}
function completionEntry(name: string, kind: ScriptElementKind): protocol.CompletionEntry {
return {
name,
kind,
kindModifiers: "",
sortText: Completions.SortText.LocationPriority,
hasAction: undefined,
insertText: undefined,
isPackageJsonImport: undefined,
isRecommended: undefined,
replacementSpan: undefined,
source: undefined
};
}
function verifyGoToDefToB() {
const response = session.executeCommandSeq<protocol.DefinitionAndBoundSpanRequest>({
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: protocolFileLocationFromSubstring(file1, "y")
}).response as protocol.DefinitionInfoAndBoundSpan;
assert.deepEqual(response, {
definitions: [{
file: file2.path,
...protocolTextSpanWithContextFromSubstring({ fileText: file2.content, text: "y", contextText: "export const y = 10;" })
}],
textSpan: protocolTextSpanWithContextFromSubstring({ fileText: file1.content, text: "y" })
});
}
function verifyGoToDefToC() {
const response = session.executeCommandSeq<protocol.DefinitionAndBoundSpanRequest>({
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: protocolFileLocationFromSubstring(file1, "cc")
}).response as protocol.DefinitionInfoAndBoundSpan;
assert.deepEqual(response, {
definitions: [{
file: file3.path,
...protocolTextSpanWithContextFromSubstring({ fileText: file3.content, text: "cc", contextText: "export const cc = 10;" })
}],
textSpan: protocolTextSpanWithContextFromSubstring({ fileText: file1.content, text: "cc" })
});
}
});
it("throws on unsupported commands", () => {
const { session, file1 } = setup();
const service = session.getProjectService();
openFilesForSession([file1], session);
let hasException = false;
const request: protocol.SemanticDiagnosticsSyncRequest = {
type: "request",
seq: 1,
command: protocol.CommandTypes.SemanticDiagnosticsSync,
arguments: { file: file1.path }
};
try {
session.executeCommand(request);
}
catch (e) {
assert.equal(e.message, `Request: semanticDiagnosticsSync not allowed on approximate semantic only server`);
hasException = true;
}
assert.isTrue(hasException);
hasException = false;
const project = service.inferredProjects[0];
try {
project.getLanguageService().getSemanticDiagnostics(file1.path);
}
catch (e) {
assert.equal(e.message, `LanguageService Operation: getSemanticDiagnostics not allowed on approximate semantic only server`);
hasException = true;
}
assert.isTrue(hasException);
});
it("should not include auto type reference directives", () => {
const { host, session, file1, file2 } = setup();
const atTypes: File = {
path: `/node_modules/@types/somemodule/index.d.ts`,
content: "export const something = 10;"
};
host.ensureFileOrFolder(atTypes);
const service = session.getProjectService();
openFilesForSession([file1], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]); // Should not contain atTypes
});
it("should not include referenced files from unopened files", () => {
const file1: File = {
path: `${tscWatch.projectRoot}/a.ts`,
content: `///<reference path="b.ts"/>
///<reference path="${tscWatch.projectRoot}/node_modules/something/index.d.ts"/>
function fooA() { }`
};
const file2: File = {
path: `${tscWatch.projectRoot}/b.ts`,
content: `///<reference path="./c.ts"/>
///<reference path="${tscWatch.projectRoot}/node_modules/something/index.d.ts"/>
function fooB() { }`
};
const file3: File = {
path: `${tscWatch.projectRoot}/c.ts`,
content: `function fooC() { }`
};
const something: File = {
path: `${tscWatch.projectRoot}/node_modules/something/index.d.ts`,
content: "function something() {}"
};
const configFile: File = {
path: `${tscWatch.projectRoot}/tsconfig.json`,
content: "{}"
};
const host = createServerHost([file1, file2, file3, something, libFile, configFile]);
const session = createSession(host, { serverMode: LanguageServiceMode.ApproximateSemanticOnly, useSingleInferredProject: true });
const service = session.getProjectService();
openFilesForSession([file1], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, something.path]); // Should not contains c
openFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
assert.isTrue(project.dirty);
project.updateGraph();
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
closeFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
assert.isFalse(project.dirty);
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
});
});
}

View File

@@ -86,7 +86,7 @@ namespace ts.projectSystem {
const proj = projectService.inferredProjects[0];
assert.isDefined(proj);
assert.isTrue(proj.languageServiceEnabled);
assert.isFalse(proj.languageServiceEnabled);
});
it("project settings for inferred projects", () => {

View File

@@ -30,95 +30,74 @@ import { something } from "something";
return { host, session, file1, file2, file3, something, configFile };
}
it("open files are added to inferred project even if config file is present and semantic operations succeed", () => {
function verifySessionException<T extends server.protocol.Request>(session: TestSession, request: Partial<T>) {
let hasException = false;
try {
session.executeCommandSeq(request);
}
catch (e) {
assert.equal(e.message, `Request: ${request.command} not allowed on syntax only server`);
hasException = true;
}
assert.isTrue(hasException);
}
it("open files are added to inferred project even if config file is present and semantic operations fail", () => {
const { host, session, file1, file2, file3, something } = setup();
const service = session.getProjectService();
openFilesForSession([file1], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]); // Relative import from open file is resolves but not non relative
checkProjectActualFiles(project, emptyArray);
verifyCompletions();
verifyGoToDefToB();
openFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path]);
checkProjectActualFiles(project, emptyArray);
verifyCompletions();
verifyGoToDefToB();
verifyGoToDefToC();
openFilesForSession([file3], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path]);
checkProjectActualFiles(project, emptyArray);
openFilesForSession([something], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
checkProjectActualFiles(project, emptyArray);
// Close open files and verify resolutions
closeFilesForSession([file3], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
checkProjectActualFiles(project, emptyArray);
closeFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
checkProjectActualFiles(project, emptyArray);
function verifyCompletions() {
assert.isTrue(project.languageServiceEnabled);
assert.isFalse(project.languageServiceEnabled);
checkWatchedFiles(host, emptyArray);
checkWatchedDirectories(host, emptyArray, /*recursive*/ true);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
const response = session.executeCommandSeq<protocol.CompletionsRequest>({
verifySessionException<protocol.CompletionsRequest>(session, {
command: protocol.CommandTypes.Completions,
arguments: protocolFileLocationFromSubstring(file1, "prop", { index: 1 })
}).response as protocol.CompletionEntry[];
assert.deepEqual(response, [
completionEntry("foo", ScriptElementKind.memberFunctionElement),
completionEntry("prop", ScriptElementKind.memberVariableElement),
]);
}
function completionEntry(name: string, kind: ScriptElementKind): protocol.CompletionEntry {
return {
name,
kind,
kindModifiers: "",
sortText: Completions.SortText.LocationPriority,
hasAction: undefined,
insertText: undefined,
isPackageJsonImport: undefined,
isRecommended: undefined,
replacementSpan: undefined,
source: undefined
};
});
}
function verifyGoToDefToB() {
const response = session.executeCommandSeq<protocol.DefinitionAndBoundSpanRequest>({
verifySessionException<protocol.DefinitionAndBoundSpanRequest>(session, {
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: protocolFileLocationFromSubstring(file1, "y")
}).response as protocol.DefinitionInfoAndBoundSpan;
assert.deepEqual(response, {
definitions: [{
file: file2.path,
...protocolTextSpanWithContextFromSubstring({ fileText: file2.content, text: "y", contextText: "export const y = 10;" })
}],
textSpan: protocolTextSpanWithContextFromSubstring({ fileText: file1.content, text: "y" })
});
}
function verifyGoToDefToC() {
const response = session.executeCommandSeq<protocol.DefinitionAndBoundSpanRequest>({
verifySessionException<protocol.DefinitionAndBoundSpanRequest>(session, {
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: protocolFileLocationFromSubstring(file1, "cc")
}).response as protocol.DefinitionInfoAndBoundSpan;
assert.deepEqual(response, {
definitions: [{
file: file3.path,
...protocolTextSpanWithContextFromSubstring({ fileText: file3.content, text: "cc", contextText: "export const cc = 10;" })
}],
textSpan: protocolTextSpanWithContextFromSubstring({ fileText: file1.content, text: "cc" })
});
}
});
@@ -127,36 +106,27 @@ import { something } from "something";
const { session, file1 } = setup();
const service = session.getProjectService();
openFilesForSession([file1], session);
let hasException = false;
const request: protocol.SemanticDiagnosticsSyncRequest = {
verifySessionException<protocol.SemanticDiagnosticsSyncRequest>(session, {
type: "request",
seq: 1,
command: protocol.CommandTypes.SemanticDiagnosticsSync,
arguments: { file: file1.path }
};
try {
session.executeCommand(request);
}
catch (e) {
assert.equal(e.message, `Request: semanticDiagnosticsSync not allowed on syntaxServer`);
hasException = true;
}
assert.isTrue(hasException);
});
hasException = false;
let hasException = false;
const project = service.inferredProjects[0];
try {
project.getLanguageService().getSemanticDiagnostics(file1.path);
}
catch (e) {
assert.equal(e.message, `LanguageService Operation: getSemanticDiagnostics not allowed on syntaxServer`);
assert.equal(e.message, `LanguageService Operation: getSemanticDiagnostics not allowed on syntax only server`);
hasException = true;
}
assert.isTrue(hasException);
});
it("should not include auto type reference directives", () => {
const { host, session, file1, file2 } = setup();
const { host, session, file1 } = setup();
const atTypes: File = {
path: `/node_modules/@types/somemodule/index.d.ts`,
content: "export const something = 10;"
@@ -166,7 +136,7 @@ import { something } from "something";
openFilesForSession([file1], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]); // Should not contain atTypes
checkProjectActualFiles(project, emptyArray); // Should not contain atTypes
});
it("should not include referenced files from unopened files", () => {
@@ -200,18 +170,18 @@ function fooB() { }`
openFilesForSession([file1], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, something.path]); // Should not contains c
checkProjectActualFiles(project, emptyArray);
openFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
assert.isTrue(project.dirty);
assert.isFalse(project.dirty);
project.updateGraph();
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
checkProjectActualFiles(project, emptyArray);
closeFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
assert.isFalse(project.dirty);
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
assert.isTrue(project.dirty);
checkProjectActualFiles(project, emptyArray);
});
});
}