diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 0971adfc098..347e6e97ebd 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -908,6 +908,13 @@ namespace ts { function getSemanticDiagnosticsForFileNoCache(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] { return runWithCancellationToken(() => { + // If skipLibCheck is enabled, skip reporting errors if file is a declaration file. + // If skipDefaultLibCheck is enabled, skip reporting errors if file contains a + // '/// ' directive. + if (options.skipLibCheck && sourceFile.isDeclarationFile || options.skipDefaultLibCheck && sourceFile.hasNoDefaultLib) { + return emptyArray; + } + const typeChecker = getDiagnosticsProducingTypeChecker(); Debug.assert(!!sourceFile.bindDiagnostics); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 321ca9e3e0c..33838b5805f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -3204,6 +3204,122 @@ namespace ts.projectSystem { const errorResult = session.executeCommand(dTsFileGetErrRequest).response; assert.isTrue(errorResult.length === 0); }); + + it("should not report bind errors for declaration files with skipLibCheck=true", () => { + const jsconfigFile = { + path: "/a/jsconfig.json", + content: "{}" + }; + const jsFile = { + path: "/a/jsFile.js", + content: "let x = 1;" + }; + const dTsFile1 = { + path: "/a/dTsFile1.d.ts", + content: ` + declare var x: number;` + }; + const dTsFile2 = { + path: "/a/dTsFile2.d.ts", + content: ` + declare var x: string;` + }; + const host = createServerHost([jsconfigFile, jsFile, dTsFile1, dTsFile2]); + const session = createSession(host); + openFilesForSession([jsFile], session); + + const dTsFile1GetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: dTsFile1.path } + ); + const error1Result = session.executeCommand(dTsFile1GetErrRequest).response; + assert.isTrue(error1Result.length === 0); + + const dTsFile2GetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: dTsFile2.path } + ); + const error2Result = session.executeCommand(dTsFile2GetErrRequest).response; + assert.isTrue(error2Result.length === 0); + }); + + it("should report semanitc errors for loose JS files with '// @ts-check' and skipLibCheck=true", () => { + const jsFile = { + path: "/a/jsFile.js", + content: ` + // @ts-check + let x = 1; + x === "string";` + }; + + const host = createServerHost([jsFile]); + const session = createSession(host); + openFilesForSession([jsFile], session); + + const getErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: jsFile.path } + ); + const errorResult = session.executeCommand(getErrRequest).response; + assert.isTrue(errorResult.length === 1); + assert.equal(errorResult[0].code, Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2.code); + }); + + it("should report semanitc errors for configured js project with '// @ts-check' and skipLibCheck=true", () => { + const jsconfigFile = { + path: "/a/jsconfig.json", + content: "{}" + }; + + const jsFile = { + path: "/a/jsFile.js", + content: ` + // @ts-check + let x = 1; + x === "string";` + }; + + const host = createServerHost([jsconfigFile, jsFile]); + const session = createSession(host); + openFilesForSession([jsFile], session); + + const getErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: jsFile.path } + ); + const errorResult = session.executeCommand(getErrRequest).response; + assert.isTrue(errorResult.length === 1); + assert.equal(errorResult[0].code, Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2.code); + }); + + it("should report semanitc errors for configured js project with checkJs=true and skipLibCheck=true", () => { + const jsconfigFile = { + path: "/a/jsconfig.json", + content: JSON.stringify({ + compilerOptions: { + checkJs: true, + skipLibCheck: true + }, + }) + }; + const jsFile = { + path: "/a/jsFile.js", + content: `let x = 1; + x === "string";` + }; + + const host = createServerHost([jsconfigFile, jsFile]); + const session = createSession(host); + openFilesForSession([jsFile], session); + + const getErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: jsFile.path } + ); + const errorResult = session.executeCommand(getErrRequest).response; + assert.isTrue(errorResult.length === 1); + assert.equal(errorResult[0].code, Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2.code); + }); }); describe("non-existing directories listed in config file input array", () => { diff --git a/src/server/session.ts b/src/server/session.ts index 194c6f667bb..8c8c6387926 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -25,15 +25,26 @@ namespace ts.server { return ((1e9 * seconds) + nanoseconds) / 1000000.0; } - function shouldSkipSemanticCheck(project: Project) { - if (project.projectKind === ProjectKind.Inferred || project.projectKind === ProjectKind.External) { - return project.isJsOnlyProject(); - } - else { - // For configured projects, require that skipLibCheck be set also - const options = project.getCompilerOptions(); - return options.skipLibCheck && !options.checkJs && project.isJsOnlyProject(); + function isDeclarationFileInJSOnlyNonConfiguredProject(project: Project, file: NormalizedPath) { + // Checking for semantic diagnostics is an expensive process. We want to avoid it if we + // know for sure it is not needed. + // For instance, .d.ts files injected by ATA automatically do not produce any relevant + // errors to a JS- only project. + // + // Note that configured projects can set skipLibCheck (on by default in jsconfig.json) to + // disable checking for declaration files. We only need to verify for inferred projects (e.g. + // miscellaneous context in VS) and external projects(e.g.VS.csproj project) with only JS + // files. + // + // We still want to check .js files in a JS-only inferred or external project (e.g. if the + // file has '// @ts-check'). + + if ((project.projectKind === ProjectKind.Inferred || project.projectKind === ProjectKind.External) && + project.isJsOnlyProject()) { + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + return scriptInfo && !scriptInfo.isJavaScript(); } + return false; } interface FileStart { @@ -489,7 +500,7 @@ namespace ts.server { private semanticCheck(file: NormalizedPath, project: Project) { try { let diags: Diagnostic[] = []; - if (!shouldSkipSemanticCheck(project)) { + if (!isDeclarationFileInJSOnlyNonConfiguredProject(project, file)) { diags = project.getLanguageService().getSemanticDiagnostics(file); } @@ -597,7 +608,7 @@ namespace ts.server { private getDiagnosticsWorker(args: protocol.FileRequestArgs, isSemantic: boolean, selector: (project: Project, file: string) => Diagnostic[], includeLinePosition: boolean) { const { project, file } = this.getFileAndProject(args); - if (isSemantic && shouldSkipSemanticCheck(project)) { + if (isSemantic && isDeclarationFileInJSOnlyNonConfiguredProject(project, file)) { return []; } const scriptInfo = project.getScriptInfoForNormalizedPath(file); diff --git a/tests/cases/compiler/noDefaultLib.ts b/tests/cases/compiler/noDefaultLib.ts index 92c8a63fc64..fa1230e4cfe 100644 --- a/tests/cases/compiler/noDefaultLib.ts +++ b/tests/cases/compiler/noDefaultLib.ts @@ -1,3 +1,4 @@ +// @skipDefaultLibCheck: false /// var x; diff --git a/tests/cases/compiler/variableDeclarationInStrictMode1.ts b/tests/cases/compiler/variableDeclarationInStrictMode1.ts index 266e5af44ae..5785beb32aa 100644 --- a/tests/cases/compiler/variableDeclarationInStrictMode1.ts +++ b/tests/cases/compiler/variableDeclarationInStrictMode1.ts @@ -1,2 +1,3 @@ +// @skipDefaultLibCheck: false "use strict"; var eval; \ No newline at end of file