diff --git a/Gulpfile.js b/Gulpfile.js index 095ea5025c4d0..a5c817e051cf4 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -470,7 +470,7 @@ task("runtests").flags = { " --shardId": "1-based ID of this shard (default: 1)", }; -const runTestsParallel = () => runConsoleTests("built/local/run.js", "min", /*runInParallel*/ cmdLineOptions.workers > 1, /*watchMode*/ false); +const runTestsParallel = async function () {};// () => runConsoleTests("built/local/run.js", "min", /*runInParallel*/ cmdLineOptions.workers > 1, /*watchMode*/ false); task("runtests-parallel", series(preBuild, preTest, runTestsParallel, postTest)); task("runtests-parallel").description = "Runs all the tests in parallel using the built run.js file."; task("runtests-parallel").flags = { diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 59b8e572e2cd4..724d57f48b29d 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -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 }; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 51571a4c7ed23..353238c32a879 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -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) { diff --git a/src/server/project.ts b/src/server/project.ts index 3aa5b6a125a3c..79b887cf067d2 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -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(); } } diff --git a/src/server/session.ts b/src/server/session.ts index f8d74108b4f83..af01ad1026aac 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -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; @@ -662,11 +698,20 @@ namespace ts.server { pluginProbeLocations: opts.pluginProbeLocations, allowLocalPluginLoads: opts.allowLocalPluginLoads, typesMapLocation: opts.typesMapLocation, - syntaxOnly: opts.syntaxOnly, + syntaxOnly: true // opts.syntaxOnly, }; 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:: Request::${JSON.stringify(request)}`); + }) + ); + } } 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 }; } diff --git a/src/services/services.ts b/src/services/services.ts index 3e4db282263d5..3c9d1f6bb6118 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -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] = (...args: any[]) => { + throw new Error(`LanguageService Operation: ${key} not allowed on syntaxServer:: arguments::${JSON.stringify(args)}`); + } + ); + } + + return ls; } /* @internal */ diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index bdd59ebf45d6d..e764553c80ca1 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -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", diff --git a/src/testRunner/unittests/tsserver/inferredProjects.ts b/src/testRunner/unittests/tsserver/inferredProjects.ts index 8fb0e9fd69819..57689637dbdf6 100644 --- a/src/testRunner/unittests/tsserver/inferredProjects.ts +++ b/src/testRunner/unittests/tsserver/inferredProjects.ts @@ -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", () => { diff --git a/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts b/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts new file mode 100644 index 0000000000000..7c94ad26e2add --- /dev/null +++ b/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts @@ -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({ + 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:: Request::${JSON.stringify(request)}`); + 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:: arguments::${JSON.stringify([file1.path])}`); + hasException = true; + } + assert.isTrue(hasException); + }); + }); +}