diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index efc1e53efb8..fe6a2844896 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2223,9 +2223,9 @@ namespace ts { if (currentFlow) { node.flowNode = currentFlow; } - checkStrictModeFunctionName(node); - const bindingName = (node).name ? (node).name.text : "__function"; - return bindAnonymousDeclaration(node, SymbolFlags.Function, bindingName); + checkStrictModeFunctionName(node); + const bindingName = node.name ? node.name.text : "__function"; + return bindAnonymousDeclaration(node, SymbolFlags.Function, bindingName); } function bindPropertyOrMethodOrAccessor(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 12e36f7534a..6ab346655c4 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -972,7 +972,7 @@ namespace ts { basePath: string, errors: Diagnostic[], configFileName?: string): CompilerOptions { const options: CompilerOptions = getBaseFileName(configFileName) === "jsconfig.json" - ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true } + ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true } : {}; convertOptionsFromJson(optionDeclarations, jsonOptions, basePath, options, Diagnostics.Unknown_compiler_option_0, errors); return options; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 58c0e6ee3f2..1b8c995ea05 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3073,7 +3073,6 @@ "category": "Error", "code": 17010 }, - "Circularity detected while resolving configuration: {0}": { "category": "Error", "code": 18000 @@ -3081,5 +3080,33 @@ "The path in an 'extends' options must be relative or rooted.": { "category": "Error", "code": 18001 + }, + "Add missing 'super()' call.": { + "category": "Message", + "code": 90001 + }, + "Make 'super()' call the first statement in the constructor.": { + "category": "Message", + "code": 90002 + }, + "Change 'extends' to 'implements'": { + "category": "Message", + "code": 90003 + }, + "Remove unused identifiers": { + "category": "Message", + "code": 90004 + }, + "Implement interface on reference": { + "category": "Message", + "code": 90005 + }, + "Implement interface on class": { + "category": "Message", + "code": 90006 + }, + "Implement inherited abstract class": { + "category": "Message", + "code": 90007 } } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 55b58786e5d..69daa867cfc 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5352,7 +5352,7 @@ namespace ts { parseExpected(SyntaxKind.EqualsToken); node.type = parseType(); parseSemicolon(); - return finishNode(node); + return addJSDocComment(finishNode(node)); } // In an ambient declaration, the grammar only allows integer literals as initializers. diff --git a/src/compiler/program.ts b/src/compiler/program.ts index ff58f2ca4bb..14a602684e6 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -358,7 +358,7 @@ namespace ts { // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders const typeReferences: string[] = getAutomaticTypeDirectiveNames(options, host); - if (typeReferences) { + if (typeReferences.length) { // This containingFilename needs to match with the one used in managed-side const containingFilename = combinePaths(host.getCurrentDirectory(), "__inferred type names__.ts"); const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeReferences, containingFilename); @@ -1307,7 +1307,6 @@ namespace ts { for (let i = 0; i < moduleNames.length; i++) { const resolution = resolutions[i]; setResolvedModule(file, moduleNames[i], resolution); - const resolvedPath = resolution ? toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName) : undefined; // add file to program only if: // - resolution was successful @@ -1329,7 +1328,7 @@ namespace ts { } else if (shouldAddFile) { findSourceFile(resolution.resolvedFileName, - resolvedPath, + toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName), /*isDefaultLib*/ false, /*isReference*/ false, file, skipTrivia(file.text, file.imports[i].pos), diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index eb957ae49d0..9645c068595 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1,4 +1,4 @@ -// +// // Copyright (c) Microsoft Corporation. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -427,7 +427,7 @@ namespace FourSlash { if (exists !== negative) { this.printErrorLog(negative, this.getAllDiagnostics()); - throw new Error("Failure between markers: " + startMarkerName + ", " + endMarkerName); + throw new Error(`Failure between markers: '${startMarkerName}', '${endMarkerName}'`); } } @@ -740,7 +740,6 @@ namespace FourSlash { } } - public verifyCompletionListAllowsNewIdentifier(negative: boolean) { const completions = this.getCompletionListAtCaret(); @@ -1610,7 +1609,7 @@ namespace FourSlash { if (isFormattingEdit) { const newContent = this.getFileContent(fileName); - if (newContent.replace(/\s/g, "") !== oldContent.replace(/\s/g, "")) { + if (this.removeWhitespace(newContent) !== this.removeWhitespace(oldContent)) { this.raiseError("Formatting operation destroyed non-whitespace content"); } } @@ -1676,6 +1675,10 @@ namespace FourSlash { } } + private removeWhitespace(text: string): string { + return text.replace(/\s/g, ""); + } + public goToBOF() { this.goToPosition(0); } @@ -2041,6 +2044,47 @@ namespace FourSlash { } } + private getCodeFixes(errorCode?: number) { + const fileName = this.activeFile.fileName; + const diagnostics = this.getDiagnostics(fileName); + + if (diagnostics.length === 0) { + this.raiseError("Errors expected."); + } + + if (diagnostics.length > 1 && errorCode !== undefined) { + this.raiseError("When there's more than one error, you must specify the errror to fix."); + } + + const diagnostic = !errorCode ? diagnostics[0] : ts.find(diagnostics, d => d.code == errorCode); + + return this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.length, [diagnostic.code]); + } + + public verifyCodeFixAtPosition(expectedText: string, errorCode?: number) { + const ranges = this.getRanges(); + if (ranges.length == 0) { + this.raiseError("At least one range should be specified in the testfile."); + } + + const actual = this.getCodeFixes(errorCode); + + if (!actual || actual.length == 0) { + this.raiseError("No codefixes returned."); + } + + if (actual.length > 1) { + this.raiseError("More than 1 codefix returned."); + } + + this.applyEdits(actual[0].changes[0].fileName, actual[0].changes[0].textChanges, /*isFormattingEdit*/ false); + const actualText = this.rangeText(ranges[0]); + + if (this.removeWhitespace(actualText) !== this.removeWhitespace(expectedText)) { + this.raiseError(`Actual text doesn't match expected text. Actual: '${actualText}' Expected: '${expectedText}'`); + } + } + public verifyDocCommentTemplate(expected?: ts.TextInsertion) { const name = "verifyDocCommentTemplate"; const actual = this.languageService.getDocCommentTemplateAtPosition(this.activeFile.fileName, this.currentCaretPosition); @@ -2312,6 +2356,18 @@ namespace FourSlash { } } + public verifyCodeFixAvailable(negative: boolean, errorCode?: number) { + const fixes = this.getCodeFixes(errorCode); + + if (negative && fixes && fixes.length > 0) { + this.raiseError(`verifyCodeFixAvailable failed - expected no fixes, actual: ${fixes.length}`); + } + + if (!negative && (fixes === undefined || fixes.length === 0)) { + this.raiseError(`verifyCodeFixAvailable failed - expected code fixes, actual: 0`); + } + } + // Get the text of the entire line the caret is currently at private getCurrentLineContent() { const text = this.getFileContent(this.activeFile.fileName); @@ -3099,6 +3155,10 @@ namespace FourSlashInterface { public isValidBraceCompletionAtPosition(openingBrace: string) { this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace); } + + public codeFixAvailable(errorCode?: number) { + this.state.verifyCodeFixAvailable(this.negative, errorCode); + } } export class Verify extends VerifyNegatable { @@ -3278,6 +3338,10 @@ namespace FourSlashInterface { this.DocCommentTemplate(/*expectedText*/ undefined, /*expectedOffset*/ undefined, /*empty*/ true); } + public codeFixAtPosition(expectedText: string, errorCode?: number): void { + this.state.verifyCodeFixAtPosition(expectedText, errorCode); + } + public navigationBar(json: any) { this.state.verifyNavigationBar(json); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 38b780c5900..3139511d2c7 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -486,6 +486,9 @@ namespace Harness.LanguageService { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); } + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): ts.CodeAction[] { + throw new Error("Not supported on the shim."); + } getEmitOutput(fileName: string): ts.EmitOutput { return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); } diff --git a/src/harness/unittests/convertCompilerOptionsFromJson.ts b/src/harness/unittests/convertCompilerOptionsFromJson.ts index ba25409a9b4..33cad7130c7 100644 --- a/src/harness/unittests/convertCompilerOptionsFromJson.ts +++ b/src/harness/unittests/convertCompilerOptionsFromJson.ts @@ -405,6 +405,7 @@ namespace ts { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, + skipLibCheck: true, module: ModuleKind.CommonJS, target: ScriptTarget.ES5, noImplicitAny: false, @@ -433,6 +434,7 @@ namespace ts { allowJs: false, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, + skipLibCheck: true, module: ModuleKind.CommonJS, target: ScriptTarget.ES5, noImplicitAny: false, @@ -456,7 +458,8 @@ namespace ts { { allowJs: true, maxNodeModuleJsDepth: 2, - allowSyntheticDefaultImports: true + allowSyntheticDefaultImports: true, + skipLibCheck: true }, errors: [{ file: undefined, @@ -477,7 +480,8 @@ namespace ts { { allowJs: true, maxNodeModuleJsDepth: 2, - allowSyntheticDefaultImports: true + allowSyntheticDefaultImports: true, + skipLibCheck: true }, errors: [] } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index ce46fe02e29..3faca6af7e4 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1,8 +1,10 @@ -/// +/// /// namespace ts.projectSystem { import TI = server.typingsInstaller; + import protocol = server.protocol; + import CommandNames = server.CommandNames; const safeList = { path: "/safeList.json", @@ -128,7 +130,7 @@ namespace ts.projectSystem { return combinePaths(getDirectoryPath(libFile.path), "tsc.js"); } - export function toExternalFile(fileName: string): server.protocol.ExternalFile { + export function toExternalFile(fileName: string): protocol.ExternalFile { return { fileName }; } @@ -136,6 +138,19 @@ namespace ts.projectSystem { return map(fileNames, toExternalFile); } + export class TestServerEventManager { + private events: server.ProjectServiceEvent[] = []; + + handler: server.ProjectServiceEventHandler = (event: server.ProjectServiceEvent) => { + this.events.push(event); + } + + checkEventCountOfType(eventType: "context" | "configFileDiag", expectedCount: number) { + const eventsOfType = filter(this.events, e => e.eventName === eventType); + assert.equal(eventsOfType.length, expectedCount, `The actual event counts of type ${eventType} is ${eventsOfType.length}, while expected ${expectedCount}`); + } + } + export interface TestServerHostCreationParameters { useCaseSensitiveFileNames?: boolean; executingFilePath?: string; @@ -159,11 +174,11 @@ namespace ts.projectSystem { return host; } - export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller) { + export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller, projectServiceEventHandler?: server.ProjectServiceEventHandler) { if (typingsInstaller === undefined) { typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); } - return new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false); + return new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ projectServiceEventHandler !== undefined, projectServiceEventHandler); } export interface CreateProjectServiceParameters { @@ -505,7 +520,7 @@ namespace ts.projectSystem { } export function makeSessionRequest(command: string, args: T) { - const newRequest: server.protocol.Request = { + const newRequest: protocol.Request = { seq: 0, type: "request", command, @@ -516,7 +531,7 @@ namespace ts.projectSystem { export function openFilesForSession(files: FileOrFolder[], session: server.Session) { for (const file of files) { - const request = makeSessionRequest(server.CommandNames.Open, { file: file.path }); + const request = makeSessionRequest(CommandNames.Open, { file: file.path }); session.executeCommand(request); } } @@ -1729,7 +1744,7 @@ namespace ts.projectSystem { }); describe("navigate-to for javascript project", () => { - function containsNavToItem(items: server.protocol.NavtoItem[], itemName: string, itemKind: string) { + function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; } @@ -1747,12 +1762,12 @@ namespace ts.projectSystem { openFilesForSession([file1], session); // Try to find some interface type defined in lib.d.ts - const libTypeNavToRequest = makeSessionRequest(server.CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); - const items: server.protocol.NavtoItem[] = session.executeCommand(libTypeNavToRequest).response; + const libTypeNavToRequest = makeSessionRequest(CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); + const items: protocol.NavtoItem[] = session.executeCommand(libTypeNavToRequest).response; assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`); - const localFunctionNavToRequst = makeSessionRequest(server.CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); - const items2: server.protocol.NavtoItem[] = session.executeCommand(localFunctionNavToRequst).response; + const localFunctionNavToRequst = makeSessionRequest(CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); + const items2: protocol.NavtoItem[] = session.executeCommand(localFunctionNavToRequst).response; assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); }); }); @@ -2112,4 +2127,255 @@ namespace ts.projectSystem { projectService.inferredProjects[0].getLanguageService().getProgram(); }); }); + + describe("rename a module file and rename back", () => { + it("should restore the states for inferred projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createServerHost([moduleFile, file1]); + const session = createSession(host); + + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 0); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1]); + host.triggerFileWatcherCallback(moduleFileOldPath); + host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 1); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1]); + host.triggerFileWatcherCallback(moduleFileNewPath); + host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); + host.runQueuedTimeoutCallbacks(); + + // Make a change to trigger the program rebuild + const changeRequest = makeSessionRequest( + server.CommandNames.Change, + { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } + ); + session.executeCommand(changeRequest); + host.runQueuedTimeoutCallbacks(); + + diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 0); + }); + + it("should restore the states for configured projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([moduleFile, file1, configFile]); + const session = createSession(host); + + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 0); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, configFile]); + host.triggerFileWatcherCallback(moduleFileOldPath); + host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 1); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, configFile]); + host.triggerFileWatcherCallback(moduleFileNewPath); + host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 0); + }); + + }); + + describe("add the missing module file for inferred project", () => { + it("should remove the `module not found` error", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createServerHost([file1]); + const session = createSession(host); + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 1); + + host.reloadFS([file1, moduleFile]); + host.triggerDirectoryWatcherCallback(getDirectoryPath(file1.path), moduleFile.path); + host.runQueuedTimeoutCallbacks(); + + // Make a change to trigger the program rebuild + const changeRequest = makeSessionRequest( + server.CommandNames.Change, + { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } + ); + session.executeCommand(changeRequest); + + // Recheck + diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 0); + }); + }); + + describe("Configure file diagnostics events", () => { + + it("are generated when the config file has errors", () => { + const serverEventManager = new TestServerEventManager(); + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + + const host = createServerHost([file, configFile]); + const session = createSession(host, /*typingsInstaller*/ undefined, serverEventManager.handler); + openFilesForSession([file], session); + serverEventManager.checkEventCountOfType("configFileDiag", 1); + }); + + it("are generated when the config file doesn't have errors", () => { + const serverEventManager = new TestServerEventManager(); + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const host = createServerHost([file, configFile]); + const session = createSession(host, /*typingsInstaller*/ undefined, serverEventManager.handler); + openFilesForSession([file], session); + serverEventManager.checkEventCountOfType("configFileDiag", 1); + }); + }); + + describe("skipLibCheck", () => { + it("should be turned on for js-only inferred projects", () => { + const file1 = { + path: "/a/b/file1.js", + content: ` + /// + var x = 1;` + }; + const file2 = { + path: "/a/b/file2.d.ts", + content: ` + interface T { + name: string; + }; + interface T { + name: number; + };` + }; + const host = createServerHost([file1, file2]); + const session = createSession(host); + openFilesForSession([file1, file2], session); + + const file2GetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: file2.path } + ); + let errorResult = session.executeCommand(file2GetErrRequest).response; + assert.isTrue(errorResult.length === 0); + + const closeFileRequest = makeSessionRequest(CommandNames.Close, { file: file1.path }); + session.executeCommand(closeFileRequest); + errorResult = session.executeCommand(file2GetErrRequest).response; + assert.isTrue(errorResult.length !== 0); + + openFilesForSession([file1], session); + errorResult = session.executeCommand(file2GetErrRequest).response; + assert.isTrue(errorResult.length === 0); + }); + + it("should be turned on for js-only external projects", () => { + const jsFile = { + path: "/a/b/file1.js", + content: "let x =1;" + }; + const dTsFile = { + path: "/a/b/file2.d.ts", + content: ` + interface T { + name: string; + }; + interface T { + name: number; + };` + }; + const host = createServerHost([jsFile, dTsFile]); + const session = createSession(host); + + const openExternalProjectRequest = makeSessionRequest( + CommandNames.OpenExternalProject, + { + projectFileName: "project1", + rootFiles: toExternalFiles([jsFile.path, dTsFile.path]), + options: {} + } + ); + session.executeCommand(openExternalProjectRequest); + + const dTsFileGetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: dTsFile.path } + ); + const errorResult = session.executeCommand(dTsFileGetErrRequest).response; + assert.isTrue(errorResult.length === 0); + }); + }); } \ No newline at end of file diff --git a/src/server/client.ts b/src/server/client.ts index 0b82073fb32..a9fbb585364 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -425,11 +425,35 @@ namespace ts.server { } getSyntacticDiagnostics(fileName: string): Diagnostic[] { - throw new Error("Not Implemented Yet."); + const args: protocol.SyntacticDiagnosticsSyncRequestArgs = { file: fileName }; + + const request = this.processRequest(CommandNames.SyntacticDiagnosticsSync, args); + const response = this.processResponse(request); + + return (response.body).map(entry => this.convertDiagnostic(entry, fileName)); } getSemanticDiagnostics(fileName: string): Diagnostic[] { - throw new Error("Not Implemented Yet."); + const args: protocol.SemanticDiagnosticsSyncRequestArgs = { file: fileName }; + + const request = this.processRequest(CommandNames.SemanticDiagnosticsSync, args); + const response = this.processResponse(request); + + return (response.body).map(entry => this.convertDiagnostic(entry, fileName)); + } + + convertDiagnostic(entry: protocol.Diagnostic, fileName: string): Diagnostic { + const start = this.lineOffsetToPosition(fileName, entry.start); + const end = this.lineOffsetToPosition(fileName, entry.end); + + return { + file: undefined, + start: start, + length: end - start, + messageText: entry.text, + category: undefined, + code: entry.code + }; } getCompilerOptionsDiagnostics(): Diagnostic[] { @@ -630,6 +654,48 @@ namespace ts.server { throw new Error("Not Implemented Yet."); } + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[] { + const startLineOffset = this.positionToOneBasedLineOffset(fileName, start); + const endLineOffset = this.positionToOneBasedLineOffset(fileName, end); + + const args: protocol.CodeFixRequestArgs = { + file: fileName, + startLine: startLineOffset.line, + startOffset: startLineOffset.offset, + endLine: endLineOffset.line, + endOffset: endLineOffset.offset, + errorCodes: errorCodes, + }; + + const request = this.processRequest(CommandNames.GetCodeFixes, args); + const response = this.processResponse(request); + + return response.body.map(entry => this.convertCodeActions(entry, fileName)); + } + + convertCodeActions(entry: protocol.CodeAction, fileName: string): CodeAction { + return { + description: entry.description, + changes: entry.changes.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) + })) + }; + } + + convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): ts.TextChange { + const start = this.lineOffsetToPosition(fileName, change.start); + const end = this.lineOffsetToPosition(fileName, change.end); + + return { + span: { + start: start, + length: end - start + }, + newText: change.newText ? change.newText : "" + }; + } + getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { const lineOffset = this.positionToOneBasedLineOffset(fileName, position); const args: protocol.FileLocationRequestArgs = { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ebc7a389706..8c96a5c4c85 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -181,6 +181,8 @@ namespace ts.server { private toCanonicalFileName: (f: string) => string; + public lastDeletedFile: ScriptInfo; + constructor(public readonly host: ServerHost, public readonly logger: Logger, public readonly cancellationToken: HostCancellationToken, @@ -273,7 +275,7 @@ namespace ts.server { else { projectsToUpdate = []; for (const f of this.changedFiles) { - projectsToUpdate = projectsToUpdate.concat(f.containingProjects); + projectsToUpdate = projectsToUpdate.concat(f.containingProjects); } } this.updateProjectGraphs(projectsToUpdate); @@ -343,6 +345,7 @@ namespace ts.server { if (!info.isOpen) { this.filenameToScriptInfo.remove(info.path); + this.lastDeletedFile = info; // capture list of projects since detachAllProjects will wipe out original list const containingProjects = info.containingProjects.slice(); @@ -351,6 +354,7 @@ namespace ts.server { // update projects to make sure that set of referenced files is correct this.updateProjectGraphs(containingProjects); + this.lastDeletedFile = undefined; if (!this.eventHandler) { return; @@ -756,12 +760,14 @@ namespace ts.server { } private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile?: string) { - if (diagnostics && diagnostics.length > 0) { - this.eventHandler({ - eventName: "configFileDiag", - data: { configFileName, diagnostics, triggerFile } - }); + if (!this.eventHandler) { + return; } + + this.eventHandler({ + eventName: "configFileDiag", + data: { configFileName, diagnostics: diagnostics || [], triggerFile } + }); } private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], clientFileName?: string) { diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index d05b7897157..a2abe224a5f 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -52,7 +52,7 @@ namespace ts.server { }; } - private resolveNamesWithLocalCache( + private resolveNamesWithLocalCache( names: string[], containingFile: string, cache: ts.FileMap>, @@ -65,6 +65,7 @@ namespace ts.server { const newResolutions = new StringMap(); const resolvedModules: R[] = []; const compilerOptions = this.getCompilationSettings(); + const lastDeletedFileName = this.project.projectService.lastDeletedFile && this.project.projectService.lastDeletedFile.fileName; for (const name of names) { // check if this is a duplicate entry in the list @@ -94,8 +95,11 @@ namespace ts.server { return false; } - if (getResult(resolution)) { - // TODO: consider checking failedLookupLocations + const result = getResult(resolution); + if (result) { + if (result.resolvedFileName && result.resolvedFileName === lastDeletedFileName) { + return false; + } return true; } diff --git a/src/server/project.ts b/src/server/project.ts index 4d65e59d59a..c6b03d01fb9 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -20,16 +20,42 @@ namespace ts.server { } } - function isJsOrDtsFile(info: ScriptInfo) { - return info.scriptKind === ScriptKind.JS || info.scriptKind == ScriptKind.JSX || fileExtensionIs(info.fileName, ".d.ts"); + function countEachFileTypes(infos: ScriptInfo[]): { js: number, jsx: number, ts: number, tsx: number, dts: number } { + const result = { js: 0, jsx: 0, ts: 0, tsx: 0, dts: 0 }; + for (const info of infos) { + switch (info.scriptKind) { + case ScriptKind.JS: + result.js += 1; + break; + case ScriptKind.JSX: + result.jsx += 1; + break; + case ScriptKind.TS: + fileExtensionIs(info.fileName, ".d.ts") + ? result.dts += 1 + : result.ts += 1; + break; + case ScriptKind.TSX: + result.tsx += 1; + break; + } + } + return result; + } + + function hasOneOrMoreJsAndNoTsFiles(project: Project) { + const counts = countEachFileTypes(project.getScriptInfos()); + return counts.js > 0 && counts.ts === 0 && counts.tsx === 0; } export function allRootFilesAreJsOrDts(project: Project): boolean { - return project.getRootScriptInfos().every(isJsOrDtsFile); + const counts = countEachFileTypes(project.getRootScriptInfos()); + return counts.ts === 0 && counts.tsx === 0; } export function allFilesAreJsOrDts(project: Project): boolean { - return project.getScriptInfos().every(isJsOrDtsFile); + const counts = countEachFileTypes(project.getScriptInfos()); + return counts.ts === 0 && counts.tsx === 0; } export interface ProjectFilesWithTSDiagnostics extends protocol.ProjectFiles { @@ -71,11 +97,16 @@ namespace ts.server { public typesVersion = 0; - public isJsOnlyProject() { + public isNonTsProject() { this.updateGraph(); return allFilesAreJsOrDts(this); } + public isJsOnlyProject() { + this.updateGraph(); + return hasOneOrMoreJsAndNoTsFiles(this); + } + constructor( readonly projectKind: ProjectKind, readonly projectService: ProjectService, diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index bad403deaf9..0ae54b8e0cf 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -1,4 +1,4 @@ -/** +/** * Declaration module describing the TypeScript Server protocol */ declare namespace ts.server.protocol { @@ -98,19 +98,38 @@ declare namespace ts.server.protocol { projectFileName?: string; } + /** + * A request to get TODO comments from the file + */ export interface TodoCommentRequest extends FileRequest { arguments: TodoCommentRequestArgs; } + /** + * Arguments for TodoCommentRequest request. + */ export interface TodoCommentRequestArgs extends FileRequestArgs { + /** + * Array of target TodoCommentDescriptors that describes TODO comments to be found + */ descriptors: TodoCommentDescriptor[]; } + /** + * A request to get indentation for a location in file + */ export interface IndentationRequest extends FileLocationRequest { arguments: IndentationRequestArgs; } + /** + * Arguments for IndentationRequest request. + */ export interface IndentationRequestArgs extends FileLocationRequestArgs { + /** + * An optional set of settings to be used when computing indentation. + * If argument is omitted - then it will use settings for file that were previously set via 'configure' request or global settings. + */ options?: EditorSettings; } @@ -125,17 +144,26 @@ declare namespace ts.server.protocol { } /** - * A request to get the project information of the current file + * A request to get the project information of the current file. */ export interface ProjectInfoRequest extends Request { arguments: ProjectInfoRequestArgs; } - export interface ProjectRequest extends Request { - arguments: ProjectRequestArgs; + /** + * A request to retrieve compiler options diagnostics for a project + */ + export interface CompilerOptionsDiagnosticsRequest extends Request { + arguments: CompilerOptionsDiagnosticsRequestArgs; } - export interface ProjectRequestArgs { + /** + * Arguments for CompilerOptionsDiagnosticsRequest request. + */ + export interface CompilerOptionsDiagnosticsRequestArgs { + /** + * Name of the project to retrieve compiler options diagnostics. + */ projectFileName: string; } @@ -158,6 +186,11 @@ declare namespace ts.server.protocol { languageServiceDisabled?: boolean; } + /** + * Represents diagnostic info that includes location of diagnostic in two forms + * - start position and length of the error span + * - startLocation and endLocation - a pair of Location objects that store start/end line and offset of the error span. + */ export interface DiagnosticWithLinePosition { message: string; start: number; @@ -203,6 +236,53 @@ declare namespace ts.server.protocol { position?: number; } + /** + * Request for the available codefixes at a specific position. + */ + export interface CodeFixRequest extends Request { + arguments: CodeFixRequestArgs; + } + + /** + * Instances of this interface specify errorcodes on a specific location in a sourcefile. + */ + export interface CodeFixRequestArgs extends FileRequestArgs { + /** + * The line number for the request (1-based). + */ + startLine?: number; + + /** + * The character offset (on the line) for the request (1-based). + */ + startOffset?: number; + + /** + * Position (can be specified instead of line/offset pair) + */ + startPosition?: number; + + /** + * The line number for the request (1-based). + */ + endLine?: number; + + /** + * The character offset (on the line) for the request (1-based). + */ + endOffset?: number; + + /** + * Position (can be specified instead of line/offset pair) + */ + endPosition?: number; + + /** + * Errorcodes we want to get the fixes for. + */ + errorCodes?: number[]; + } + /** * A request whose arguments specify a file location (file, line, col). */ @@ -210,13 +290,25 @@ declare namespace ts.server.protocol { arguments: FileLocationRequestArgs; } - export interface FileSpanRequestArgs extends FileRequestArgs { - start: number; - length: number; + /** + * A request to get semantic diagnostics for a span in the file + */ + export interface SemanticDiagnosticsRequest extends FileRequest { + arguments: SemanticDiagnosticsRequestArgs; } - export interface FileSpanRequest extends FileRequest { - arguments: FileSpanRequestArgs; + /** + * Arguments for SemanticDiagnosticsRequest request. + */ + export interface SemanticDiagnosticsRequestArgs extends FileRequestArgs { + /** + * Start position of the span. + */ + start: number; + /** + * Length of the span. + */ + length: number; } /** @@ -308,11 +400,20 @@ declare namespace ts.server.protocol { body?: FileSpan[]; } + /** + * Request to get brace completion for a location in the file. + */ export interface BraceCompletionRequest extends FileLocationRequest { arguments: BraceCompletionRequestArgs; } + /** + * Argument for BraceCompletionRequest request. + */ export interface BraceCompletionRequestArgs extends FileLocationRequestArgs { + /** + * Kind of opening brace + */ openingBrace: string; } @@ -344,10 +445,17 @@ declare namespace ts.server.protocol { arguments: DocumentHighlightsRequestArgs; } + /** + * Span augmented with extra information that denotes the kind of the highlighting to be used for span. + * Kind is taken from HighlightSpanKind type. + */ export interface HighlightSpan extends TextSpan { kind: string; } + /** + * Represents a set of highligh spans for a give name + */ export interface DocumentHighlightsItem { /** * File containing highlight spans. @@ -360,6 +468,9 @@ declare namespace ts.server.protocol { highlightSpans: HighlightSpan[]; } + /** + * Response for a DocumentHighlightsRequest request. + */ export interface DocumentHighlightsResponse extends Response { body?: DocumentHighlightsItem[]; } @@ -423,12 +534,20 @@ declare namespace ts.server.protocol { body?: ReferencesResponseBody; } + /** + * Argument for RenameRequest request. + */ export interface RenameRequestArgs extends FileLocationRequestArgs { + /** + * Should text at specified location be found/changed in comments? + */ findInComments?: boolean; + /** + * Should text at specified location be found/changed in strings? + */ findInStrings?: boolean; } - /** * Rename request; value of command field is "rename". Return * response giving the file locations that reference the symbol @@ -503,17 +622,53 @@ declare namespace ts.server.protocol { body?: RenameResponseBody; } + /** + * Represents a file in external project. + * External project is project whose set of files, compilation options and open\close state + * is maintained by the client (i.e. if all this data come from .csproj file in Visual Studio). + * External project will exist even if all files in it are closed and should be closed explicity. + * If external project includes one or more tsconfig.json/jsconfig.json files then tsserver will + * create configured project for every config file but will maintain a link that these projects were created + * as a result of opening external project so they should be removed once external project is closed. + */ export interface ExternalFile { + /** + * Name of file file + */ fileName: string; + /** + * Script kind of the file + */ scriptKind?: ScriptKind; + /** + * Whether file has mixed content (i.e. .cshtml file that combines html markup with C#/JavaScript) + */ hasMixedContent?: boolean; + /** + * Content of the file + */ content?: string; } + /** + * Represent an external project + */ export interface ExternalProject { + /** + * Project name + */ projectFileName: string; + /** + * List of root files in project + */ rootFiles: ExternalFile[]; + /** + * Compiler options for the project + */ options: ExternalProjectCompilerOptions; + /** + * Explicitly specified typing options for the project + */ typingOptions?: TypingOptions; } @@ -522,18 +677,45 @@ declare namespace ts.server.protocol { * compiler settings. */ export interface ExternalProjectCompilerOptions extends CompilerOptions { + /** + * If compile on save is enabled for the project + */ compileOnSave?: boolean; } + /** + * Contains information about current project version + */ export interface ProjectVersionInfo { + /** + * Project name + */ projectName: string; + /** + * true if project is inferred or false if project is external or configured + */ isInferred: boolean; + /** + * Project version + */ version: number; + /** + * Current set of compiler options for project + */ options: CompilerOptions; } + /** + * Represents a set of changes that happen in project + */ export interface ProjectChanges { + /** + * List of added files + */ added: string[]; + /** + * List of removed files + */ removed: string[]; } @@ -545,17 +727,41 @@ declare namespace ts.server.protocol { * otherwise - assume that nothing is changed */ export interface ProjectFiles { + /** + * Information abount project verison + */ info?: ProjectVersionInfo; + /** + * List of files in project (might be omitted if current state of project can be computed using only information from 'changes') + */ files?: string[]; + /** + * Set of changes in project (omitted if the entire set of files in project should be replaced) + */ changes?: ProjectChanges; } + /** + * Combines project information with project level errors. + */ export interface ProjectFilesWithDiagnostics extends ProjectFiles { + /** + * List of errors in project + */ projectErrors: DiagnosticWithLinePosition[]; } + /** + * Represents set of changes in open file + */ export interface ChangedOpenFile { + /** + * Name of file + */ fileName: string; + /** + * List of changes that should be applied to known open file + */ changes: ts.TextChange[]; } @@ -689,54 +895,117 @@ declare namespace ts.server.protocol { arguments: OpenRequestArgs; } - type OpenExternalProjectArgs = ExternalProject; - + /** + * Request to open or update external project + */ export interface OpenExternalProjectRequest extends Request { arguments: OpenExternalProjectArgs; } - export interface CloseExternalProjectRequestArgs { - projectFileName: string; - } + /** + * Arguments to OpenExternalProjectRequest request + */ + type OpenExternalProjectArgs = ExternalProject; + /** + * Request to open multiple external projects + */ export interface OpenExternalProjectsRequest extends Request { arguments: OpenExternalProjectsArgs; } + /** + * Arguments to OpenExternalProjectsRequest + */ export interface OpenExternalProjectsArgs { + /** + * List of external projects to open or update + */ projects: ExternalProject[]; } + /** + * Request to close external project. + */ export interface CloseExternalProjectRequest extends Request { arguments: CloseExternalProjectRequestArgs; } + /** + * Arguments to CloseExternalProjectRequest request + */ + export interface CloseExternalProjectRequestArgs { + /** + * Name of the project to close + */ + projectFileName: string; + } + + /** + * Request to check if given list of projects is up-to-date and synchronize them if necessary + */ export interface SynchronizeProjectListRequest extends Request { arguments: SynchronizeProjectListRequestArgs; } + /** + * Arguments to SynchronizeProjectListRequest + */ export interface SynchronizeProjectListRequestArgs { + /** + * List of last known projects + */ knownProjects: protocol.ProjectVersionInfo[]; } + /** + * Request to synchronize list of open files with the client + */ export interface ApplyChangedToOpenFilesRequest extends Request { arguments: ApplyChangedToOpenFilesRequestArgs; } + /** + * Arguments to ApplyChangedToOpenFilesRequest + */ export interface ApplyChangedToOpenFilesRequestArgs { + /** + * List of newly open files + */ openFiles?: ExternalFile[]; + /** + * List of open files files that were changes + */ changedFiles?: ChangedOpenFile[]; + /** + * List of files that were closed + */ closedFiles?: string[]; } - export interface SetCompilerOptionsForInferredProjectsArgs { - options: ExternalProjectCompilerOptions; - } - + /** + * Request to set compiler options for inferred projects. + * External projects are opened / closed explicitly. + * Configured projects are opened when user opens loose file that has 'tsconfig.json' or 'jsconfig.json' anywhere in one of containing folders. + * This configuration file will be used to obtain a list of files and configuration settings for the project. + * Inferred projects are created when user opens a loose file that is not the part of external project + * or configured project and will contain only open file and transitive closure of referenced files if 'useOneInferredProject' is false, + * or all open loose files and its transitive closure of referenced files if 'useOneInferredProject' is true. + */ export interface SetCompilerOptionsForInferredProjectsRequest extends Request { arguments: SetCompilerOptionsForInferredProjectsArgs; } + /** + * Argument for SetCompilerOptionsForInferredProjectsRequest request. + */ + export interface SetCompilerOptionsForInferredProjectsArgs { + /** + * Compiler options to be used with inferred projects. + */ + options: ExternalProjectCompilerOptions; + } + /** * Exit request; value of command field is "exit". Ask the server process * to exit. @@ -754,23 +1023,48 @@ declare namespace ts.server.protocol { export interface CloseRequest extends FileRequest { } + /** + * Request to obtain the list of files that should be regenerated if target file is recompiled. + * NOTE: this us query-only operation and does not generate any output on disk. + */ export interface CompileOnSaveAffectedFileListRequest extends FileRequest { } + /** + * Contains a list of files that should be regenerated in a project + */ export interface CompileOnSaveAffectedFileListSingleProject { + /** + * Project name + */ projectFileName: string; + /** + * List of files names that should be recompiled + */ fileNames: string[]; } + /** + * Response for CompileOnSaveAffectedFileListRequest request; + */ export interface CompileOnSaveAffectedFileListResponse extends Response { body: CompileOnSaveAffectedFileListSingleProject[]; } + /** + * Request to recompile the file. All generated outputs (.js, .d.ts or .js.map files) is written on disk. + */ export interface CompileOnSaveEmitFileRequest extends FileRequest { args: CompileOnSaveEmitFileRequestArgs; } + /** + * Arguments for CompileOnSaveEmitFileRequest + */ export interface CompileOnSaveEmitFileRequestArgs extends FileRequestArgs { + /** + * if true - then file should be recompiled even if it does not have any changes. + */ forced?: boolean; } @@ -839,7 +1133,14 @@ declare namespace ts.server.protocol { */ endOffset: number; + /** + * End position of the range for which to format text in file. + */ endPosition?: number; + + /** + * Format options to be used. + */ options?: ts.FormatCodeOptions; } @@ -879,6 +1180,23 @@ declare namespace ts.server.protocol { newText: string; } + export interface FileCodeEdits { + fileName: string; + textChanges: CodeEdit[]; + } + + export interface CodeFixResponse extends Response { + /** The code actions that are available */ + body?: CodeAction[]; + } + + export interface CodeAction { + /** Description of the code action to display in the UI of the editor */ + description: string; + /** Text changes to apply to each file as part of the code action */ + changes: FileCodeEdits[]; + } + /** * Format and format on key response message. */ @@ -1253,6 +1571,11 @@ declare namespace ts.server.protocol { * Text of diagnostic message. */ text: string; + + /** + * The error code of the diagnostic message. + */ + code?: number; } export interface DiagnosticEventBody { diff --git a/src/server/session.ts b/src/server/session.ts index 957258e3072..0acdb1e57b9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -14,6 +14,17 @@ namespace ts.server { return ((1e9 * seconds) + nanoseconds) / 1000000.0; } + function shouldSkipSematicCheck(project: Project) { + if (project.getCompilerOptions().skipLibCheck !== undefined) { + return false; + } + + if ((project.projectKind === ProjectKind.Inferred || project.projectKind === ProjectKind.External) && project.isJsOnlyProject()) { + return true; + } + return false; + } + interface FileStart { file: string; start: ILineInfo; @@ -44,7 +55,8 @@ namespace ts.server { return { start: scriptInfo.positionToLineOffset(diag.start), end: scriptInfo.positionToLineOffset(diag.start + diag.length), - text: ts.flattenDiagnosticMessageText(diag.messageText, "\n") + text: ts.flattenDiagnosticMessageText(diag.messageText, "\n"), + code: diag.code }; } @@ -134,6 +146,9 @@ namespace ts.server { export const NameOrDottedNameSpan = "nameOrDottedNameSpan"; export const BreakpointStatement = "breakpointStatement"; export const CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects"; + export const GetCodeFixes = "getCodeFixes"; + export const GetCodeFixesFull = "getCodeFixes-full"; + export const GetSupportedCodeFixes = "getSupportedCodeFixes"; } export function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string { @@ -155,6 +170,8 @@ namespace ts.server { private immediateId: any; private changeSeq = 0; + private eventHander: ProjectServiceEventHandler; + constructor( private host: ServerHost, cancellationToken: HostCancellationToken, @@ -163,17 +180,18 @@ namespace ts.server { private byteLength: (buf: string, encoding?: string) => number, private hrtime: (start?: number[]) => number[], protected logger: Logger, - protected readonly canUseEvents: boolean) { + protected readonly canUseEvents: boolean, + eventHandler?: ProjectServiceEventHandler) { - const eventHandler: ProjectServiceEventHandler = canUseEvents - ? event => this.handleEvent(event) + this.eventHander = canUseEvents + ? eventHandler || (event => this.defaultEventHandler(event)) : undefined; - this.projectService = new ProjectService(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, eventHandler); + this.projectService = new ProjectService(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, this.eventHander); this.gcTimer = new GcTimer(host, /*delay*/ 7000, logger); } - private handleEvent(event: ProjectServiceEvent) { + private defaultEventHandler(event: ProjectServiceEvent) { switch (event.eventName) { case "context": const { project, fileName } = event.data; @@ -252,12 +270,13 @@ namespace ts.server { private semanticCheck(file: NormalizedPath, project: Project) { try { - const diags = project.getLanguageService().getSemanticDiagnostics(file); - - if (diags) { - const bakedDiags = diags.map((diag) => formatDiag(file, project, diag)); - this.event({ file: file, diagnostics: bakedDiags }, "semanticDiag"); + let diags: Diagnostic[] = []; + if (!shouldSkipSematicCheck(project)) { + diags = project.getLanguageService().getSemanticDiagnostics(file); } + + const bakedDiags = diags.map((diag) => formatDiag(file, project, diag)); + this.event({ file: file, diagnostics: bakedDiags }, "semanticDiag"); } catch (err) { this.logError(err, "semantic check"); @@ -342,7 +361,7 @@ namespace ts.server { } } - private getEncodedSemanticClassifications(args: protocol.FileSpanRequestArgs) { + private getEncodedSemanticClassifications(args: protocol.SemanticDiagnosticsRequestArgs) { const { file, project } = this.getFileAndProject(args); return project.getLanguageService().getEncodedSemanticClassifications(file, args); } @@ -351,7 +370,7 @@ namespace ts.server { return projectFileName && this.projectService.findProject(projectFileName); } - private getCompilerOptionsDiagnostics(args: protocol.ProjectRequestArgs) { + private getCompilerOptionsDiagnostics(args: protocol.CompilerOptionsDiagnosticsRequestArgs) { const project = this.getProject(args.projectFileName); return this.convertToDiagnosticsWithLinePosition(project.getLanguageService().getCompilerOptionsDiagnostics(), /*scriptInfo*/ undefined); } @@ -370,6 +389,9 @@ namespace ts.server { private getDiagnosticsWorker(args: protocol.FileRequestArgs, selector: (project: Project, file: string) => Diagnostic[], includeLinePosition: boolean) { const { project, file } = this.getFileAndProject(args); + if (shouldSkipSematicCheck(project)) { + return []; + } const scriptInfo = project.getScriptInfoForNormalizedPath(file); const diagnostics = selector(project, file); return includeLinePosition @@ -734,8 +756,11 @@ namespace ts.server { */ private openClientFile(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind) { const { configFileName, configFileErrors } = this.projectService.openClientFileWithNormalizedPath(fileName, fileContent, scriptKind); - if (configFileErrors) { - this.configFileDiagnosticEvent(fileName, configFileName, configFileErrors); + if (this.eventHander) { + this.eventHander({ + eventName: "configFileDiag", + data: { fileName, configFileName, diagnostics: configFileErrors || [] } + }); } } @@ -751,7 +776,7 @@ namespace ts.server { return this.getFileAndProjectWorker(args.file, args.projectFileName, /*refreshInferredProjects*/ false, errorOnMissingProject); } - private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, refreshInferredProjects: boolean, errorOnMissingProject: boolean) { + private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, refreshInferredProjects: boolean, errorOnMissingProject: boolean) { const file = toNormalizedPath(uncheckedFileName); const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file, refreshInferredProjects); if (!project && errorOnMissingProject) { @@ -842,13 +867,7 @@ namespace ts.server { return undefined; } - return edits.map((edit) => { - return { - start: scriptInfo.positionToLineOffset(edit.span.start), - end: scriptInfo.positionToLineOffset(ts.textSpanEnd(edit.span)), - newText: edit.newText ? edit.newText : "" - }; - }); + return edits.map(edit => this.convertTextChangeToCodeEdit(edit, scriptInfo)); } private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { @@ -1127,7 +1146,7 @@ namespace ts.server { return combineProjectOutput( projects, project => { - const navItems = project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isJsOnlyProject()); + const navItems = project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isNonTsProject()); if (!navItems) { return []; } @@ -1165,7 +1184,7 @@ namespace ts.server { else { return combineProjectOutput( projects, - project => project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isJsOnlyProject()), + project => project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isNonTsProject()), /*comparer*/ undefined, navigateToItemIsEqualTo); } @@ -1199,6 +1218,55 @@ namespace ts.server { } } + private getSupportedCodeFixes(): string[] { + return ts.getSupportedCodeFixes(); + } + + private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): protocol.CodeAction[] | CodeAction[] { + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const startPosition = getStartPosition(); + const endPosition = getEndPosition(); + + const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes); + if (!codeActions) { + return undefined; + } + if (simplifiedResult) { + return codeActions.map(codeAction => this.mapCodeAction(codeAction, scriptInfo)); + } + else { + return codeActions; + } + + function getStartPosition() { + return args.startPosition !== undefined ? args.startPosition : scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset); + } + + function getEndPosition() { + return args.endPosition !== undefined ? args.endPosition : scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); + } + } + + private mapCodeAction(codeAction: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction { + return { + description: codeAction.description, + changes: codeAction.changes.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)) + })) + }; + } + + private convertTextChangeToCodeEdit(change: ts.TextChange, scriptInfo: ScriptInfo): protocol.CodeEdit { + return { + start: scriptInfo.positionToLineOffset(change.span.start), + end: scriptInfo.positionToLineOffset(change.span.start + change.span.length), + newText: change.newText ? change.newText : "" + }; + } + private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] { const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); @@ -1438,10 +1506,10 @@ namespace ts.server { [CommandNames.SignatureHelpFull]: (request: protocol.SignatureHelpRequest) => { return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.CompilerOptionsDiagnosticsFull]: (request: protocol.ProjectRequest) => { + [CommandNames.CompilerOptionsDiagnosticsFull]: (request: protocol.CompilerOptionsDiagnosticsRequest) => { return this.requiredResponse(this.getCompilerOptionsDiagnostics(request.arguments)); }, - [CommandNames.EncodedSemanticClassificationsFull]: (request: protocol.FileSpanRequest) => { + [CommandNames.EncodedSemanticClassificationsFull]: (request: protocol.SemanticDiagnosticsRequest) => { return this.requiredResponse(this.getEncodedSemanticClassifications(request.arguments)); }, [CommandNames.Cleanup]: (request: protocol.Request) => { @@ -1521,6 +1589,15 @@ namespace ts.server { [CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => { this.projectService.reloadProjects(); return this.notRequired(); + }, + [CommandNames.GetCodeFixes]: (request: protocol.CodeFixRequest) => { + return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.GetCodeFixesFull]: (request: protocol.CodeFixRequest) => { + return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ false)); + }, + [CommandNames.GetSupportedCodeFixes]: (request: protocol.Request) => { + return this.requiredResponse(this.getSupportedCodeFixes()); } }); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 97bad9be9de..110727e5c4f 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -175,7 +175,8 @@ namespace ts.server { dispose: (): any => throwLanguageServiceIsDisabledError(), getCompletionEntrySymbol: (): any => throwLanguageServiceIsDisabledError(), getImplementationAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getSourceFile: (): any => throwLanguageServiceIsDisabledError() + getSourceFile: (): any => throwLanguageServiceIsDisabledError(), + getCodeFixesAtPosition: (): any => throwLanguageServiceIsDisabledError() }; export interface ServerLanguageServiceHost { diff --git a/src/services/codefixes/codeFixProvider.ts b/src/services/codefixes/codeFixProvider.ts new file mode 100644 index 00000000000..02aaeceb56d --- /dev/null +++ b/src/services/codefixes/codeFixProvider.ts @@ -0,0 +1,47 @@ +/* @internal */ +namespace ts { + export interface CodeFix { + errorCodes: number[]; + getCodeActions(context: CodeFixContext): CodeAction[] | undefined; + } + + export interface CodeFixContext { + errorCode: number; + sourceFile: SourceFile; + span: TextSpan; + program: Program; + newLineCharacter: string; + } + + export namespace codefix { + const codeFixes = new NumberMap(); + + export function registerCodeFix(action: CodeFix) { + forEach(action.errorCodes, error => { + multiMapAdd(codeFixes, error, action); + }); + } + + export function getSupportedErrorCodes() { + const supportedErrorCodes: string[] = []; + codeFixes.forEach((_, key) => { + supportedErrorCodes.push(key.toString()) + }) + return supportedErrorCodes; + } + + export function getFixes(context: CodeFixContext): CodeAction[] { + const fixes = codeFixes.get(context.errorCode); + let allActions: CodeAction[] = []; + + forEach(fixes, f => { + const actions = f.getCodeActions(context); + if (actions && actions.length > 0) { + allActions = allActions.concat(actions); + } + }); + + return allActions; + } + } +} diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts new file mode 100644 index 00000000000..d64a99ca1b9 --- /dev/null +++ b/src/services/codefixes/fixes.ts @@ -0,0 +1 @@ +/// diff --git a/src/services/codefixes/superFixes.ts b/src/services/codefixes/superFixes.ts new file mode 100644 index 00000000000..f117799f3e1 --- /dev/null +++ b/src/services/codefixes/superFixes.ts @@ -0,0 +1,81 @@ +/* @internal */ +namespace ts.codefix { + function getOpenBraceEnd(constructor: ConstructorDeclaration, sourceFile: SourceFile) { + // First token is the open curly, this is where we want to put the 'super' call. + return constructor.body.getFirstToken(sourceFile).getEnd(); + } + + registerCodeFix({ + errorCodes: [Diagnostics.Constructors_for_derived_classes_must_contain_a_super_call.code], + getCodeActions: (context: CodeFixContext) => { + const sourceFile = context.sourceFile; + const token = getTokenAtPosition(sourceFile, context.span.start); + + if (token.kind !== SyntaxKind.ConstructorKeyword) { + return undefined; + } + + const newPosition = getOpenBraceEnd(token.parent, sourceFile); + return [{ + description: getLocaleSpecificMessage(Diagnostics.Add_missing_super_call), + changes: [{ fileName: sourceFile.fileName, textChanges: [{ newText: "super();", span: { start: newPosition, length: 0 } }] }] + }]; + } + }); + + registerCodeFix({ + errorCodes: [Diagnostics.super_must_be_called_before_accessing_this_in_the_constructor_of_a_derived_class.code], + getCodeActions: (context: CodeFixContext) => { + const sourceFile = context.sourceFile; + + const token = getTokenAtPosition(sourceFile, context.span.start); + if (token.kind !== SyntaxKind.ThisKeyword) { + return undefined; + } + + const constructor = getContainingFunction(token); + const superCall = findSuperCall((constructor).body); + if (!superCall) { + return undefined; + } + + // figure out if the this access is actuall inside the supercall + // i.e. super(this.a), since in that case we won't suggest a fix + if (superCall.expression && superCall.expression.kind == SyntaxKind.CallExpression) { + const arguments = (superCall.expression).arguments; + for (let i = 0; i < arguments.length; i++) { + if ((arguments[i]).expression === token) { + return undefined; + } + } + } + + const newPosition = getOpenBraceEnd(constructor, sourceFile); + const changes = [{ + fileName: sourceFile.fileName, textChanges: [{ + newText: superCall.getText(sourceFile), + span: { start: newPosition, length: 0 } + }, + { + newText: "", + span: { start: superCall.getStart(sourceFile), length: superCall.getWidth(sourceFile) } + }] + }]; + + return [{ + description: getLocaleSpecificMessage(Diagnostics.Make_super_call_the_first_statement_in_the_constructor), + changes + }]; + + function findSuperCall(n: Node): ExpressionStatement { + if (n.kind === SyntaxKind.ExpressionStatement && isSuperCall((n).expression)) { + return n; + } + if (isFunctionLike(n)) { + return undefined; + } + return forEachChild(n, findSuperCall); + } + } + }); +} \ No newline at end of file diff --git a/src/services/navigationBar.ts b/src/services/navigationBar.ts index f3b91068d94..57cfc1f2c40 100644 --- a/src/services/navigationBar.ts +++ b/src/services/navigationBar.ts @@ -324,7 +324,7 @@ namespace ts.NavigationBar { } // More efficient to create a collator once and use its `compare` than to call `a.localeCompare(b)` many times. - const collator: { compare(a: string, b: string): number } = typeof Intl === "undefined" ? undefined : new Intl.Collator(); + const collator: { compare(a: string, b: string): number } = typeof Intl === "object" && typeof Intl.Collator === "function" ? new Intl.Collator() : undefined; // Intl is missing in Safari, and node 0.10 treats "a" as greater than "B". const localeCompareIsCorrect = collator && collator.compare("a", "B") < 0; const localeCompareFix: (a: string, b: string) => number = localeCompareIsCorrect ? collator.compare : function(a, b) { diff --git a/src/services/services.ts b/src/services/services.ts index d6e3d633228..74b6a5459db 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -24,6 +24,8 @@ /// /// /// +/// +/// namespace ts { /** The version of the language service API */ @@ -664,6 +666,7 @@ namespace ts { return { getNodeConstructor: () => NodeObject, getTokenConstructor: () => TokenObject, + getIdentifierConstructor: () => IdentifierObject, getSourceFileConstructor: () => SourceFileObject, getSymbolConstructor: () => SymbolObject, @@ -730,9 +733,13 @@ namespace ts { }; } - // Cache host information about script should be refreshed + export function getSupportedCodeFixes() { + return codefix.getSupportedErrorCodes(); + } + + // Cache host information about script Should be refreshed // at each language service public entry point, since we don't know when - // set of scripts handled by the host changes. + // the set of scripts handled by the host changes. class HostCache { private fileNameToEntry: FileMap; private _compilationSettings: CompilerOptions; @@ -1654,6 +1661,34 @@ namespace ts { return []; } + function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const span = { start, length: end - start }; + const newLineChar = getNewLineOrDefaultFromHost(host); + + let allFixes: CodeAction[] = []; + + forEach(errorCodes, error => { + cancellationToken.throwIfCancellationRequested(); + + const context = { + errorCode: error, + sourceFile: sourceFile, + span: span, + program: program, + newLineCharacter: newLineChar + }; + + const fixes = codefix.getFixes(context); + if (fixes) { + allFixes = allFixes.concat(fixes); + } + }); + + return allFixes; + } + function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion { return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position); } @@ -1877,6 +1912,7 @@ namespace ts { getFormattingEditsAfterKeystroke, getDocCommentTemplateAtPosition, isValidBraceCompletionAtPosition, + getCodeFixesAtPosition, getEmitOutput, getNonBoundSourceFile, getSourceFile, diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 58312c6f38f..0066ef94343 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -78,6 +78,8 @@ "formatting/rulesMap.ts", "formatting/rulesProvider.ts", "formatting/smartIndenter.ts", - "formatting/tokenRange.ts" + "formatting/tokenRange.ts", + "codeFixes/codeFixProvider.ts", + "codeFixes/fixes.ts" ] } diff --git a/src/services/types.ts b/src/services/types.ts index d686d1a16ce..31ab65d2013 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -239,6 +239,8 @@ namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[]; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; @@ -291,6 +293,18 @@ namespace ts { newText: string; } + export interface FileTextChanges { + fileName: string; + textChanges: TextChange[]; + } + + export interface CodeAction { + /** Description of the code action to display in the UI of the editor */ + description: string; + /** Text changes to apply to each file as part of the code action */ + changes: FileTextChanges[]; + } + export interface TextInsertion { newText: string; /** The position in newText the caret should point to after the insertion. */ diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 5f86cbbafa4..5e8666e8670 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -136,6 +136,7 @@ declare namespace FourSlashInterface { typeDefinitionCountIs(expectedCount: number): void; implementationListIsEmpty(): void; isValidBraceCompletionAtPosition(openingBrace?: string): void; + codeFixAvailable(): void; } class verify extends verifyNegatable { assertHasRanges(ranges: Range[]): void; @@ -208,6 +209,7 @@ declare namespace FourSlashInterface { noMatchingBracePositionInCurrentFile(bracePosition: number): void; DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void; noDocCommentTemplate(): void; + codeFixAtPosition(expectedText: string, errorCode?: number): void; navigationBar(json: any): void; navigationItemsListCount(count: number, searchValue: string, matchKind?: string, fileName?: string): void; diff --git a/tests/cases/fourslash/jsDocForTypeAlias.ts b/tests/cases/fourslash/jsDocForTypeAlias.ts new file mode 100644 index 00000000000..dbda7ccbc13 --- /dev/null +++ b/tests/cases/fourslash/jsDocForTypeAlias.ts @@ -0,0 +1,7 @@ +/// + +/////** DOC */ +////type /**/T = number + +goTo.marker(); +verify.quickInfoIs("type T = number", "DOC "); diff --git a/tests/cases/fourslash/server/codefix.ts b/tests/cases/fourslash/server/codefix.ts new file mode 100644 index 00000000000..7fbe2cb4fd7 --- /dev/null +++ b/tests/cases/fourslash/server/codefix.ts @@ -0,0 +1,10 @@ +/// + +////class Base{ +////} +////class C extends Base{ +//// constructor() {[| |] +//// } +////} + +verify.codeFixAtPosition('super();'); diff --git a/tests/cases/fourslash/superFix1.ts b/tests/cases/fourslash/superFix1.ts new file mode 100644 index 00000000000..7fbe2cb4fd7 --- /dev/null +++ b/tests/cases/fourslash/superFix1.ts @@ -0,0 +1,10 @@ +/// + +////class Base{ +////} +////class C extends Base{ +//// constructor() {[| |] +//// } +////} + +verify.codeFixAtPosition('super();'); diff --git a/tests/cases/fourslash/superFix2.ts b/tests/cases/fourslash/superFix2.ts new file mode 100644 index 00000000000..880b5d43167 --- /dev/null +++ b/tests/cases/fourslash/superFix2.ts @@ -0,0 +1,13 @@ +/// + +////class Base{ +////} +////class C extends Base{ +//// private a:number; +//// constructor() {[| +//// this.a = 12; +//// super();|] +//// } +////} + +verify.codeFixAtPosition("super(); this.a = 12;"); \ No newline at end of file diff --git a/tests/cases/fourslash/superFix3.ts b/tests/cases/fourslash/superFix3.ts new file mode 100644 index 00000000000..9b443b7df62 --- /dev/null +++ b/tests/cases/fourslash/superFix3.ts @@ -0,0 +1,12 @@ +/// + +////class Base{ +//// constructor(id: number) { } +////} +////class C extends Base{ +//// constructor(private a:number) { +//// super(this.a); +//// } +////} + +verify.not.codeFixAvailable(); \ No newline at end of file