Syntax only server creates inferred project with all the open files w… (#38561)

* Syntax only server creates inferred project with all the open files with noResolve and can handle semantic operations

* No Watching

* Disable tests

* Add and fix tests

* Only support selected commands

* Revert "Disable tests"

This reverts commit 90d8a966eb.

* Dont log request details for unsupported commands
This commit is contained in:
Sheetal Nandi
2020-06-16 13:39:20 -07:00
committed by GitHub
parent e97003f263
commit 25f6232d45
8 changed files with 196 additions and 20 deletions

View File

@@ -234,13 +234,14 @@ namespace ts {
}
export const noopFileWatcher: FileWatcher = { close: noop };
export const returnNoopFileWatcher = () => noopFileWatcher;
export function createWatchHost(system = sys, reportWatchStatus?: WatchStatusReporter): WatchHost {
const onWatchStatusChange = reportWatchStatus || createWatchStatusReporter(system);
return {
onWatchStatusChange,
watchFile: maybeBind(system, system.watchFile) || (() => noopFileWatcher),
watchDirectory: maybeBind(system, system.watchDirectory) || (() => noopFileWatcher),
watchFile: maybeBind(system, system.watchFile) || returnNoopFileWatcher,
watchDirectory: maybeBind(system, system.watchDirectory) || returnNoopFileWatcher,
setTimeout: maybeBind(system, system.setTimeout) || noop,
clearTimeout: maybeBind(system, system.clearTimeout) || noop
};

View File

@@ -725,7 +725,13 @@ namespace ts.server {
const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose :
this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None;
const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop;
this.watchFactory = getWatchFactory(watchLogLevel, log, getDetailWatchInfo);
this.watchFactory = this.syntaxOnly ?
{
watchFile: returnNoopFileWatcher,
watchFilePath: returnNoopFileWatcher,
watchDirectory: returnNoopFileWatcher,
} :
getWatchFactory(watchLogLevel, log, getDetailWatchInfo);
}
toPath(fileName: string) {

View File

@@ -279,7 +279,10 @@ namespace ts.server {
this.compilerOptions.allowNonTsExtensions = true;
}
this.languageServiceEnabled = !projectService.syntaxOnly;
this.languageServiceEnabled = true;
if (projectService.syntaxOnly) {
this.compilerOptions.noResolve = true;
}
this.setInternalCompilerOptionsForEmittingJsFiles();
const host = this.projectService.host;
@@ -293,7 +296,7 @@ namespace ts.server {
// Use the current directory as resolution root only if the project created using current directory string
this.resolutionCache = createResolutionCache(this, currentDirectory && this.currentDirectory, /*logChangesWhenResolvingModule*/ true);
this.languageService = createLanguageService(this, this.documentRegistry, projectService.syntaxOnly);
this.languageService = createLanguageService(this, this.documentRegistry, this.projectService.syntaxOnly);
if (lastFileExceededProgramSize) {
this.disableLanguageService(lastFileExceededProgramSize);
}
@@ -642,7 +645,7 @@ namespace ts.server {
}
enableLanguageService() {
if (this.languageServiceEnabled || this.projectService.syntaxOnly) {
if (this.languageServiceEnabled) {
return;
}
this.languageServiceEnabled = true;
@@ -654,7 +657,6 @@ namespace ts.server {
if (!this.languageServiceEnabled) {
return;
}
Debug.assert(!this.projectService.syntaxOnly);
this.languageService.cleanupSemanticCache();
this.languageServiceEnabled = false;
this.lastFileExceededProgramSize = lastFileExceededProgramSize;
@@ -970,7 +972,7 @@ namespace ts.server {
// update builder only if language service is enabled
// otherwise tell it to drop its internal state
if (this.languageServiceEnabled) {
if (this.languageServiceEnabled && !this.projectService.syntaxOnly) {
// 1. no changes in structure, no changes in unresolved imports - do nothing
// 2. no changes in structure, unresolved imports were changed - collect unresolved imports for all files
// (can reuse cached imports for files that were not changed)
@@ -1092,7 +1094,7 @@ namespace ts.server {
}
// Watch the type locations that would be added to program as part of automatic type resolutions
if (this.languageServiceEnabled) {
if (this.languageServiceEnabled && !this.projectService.syntaxOnly) {
this.resolutionCache.updateTypeRootsWatch();
}
}

View File

@@ -575,6 +575,42 @@ namespace ts.server {
undefined;
}
const invalidSyntaxOnlyCommands: readonly CommandNames[] = [
CommandNames.OpenExternalProject,
CommandNames.OpenExternalProjects,
CommandNames.CloseExternalProject,
CommandNames.SynchronizeProjectList,
CommandNames.EmitOutput,
CommandNames.CompileOnSaveAffectedFileList,
CommandNames.CompileOnSaveEmitFile,
CommandNames.CompilerOptionsDiagnosticsFull,
CommandNames.EncodedSemanticClassificationsFull,
CommandNames.SemanticDiagnosticsSync,
CommandNames.SyntacticDiagnosticsSync,
CommandNames.SuggestionDiagnosticsSync,
CommandNames.Geterr,
CommandNames.GeterrForProject,
CommandNames.Reload,
CommandNames.ReloadProjects,
CommandNames.GetCodeFixes,
CommandNames.GetCodeFixesFull,
CommandNames.GetCombinedCodeFix,
CommandNames.GetCombinedCodeFixFull,
CommandNames.ApplyCodeActionCommand,
CommandNames.GetSupportedCodeFixes,
CommandNames.GetApplicableRefactors,
CommandNames.GetEditsForRefactor,
CommandNames.GetEditsForRefactorFull,
CommandNames.OrganizeImports,
CommandNames.OrganizeImportsFull,
CommandNames.GetEditsForFileRename,
CommandNames.GetEditsForFileRenameFull,
CommandNames.ConfigurePlugin,
CommandNames.PrepareCallHierarchy,
CommandNames.ProvideCallHierarchyIncomingCalls,
CommandNames.ProvideCallHierarchyOutgoingCalls,
];
export interface SessionOptions {
host: ServerHost;
cancellationToken: ServerCancellationToken;
@@ -667,6 +703,15 @@ namespace ts.server {
this.projectService = new ProjectService(settings);
this.projectService.setPerformanceEventHandler(this.performanceEventHandler.bind(this));
this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger);
// Make sure to setup handlers to throw error for not allowed commands on syntax server;
if (this.projectService.syntaxOnly) {
invalidSyntaxOnlyCommands.forEach(commandName =>
this.handlers.set(commandName, request => {
throw new Error(`Request: ${request.command} not allowed on syntaxServer`);
})
);
}
}
private sendRequestCompletedEvent(requestId: number): void {
@@ -1253,9 +1298,9 @@ namespace ts.server {
}
private getJsxClosingTag(args: protocol.JsxClosingTagRequestArgs): TextInsertion | undefined {
const { file, project } = this.getFileAndProject(args);
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
const position = this.getPositionInFile(args, file);
const tag = project.getLanguageService().getJsxClosingTagAtPosition(file, position);
const tag = languageService.getJsxClosingTagAtPosition(file, position);
return tag === undefined ? undefined : { newText: tag.newText, caretOffset: 0 };
}

View File

@@ -1171,6 +1171,26 @@ namespace ts {
}
}
const invalidOperationsOnSyntaxOnly: readonly (keyof LanguageService)[] = [
"getSyntacticDiagnostics",
"getSemanticDiagnostics",
"getSuggestionDiagnostics",
"getCompilerOptionsDiagnostics",
"getSemanticClassifications",
"getEncodedSemanticClassifications",
"getCodeFixesAtPosition",
"getCombinedCodeFix",
"applyCodeActionCommand",
"organizeImports",
"getEditsForFileRename",
"getEmitOutput",
"getApplicableRefactors",
"getEditsForRefactor",
"prepareCallHierarchy",
"provideCallHierarchyIncomingCalls",
"provideCallHierarchyOutgoingCalls",
];
export function createLanguageService(
host: LanguageServiceHost,
documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()),
@@ -1224,8 +1244,6 @@ namespace ts {
}
function synchronizeHostData(): void {
Debug.assert(!syntaxOnly);
// perform fast check if host supports it
if (host.getProjectVersion) {
const hostProjectVersion = host.getProjectVersion();
@@ -1419,11 +1437,6 @@ namespace ts {
// TODO: GH#18217 frequently asserted as defined
function getProgram(): Program | undefined {
if (syntaxOnly) {
Debug.assert(program === undefined);
return undefined;
}
synchronizeHostData();
return program;
@@ -2199,7 +2212,7 @@ namespace ts {
return declaration ? CallHierarchy.getOutgoingCalls(program, declaration) : [];
}
return {
const ls: LanguageService = {
dispose,
cleanupSemanticCache,
getSyntacticDiagnostics,
@@ -2259,6 +2272,16 @@ namespace ts {
provideCallHierarchyIncomingCalls,
provideCallHierarchyOutgoingCalls
};
if (syntaxOnly) {
invalidOperationsOnSyntaxOnly.forEach(key =>
ls[key] = () => {
throw new Error(`LanguageService Operation: ${key} not allowed on syntaxServer`);
}
);
}
return ls;
}
/* @internal */

View File

@@ -181,6 +181,7 @@
"unittests/tsserver/reload.ts",
"unittests/tsserver/rename.ts",
"unittests/tsserver/resolutionCache.ts",
"unittests/tsserver/semanticOperationsOnSyntaxServer.ts",
"unittests/tsserver/smartSelection.ts",
"unittests/tsserver/session.ts",
"unittests/tsserver/skipLibCheck.ts",

View File

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

View File

@@ -0,0 +1,98 @@
namespace ts.projectSystem {
describe("unittests:: tsserver:: Semantic operations on Syntax server", () => {
function setup() {
const file1: File = {
path: `${tscWatch.projectRoot}/a.ts`,
content: `import { y } from "./b";
class c { prop = "hello"; foo() { return this.prop; } }`
};
const file2: File = {
path: `${tscWatch.projectRoot}/b.ts`,
content: "export const y = 10;"
};
const configFile: File = {
path: `${tscWatch.projectRoot}/tsconfig.json`,
content: "{}"
};
const host = createServerHost([file1, file2, libFile, configFile]);
const session = createSession(host, { syntaxOnly: true, useSingleInferredProject: true });
return { host, session, file1, file2, configFile };
}
it("open files are added to inferred project even if config file is present and semantic operations succeed", () => {
const { host, session, file1, file2 } = setup();
const service = session.getProjectService();
openFilesForSession([file1], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, [libFile.path, file1.path]); // Import is not resolved
verifyCompletions();
openFilesForSession([file2], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]);
verifyCompletions();
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,
isRecommended: undefined,
replacementSpan: undefined,
source: undefined
};
}
});
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 syntaxServer`);
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 syntaxServer`);
hasException = true;
}
assert.isTrue(hasException);
});
});
}