From e08c2d6043fdf4c3da0825462f8eb3d5283815de Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 24 Jul 2020 15:23:00 -0700 Subject: [PATCH 1/2] Add separate flag serverMode for server mode to allow back compatibility --- src/server/editorServices.ts | 33 ++- src/server/project.ts | 40 +++- src/server/session.ts | 62 ++++- src/services/services.ts | 68 +++++- src/services/types.ts | 6 + src/testRunner/tsconfig.json | 1 + .../tsserver/approximateSemanticOnlyServer.ts | 217 ++++++++++++++++++ .../unittests/tsserver/inferredProjects.ts | 2 +- .../semanticOperationsOnSyntaxServer.ts | 100 +++----- src/tsserver/server.ts | 23 ++ .../reference/api/tsserverlibrary.d.ts | 15 +- tests/baselines/reference/api/typescript.d.ts | 7 +- 12 files changed, 470 insertions(+), 104 deletions(-) create mode 100644 src/testRunner/unittests/tsserver/approximateSemanticOnlyServer.ts diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index fc79dc7c49159..7043f1c8c4f14 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -396,7 +396,9 @@ namespace ts.server { pluginProbeLocations?: readonly string[]; allowLocalPluginLoads?: boolean; typesMapLocation?: string; + /** @deprecated use serverMode instead */ syntaxOnly?: boolean; + serverMode?: LanguageServiceMode; } interface OriginalFileInfo { fileName: NormalizedPath; path: Path; } @@ -683,7 +685,9 @@ namespace ts.server { public readonly typesMapLocation: string | undefined; - public readonly syntaxOnly?: boolean; + /** @deprecated use serverMode instead */ + public readonly syntaxOnly: boolean; + public readonly serverMode: LanguageServiceMode; /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects = new Map(); @@ -713,7 +717,18 @@ namespace ts.server { this.pluginProbeLocations = opts.pluginProbeLocations || emptyArray; this.allowLocalPluginLoads = !!opts.allowLocalPluginLoads; this.typesMapLocation = (opts.typesMapLocation === undefined) ? combinePaths(getDirectoryPath(this.getExecutingFilePath()), "typesMap.json") : opts.typesMapLocation; - this.syntaxOnly = opts.syntaxOnly; + if (opts.serverMode !== undefined) { + this.serverMode = opts.serverMode; + this.syntaxOnly = this.serverMode === LanguageServiceMode.SyntaxOnly; + } + else if (opts.syntaxOnly) { + this.serverMode = LanguageServiceMode.SyntaxOnly; + this.syntaxOnly = true; + } + else { + this.serverMode = LanguageServiceMode.Semantic; + this.syntaxOnly = false; + } Debug.assert(!!this.host.createHash, "'ServerHost.createHash' is required for ProjectService"); if (this.host.realpath) { @@ -749,7 +764,7 @@ namespace ts.server { this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; this.packageJsonCache = createPackageJsonCache(this); - this.watchFactory = this.syntaxOnly ? + this.watchFactory = this.serverMode !== LanguageServiceMode.Semantic ? { watchFile: returnNoopFileWatcher, watchFilePath: returnNoopFileWatcher, @@ -1727,7 +1742,7 @@ namespace ts.server { * the newly opened file. */ private forEachConfigFileLocation(info: OpenScriptInfoOrClosedOrConfigFileInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) { - if (this.syntaxOnly) { + if (this.serverMode !== LanguageServiceMode.Semantic) { return undefined; } @@ -3014,7 +3029,7 @@ namespace ts.server { let retainProjects: ConfiguredProject[] | ConfiguredProject | undefined; let projectForConfigFileDiag: ConfiguredProject | undefined; let defaultConfigProjectIsCreated = false; - if (this.syntaxOnly) { + if (this.serverMode === LanguageServiceMode.ApproximateSemanticOnly) { // Invalidate resolutions in the file since this file is now open info.containingProjects.forEach(project => { if (project.resolutionCache.removeRelativeNoResolveResolutionsOfFile(info.path)) { @@ -3022,7 +3037,7 @@ namespace ts.server { } }); } - else if (!project) { // Checking syntaxOnly is an optimization + else if (!project && this.serverMode === LanguageServiceMode.Semantic) { // Checking semantic mode is an optimization configFileName = this.getConfigFileNameForFile(info); if (configFileName) { project = this.findConfiguredProjectByProjectName(configFileName); @@ -3109,7 +3124,7 @@ namespace ts.server { Debug.assert(this.openFiles.has(info.path)); this.assignOrphanScriptInfoToInferredProject(info, this.openFiles.get(info.path)); } - else if (this.syntaxOnly && info.cacheSourceFile?.sourceFile.referencedFiles.length) { + else if (this.serverMode === LanguageServiceMode.ApproximateSemanticOnly && info.cacheSourceFile?.sourceFile.referencedFiles.length) { // This file was just opened and references in this file will previously not been resolved so schedule update info.containingProjects.forEach(project => project.markAsDirty()); } @@ -3325,7 +3340,7 @@ namespace ts.server { } private telemetryOnOpenFile(scriptInfo: ScriptInfo): void { - if (this.syntaxOnly || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) { + if (this.serverMode !== LanguageServiceMode.Semantic || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) { return; } @@ -3637,7 +3652,7 @@ namespace ts.server { for (const file of proj.rootFiles) { const normalized = toNormalizedPath(file.fileName); if (getBaseConfigFileName(normalized)) { - if (!this.syntaxOnly && this.host.fileExists(normalized)) { + if (this.serverMode === LanguageServiceMode.Semantic && this.host.fileExists(normalized)) { (tsConfigFiles || (tsConfigFiles = [])).push(normalized); } } diff --git a/src/server/project.ts b/src/server/project.ts index 16f3178d7f786..e795dd0b0eb25 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -279,9 +279,21 @@ namespace ts.server { this.compilerOptions.allowNonTsExtensions = true; } - this.languageServiceEnabled = true; - if (projectService.syntaxOnly) { - this.compilerOptions.types = []; + switch (projectService.serverMode) { + case LanguageServiceMode.Semantic: + this.languageServiceEnabled = true; + break; + case LanguageServiceMode.ApproximateSemanticOnly: + this.languageServiceEnabled = true; + this.compilerOptions.types = []; + break; + case LanguageServiceMode.SyntaxOnly: + this.languageServiceEnabled = false; + this.compilerOptions.noResolve = true; + this.compilerOptions.types = []; + break; + default: + Debug.assertNever(projectService.serverMode); } this.setInternalCompilerOptionsForEmittingJsFiles(); @@ -298,10 +310,10 @@ namespace ts.server { this.resolutionCache = createResolutionCache( this, currentDirectory && this.currentDirectory, - projectService.syntaxOnly ? ResolutionKind.RelativeReferencesInOpenFileOnly : ResolutionKind.All, + projectService.serverMode === LanguageServiceMode.Semantic ? ResolutionKind.All : ResolutionKind.RelativeReferencesInOpenFileOnly, /*logChangesWhenResolvingModule*/ true ); - this.languageService = createLanguageService(this, this.documentRegistry, this.projectService.syntaxOnly); + this.languageService = createLanguageService(this, this.documentRegistry, this.projectService.serverMode); if (lastFileExceededProgramSize) { this.disableLanguageService(lastFileExceededProgramSize); } @@ -456,7 +468,16 @@ namespace ts.server { /*@internal*/ includeTripleslashReferencesFrom(containingFile: string) { - return !this.projectService.syntaxOnly || this.fileIsOpen(this.toPath(containingFile)); + switch (this.projectService.serverMode) { + case LanguageServiceMode.Semantic: + return true; + case LanguageServiceMode.ApproximateSemanticOnly: + return this.fileIsOpen(this.toPath(containingFile)); + case LanguageServiceMode.SyntaxOnly: + return false; + default: + Debug.assertNever(this.projectService.serverMode); + } } directoryExists(path: string): boolean { @@ -656,7 +677,7 @@ namespace ts.server { } enableLanguageService() { - if (this.languageServiceEnabled) { + if (this.languageServiceEnabled || this.projectService.serverMode === LanguageServiceMode.SyntaxOnly) { return; } this.languageServiceEnabled = true; @@ -668,6 +689,7 @@ namespace ts.server { if (!this.languageServiceEnabled) { return; } + Debug.assert(this.projectService.serverMode !== LanguageServiceMode.SyntaxOnly); this.languageService.cleanupSemanticCache(); this.languageServiceEnabled = false; this.lastFileExceededProgramSize = lastFileExceededProgramSize; @@ -997,7 +1019,7 @@ namespace ts.server { // update builder only if language service is enabled // otherwise tell it to drop its internal state - if (this.languageServiceEnabled && !this.projectService.syntaxOnly) { + if (this.languageServiceEnabled && this.projectService.serverMode === LanguageServiceMode.Semantic) { // 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) @@ -1128,7 +1150,7 @@ namespace ts.server { } // Watch the type locations that would be added to program as part of automatic type resolutions - if (this.languageServiceEnabled && !this.projectService.syntaxOnly) { + if (this.languageServiceEnabled && this.projectService.serverMode === LanguageServiceMode.Semantic) { this.resolutionCache.updateTypeRootsWatch(); } } diff --git a/src/server/session.ts b/src/server/session.ts index 57ba10fd59bda..2e1f88deb5637 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -585,7 +585,7 @@ namespace ts.server { undefined; } - const invalidSyntaxOnlyCommands: readonly CommandNames[] = [ + const invalidApproximateSemanticOnlyCommands: readonly CommandNames[] = [ CommandNames.OpenExternalProject, CommandNames.OpenExternalProjects, CommandNames.CloseExternalProject, @@ -621,6 +621,36 @@ namespace ts.server { CommandNames.ProvideCallHierarchyOutgoingCalls, ]; + const invalidSyntxOnlyCommands: readonly CommandNames[] = [ + ...invalidApproximateSemanticOnlyCommands, + CommandNames.Definition, + CommandNames.DefinitionFull, + CommandNames.DefinitionAndBoundSpan, + CommandNames.DefinitionAndBoundSpanFull, + CommandNames.TypeDefinition, + CommandNames.Implementation, + CommandNames.ImplementationFull, + CommandNames.References, + CommandNames.ReferencesFull, + CommandNames.Rename, + CommandNames.RenameLocationsFull, + CommandNames.RenameInfoFull, + CommandNames.Quickinfo, + CommandNames.QuickinfoFull, + CommandNames.CompletionInfo, + CommandNames.Completions, + CommandNames.CompletionsFull, + CommandNames.CompletionDetails, + CommandNames.CompletionDetailsFull, + CommandNames.SignatureHelp, + CommandNames.SignatureHelpFull, + CommandNames.Navto, + CommandNames.NavtoFull, + CommandNames.Occurrences, + CommandNames.DocumentHighlights, + CommandNames.DocumentHighlightsFull, + ]; + export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -637,7 +667,9 @@ namespace ts.server { eventHandler?: ProjectServiceEventHandler; /** Has no effect if eventHandler is also specified. */ suppressDiagnosticEvents?: boolean; + /** @deprecated use serverMode instead */ syntaxOnly?: boolean; + serverMode?: LanguageServiceMode; throttleWaitMilliseconds?: number; noGetErrOnBackgroundUpdate?: boolean; @@ -709,18 +741,32 @@ namespace ts.server { allowLocalPluginLoads: opts.allowLocalPluginLoads, typesMapLocation: opts.typesMapLocation, syntaxOnly: opts.syntaxOnly, + serverMode: opts.serverMode, }; 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`); - }) - ); + // Make sure to setup handlers to throw error for not allowed commands on syntax server + switch (this.projectService.serverMode) { + case LanguageServiceMode.Semantic: + break; + case LanguageServiceMode.ApproximateSemanticOnly: + invalidApproximateSemanticOnlyCommands.forEach(commandName => + this.handlers.set(commandName, request => { + throw new Error(`Request: ${request.command} not allowed on approximate semantic only server`); + }) + ); + break; + case LanguageServiceMode.SyntaxOnly: + invalidSyntxOnlyCommands.forEach(commandName => + this.handlers.set(commandName, request => { + throw new Error(`Request: ${request.command} not allowed on syntax only server`); + }) + ); + break; + default: + Debug.assertNever(this.projectService.serverMode); } } diff --git a/src/services/services.ts b/src/services/services.ts index a2cf8f381c200..b309cbd1461c7 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1171,7 +1171,7 @@ namespace ts { } } - const invalidOperationsOnSyntaxOnly: readonly (keyof LanguageService)[] = [ + const invalidOperationsOnApproximateSemanticOnly: readonly (keyof LanguageService)[] = [ "getSyntacticDiagnostics", "getSemanticDiagnostics", "getSuggestionDiagnostics", @@ -1191,10 +1191,42 @@ namespace ts { "provideCallHierarchyOutgoingCalls", ]; + const invalidOperationsOnSyntaxOnly: readonly (keyof LanguageService)[] = [ + ...invalidOperationsOnApproximateSemanticOnly, + "getCompletionsAtPosition", + "getCompletionEntryDetails", + "getCompletionEntrySymbol", + "getSignatureHelpItems", + "getQuickInfoAtPosition", + "getDefinitionAtPosition", + "getDefinitionAndBoundSpan", + "getImplementationAtPosition", + "getTypeDefinitionAtPosition", + "getReferencesAtPosition", + "findReferences", + "getOccurrencesAtPosition", + "getDocumentHighlights", + "getNavigateToItems", + "getRenameInfo", + "findRenameLocations", + "getApplicableRefactors", + ]; export function createLanguageService( host: LanguageServiceHost, documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()), - syntaxOnly = false): LanguageService { + syntaxOnlyOrLanguageServiceMode?: boolean | LanguageServiceMode, + ): LanguageService { + let languageServiceMode: LanguageServiceMode; + if (syntaxOnlyOrLanguageServiceMode === undefined) { + languageServiceMode = LanguageServiceMode.Semantic; + } + else if (typeof syntaxOnlyOrLanguageServiceMode === "boolean") { + // languageServiceMode = SyntaxOnly + languageServiceMode = syntaxOnlyOrLanguageServiceMode ? LanguageServiceMode.SyntaxOnly : LanguageServiceMode.Semantic; + } + else { + languageServiceMode = syntaxOnlyOrLanguageServiceMode; + } const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); let program: Program; @@ -1244,6 +1276,7 @@ namespace ts { } function synchronizeHostData(): void { + Debug.assert(languageServiceMode !== LanguageServiceMode.SyntaxOnly); // perform fast check if host supports it if (host.getProjectVersion) { const hostProjectVersion = host.getProjectVersion(); @@ -1429,6 +1462,11 @@ namespace ts { // TODO: GH#18217 frequently asserted as defined function getProgram(): Program | undefined { + if (languageServiceMode === LanguageServiceMode.SyntaxOnly) { + Debug.assert(program === undefined); + return undefined; + } + synchronizeHostData(); return program; @@ -2504,14 +2542,26 @@ namespace ts { uncommentSelection, }; - if (syntaxOnly) { - invalidOperationsOnSyntaxOnly.forEach(key => - ls[key] = () => { - throw new Error(`LanguageService Operation: ${key} not allowed on syntaxServer`); - } - ); + switch (languageServiceMode) { + case LanguageServiceMode.Semantic: + break; + case LanguageServiceMode.ApproximateSemanticOnly: + invalidOperationsOnApproximateSemanticOnly.forEach(key => + ls[key] = () => { + throw new Error(`LanguageService Operation: ${key} not allowed on approximate semantic only server`); + } + ); + break; + case LanguageServiceMode.SyntaxOnly: + invalidOperationsOnSyntaxOnly.forEach(key => + ls[key] = () => { + throw new Error(`LanguageService Operation: ${key} not allowed on syntax only server`); + } + ); + break; + default: + Debug.assertNever(languageServiceMode); } - return ls; } diff --git a/src/services/types.ts b/src/services/types.ts index 0ebd80372cbea..3306d864c5d73 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -221,6 +221,12 @@ namespace ts { durationMs: number; } + export enum LanguageServiceMode { + Semantic, + ApproximateSemanticOnly, + SyntaxOnly, + } + // // Public interface of the host of a language service instance. // diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 8b0123e1ef9be..f2b3bfdc4dcbe 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -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", diff --git a/src/testRunner/unittests/tsserver/approximateSemanticOnlyServer.ts b/src/testRunner/unittests/tsserver/approximateSemanticOnlyServer.ts new file mode 100644 index 0000000000000..1b517e027b882 --- /dev/null +++ b/src/testRunner/unittests/tsserver/approximateSemanticOnlyServer.ts @@ -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({ + 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({ + 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({ + 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: `/// +/// +function fooA() { }` + }; + const file2: File = { + path: `${tscWatch.projectRoot}/b.ts`, + content: `/// +/// +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]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/inferredProjects.ts b/src/testRunner/unittests/tsserver/inferredProjects.ts index 8676e2a62794f..216273976e288 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.isTrue(proj.languageServiceEnabled); + assert.isFalse(proj.languageServiceEnabled); }); it("project settings for inferred projects", () => { diff --git a/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts b/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts index e91acf222b659..d252410f0cc32 100644 --- a/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts +++ b/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts @@ -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(session: TestSession, request: Partial) { + 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({ + verifySessionException(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({ + verifySessionException(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({ + verifySessionException(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(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); }); }); } diff --git a/src/tsserver/server.ts b/src/tsserver/server.ts index da0d9be9ddb60..e52c89e41f29f 100644 --- a/src/tsserver/server.ts +++ b/src/tsserver/server.ts @@ -516,6 +516,7 @@ namespace ts.server { canUseEvents: true, suppressDiagnosticEvents, syntaxOnly, + serverMode, noGetErrOnBackgroundUpdate, globalPlugins, pluginProbeLocations, @@ -948,6 +949,26 @@ namespace ts.server { return arg.split(",").filter(name => name !== ""); } + let unknownServerMode: string | undefined; + function parseServerMode(): LanguageServiceMode | undefined { + const mode = findArgument("--serverMode"); + if (mode === undefined) { + return undefined; + } + + switch (mode.toLowerCase()) { + case "semantic": + return LanguageServiceMode.Semantic; + case "approximatesemanticonly": + return LanguageServiceMode.ApproximateSemanticOnly; + case "syntaxonly": + return LanguageServiceMode.SyntaxOnly; + default: + unknownServerMode = mode; + return undefined; + } + } + const globalPlugins = parseStringArray("--globalPlugins"); const pluginProbeLocations = parseStringArray("--pluginProbeLocations"); const allowLocalPluginLoads = hasArgument("--allowLocalPluginLoads"); @@ -957,6 +978,7 @@ namespace ts.server { const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition"); const suppressDiagnosticEvents = hasArgument("--suppressDiagnosticEvents"); const syntaxOnly = hasArgument("--syntaxOnly"); + const serverMode = parseServerMode(); const telemetryEnabled = hasArgument(Arguments.EnableTelemetry); const noGetErrOnBackgroundUpdate = hasArgument("--noGetErrOnBackgroundUpdate"); @@ -964,6 +986,7 @@ namespace ts.server { logger.info(`Version: ${version}`); logger.info(`Arguments: ${process.argv.join(" ")}`); logger.info(`Platform: ${os.platform()} NodeVersion: ${nodeVersion} CaseSensitive: ${sys.useCaseSensitiveFileNames}`); + logger.info(`ServerMode: ${serverMode} syntaxOnly: ${syntaxOnly} hasUnknownServerMode: ${unknownServerMode}`); const ioSession = new IOSession(); process.on("uncaughtException", err => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 16f62ef389033..0246f6e09948c 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5313,6 +5313,11 @@ declare namespace ts { kind: "UpdateGraph" | "CreatePackageJsonAutoImportProvider"; durationMs: number; } + enum LanguageServiceMode { + Semantic = 0, + ApproximateSemanticOnly = 1, + SyntaxOnly = 2 + } interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; @@ -6324,7 +6329,7 @@ declare namespace ts { function getSupportedCodeFixes(): string[]; function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile; function updateLanguageServiceSourceFile(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string, textChangeRange: TextChangeRange | undefined, aggressiveChecks?: boolean): SourceFile; - function createLanguageService(host: LanguageServiceHost, documentRegistry?: DocumentRegistry, syntaxOnly?: boolean): LanguageService; + function createLanguageService(host: LanguageServiceHost, documentRegistry?: DocumentRegistry, syntaxOnlyOrLanguageServiceMode?: boolean | LanguageServiceMode): LanguageService; /** * Get the path of the default library files (lib.d.ts) as distributed with the typescript * node package. @@ -9482,7 +9487,9 @@ declare namespace ts.server { pluginProbeLocations?: readonly string[]; allowLocalPluginLoads?: boolean; typesMapLocation?: string; + /** @deprecated use serverMode instead */ syntaxOnly?: boolean; + serverMode?: LanguageServiceMode; } export class ProjectService { private readonly scriptInfoInNodeModulesWatchers; @@ -9554,7 +9561,9 @@ declare namespace ts.server { readonly allowLocalPluginLoads: boolean; private currentPluginConfigOverrides; readonly typesMapLocation: string | undefined; - readonly syntaxOnly?: boolean; + /** @deprecated use serverMode instead */ + readonly syntaxOnly: boolean; + readonly serverMode: LanguageServiceMode; /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects; private performanceEventHandler?; @@ -9772,7 +9781,9 @@ declare namespace ts.server { eventHandler?: ProjectServiceEventHandler; /** Has no effect if eventHandler is also specified. */ suppressDiagnosticEvents?: boolean; + /** @deprecated use serverMode instead */ syntaxOnly?: boolean; + serverMode?: LanguageServiceMode; throttleWaitMilliseconds?: number; noGetErrOnBackgroundUpdate?: boolean; globalPlugins?: readonly string[]; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 76296ad0927b8..d40a9823018ee 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5313,6 +5313,11 @@ declare namespace ts { kind: "UpdateGraph" | "CreatePackageJsonAutoImportProvider"; durationMs: number; } + enum LanguageServiceMode { + Semantic = 0, + ApproximateSemanticOnly = 1, + SyntaxOnly = 2 + } interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; @@ -6324,7 +6329,7 @@ declare namespace ts { function getSupportedCodeFixes(): string[]; function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile; function updateLanguageServiceSourceFile(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string, textChangeRange: TextChangeRange | undefined, aggressiveChecks?: boolean): SourceFile; - function createLanguageService(host: LanguageServiceHost, documentRegistry?: DocumentRegistry, syntaxOnly?: boolean): LanguageService; + function createLanguageService(host: LanguageServiceHost, documentRegistry?: DocumentRegistry, syntaxOnlyOrLanguageServiceMode?: boolean | LanguageServiceMode): LanguageService; /** * Get the path of the default library files (lib.d.ts) as distributed with the typescript * node package. From dca0b019b3627f375c9a64fe0ccd12dfe9f6296c Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Mon, 3 Aug 2020 00:36:57 +0000 Subject: [PATCH 2/2] Addressed code review feedback. --- src/server/session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 2e1f88deb5637..dbce514dde507 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -621,7 +621,7 @@ namespace ts.server { CommandNames.ProvideCallHierarchyOutgoingCalls, ]; - const invalidSyntxOnlyCommands: readonly CommandNames[] = [ + const invalidSyntaxOnlyCommands: readonly CommandNames[] = [ ...invalidApproximateSemanticOnlyCommands, CommandNames.Definition, CommandNames.DefinitionFull, @@ -759,7 +759,7 @@ namespace ts.server { ); break; case LanguageServiceMode.SyntaxOnly: - invalidSyntxOnlyCommands.forEach(commandName => + invalidSyntaxOnlyCommands.forEach(commandName => this.handlers.set(commandName, request => { throw new Error(`Request: ${request.command} not allowed on syntax only server`); })