Skip to content

Commit 1fbbead

Browse files
authored
Merge pull request #14568 from Microsoft/checkJSFiles_QuickFixes
Allow skipping diagnostics in .js file using comments and quick fixes to add them
2 parents 3d03f8d + 509b2dc commit 1fbbead

31 files changed

+595
-49
lines changed

Jakefile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ task("generate-code-coverage", ["tests", builtLocalDirectory], function () {
945945
// Browser tests
946946
var nodeServerOutFile = "tests/webTestServer.js";
947947
var nodeServerInFile = "tests/webTestServer.ts";
948-
compileFile(nodeServerOutFile, [nodeServerInFile], [builtLocalDirectory, tscFile], [], /*useBuiltCompiler:*/ true, { noOutFile: true });
948+
compileFile(nodeServerOutFile, [nodeServerInFile], [builtLocalDirectory, tscFile], [], /*useBuiltCompiler:*/ true, { noOutFile: true, lib: "es6" });
949949

950950
desc("Runs browserify on run.js to produce a file suitable for running tests in the browser");
951951
task("browserify", ["tests", builtLocalDirectory, nodeServerOutFile], function() {

src/compiler/diagnosticMessages.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
{
22
"Unterminated string literal.": {
33
"category": "Error",
44
"code": 1002
@@ -3355,6 +3355,24 @@
33553355
"category": "Message",
33563356
"code": 90017
33573357
},
3358+
"Disable checking for this file.": {
3359+
"category": "Message",
3360+
"code": 90018
3361+
},
3362+
"Suppress this error message.": {
3363+
"category": "Message",
3364+
"code": 90019
3365+
},
3366+
"Initialize property '{0}' in the constructor.": {
3367+
"category": "Message",
3368+
"code": 90020
3369+
},
3370+
"Initialize static property '{0}'.": {
3371+
"category": "Message",
3372+
"code": 90021
3373+
},
3374+
3375+
33583376
"Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": {
33593377
"category": "Error",
33603378
"code": 8017

src/compiler/program.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace ts {
66
const emptyArray: any[] = [];
7+
const suppressDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-suppress)?)/;
78

89
export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string {
910
while (true) {
@@ -923,10 +924,36 @@ namespace ts {
923924
const fileProcessingDiagnosticsInFile = fileProcessingDiagnostics.getDiagnostics(sourceFile.fileName);
924925
const programDiagnosticsInFile = programDiagnostics.getDiagnostics(sourceFile.fileName);
925926

926-
return bindDiagnostics.concat(checkDiagnostics, fileProcessingDiagnosticsInFile, programDiagnosticsInFile);
927+
const diagnostics = bindDiagnostics.concat(checkDiagnostics, fileProcessingDiagnosticsInFile, programDiagnosticsInFile);
928+
return isSourceFileJavaScript(sourceFile)
929+
? filter(diagnostics, shouldReportDiagnostic)
930+
: diagnostics;
927931
});
928932
}
929933

934+
/**
935+
* Skip errors if previous line start with '// @ts-suppress' comment, not counting non-empty non-comment lines
936+
*/
937+
function shouldReportDiagnostic(diagnostic: Diagnostic) {
938+
const { file, start } = diagnostic;
939+
const lineStarts = getLineStarts(file);
940+
let { line } = computeLineAndCharacterOfPosition(lineStarts, start);
941+
while (line > 0) {
942+
const previousLineText = file.text.slice(lineStarts[line - 1], lineStarts[line]);
943+
const result = suppressDiagnosticCommentRegEx.exec(previousLineText);
944+
if (!result) {
945+
// non-empty line
946+
return true;
947+
}
948+
if (result[3]) {
949+
// @ts-suppress
950+
return false;
951+
}
952+
line--;
953+
}
954+
return true;
955+
}
956+
930957
function getJavaScriptSyntacticDiagnosticsForFile(sourceFile: SourceFile): Diagnostic[] {
931958
return runWithCancellationToken(() => {
932959
const diagnostics: Diagnostic[] = [];

src/harness/fourslash.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//
1+
//
22
// Copyright (c) Microsoft Corporation. All rights reserved.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -2550,6 +2550,11 @@ namespace FourSlash {
25502550
}
25512551
}
25522552

2553+
public printAvailableCodeFixes() {
2554+
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
2555+
Harness.IO.log(stringify(codeFixes));
2556+
}
2557+
25532558
// Get the text of the entire line the caret is currently at
25542559
private getCurrentLineContent() {
25552560
const text = this.getFileContent(this.activeFile.fileName);
@@ -3738,6 +3743,10 @@ namespace FourSlashInterface {
37383743
this.state.printCompletionListMembers();
37393744
}
37403745

3746+
public printAvailableCodeFixes() {
3747+
this.state.printAvailableCodeFixes();
3748+
}
3749+
37413750
public printBreakpointLocation(pos: number) {
37423751
this.state.printBreakpointLocation(pos);
37433752
}

src/harness/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
{
22
"extends": "../tsconfig-base",
33
"compilerOptions": {
44
"removeComments": false,
@@ -81,6 +81,7 @@
8181
"../services/codefixes/helpers.ts",
8282
"../services/codefixes/importFixes.ts",
8383
"../services/codefixes/unusedIdentifierFixes.ts",
84+
"../services/codefixes/disableJsDiagnostics.ts",
8485

8586
"harness.ts",
8687
"sourceMapRecorder.ts",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
registerCodeFix({
4+
errorCodes: getApplicableDiagnosticCodes(),
5+
getCodeActions: getDisableJsDiagnosticsCodeActions
6+
});
7+
8+
function getApplicableDiagnosticCodes(): number[] {
9+
const allDiagnostcs = <MapLike<DiagnosticMessage>>Diagnostics;
10+
return Object.keys(allDiagnostcs)
11+
.filter(d => allDiagnostcs[d] && allDiagnostcs[d].category === DiagnosticCategory.Error)
12+
.map(d => allDiagnostcs[d].code);
13+
}
14+
15+
function shouldCheckJsFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) {
16+
return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs;
17+
}
18+
19+
function getSuppressCommentLocationForLocation(sourceFile: SourceFile, position: number, newLineCharacter: string) {
20+
let { line } = getLineAndCharacterOfPosition(sourceFile, position);
21+
const lineStartPosition = getStartPositionOfLine(line, sourceFile);
22+
const startPosition = getFirstNonSpaceCharacterPosition(sourceFile.text, lineStartPosition);
23+
24+
// First try to see if we can put the '// @ts-suppress' on the previous line.
25+
// We need to make sure that we are not in the middle of a string literal or a comment.
26+
// We also want to check if the previous line holds a comment for a node on the next line
27+
// if so, we do not want to separate the node from its comment if we can.
28+
if (!isInComment(sourceFile, startPosition) && !isInString(sourceFile, startPosition) && !isInTemplateString(sourceFile, startPosition)) {
29+
const token = getTouchingToken(sourceFile, startPosition);
30+
const tokenLeadingCommnets = getLeadingCommentRangesOfNode(token, sourceFile);
31+
if (!tokenLeadingCommnets || !tokenLeadingCommnets.length || tokenLeadingCommnets[0].pos >= startPosition) {
32+
return {
33+
span: { start: startPosition, length: 0 },
34+
newText: `// @ts-suppress${newLineCharacter}`
35+
};
36+
}
37+
}
38+
39+
// If all fails, add an extra new line immediatlly before the error span.
40+
return {
41+
span: { start: position, length: 0 },
42+
newText: `${position === startPosition ? "" : newLineCharacter}// @ts-suppress${newLineCharacter}`
43+
};
44+
}
45+
46+
function getDisableJsDiagnosticsCodeActions(context: CodeFixContext): CodeAction[] | undefined {
47+
const { sourceFile, program, newLineCharacter, span } = context;
48+
49+
if (!isInJavaScriptFile(sourceFile) || !shouldCheckJsFile(sourceFile, program.getCompilerOptions())) {
50+
return undefined;
51+
}
52+
53+
return [{
54+
description: getLocaleSpecificMessage(Diagnostics.Suppress_this_error_message),
55+
changes: [{
56+
fileName: sourceFile.fileName,
57+
textChanges: [getSuppressCommentLocationForLocation(sourceFile, span.start, newLineCharacter)]
58+
}]
59+
},
60+
{
61+
description: getLocaleSpecificMessage(Diagnostics.Disable_checking_for_this_file),
62+
changes: [{
63+
fileName: sourceFile.fileName,
64+
textChanges: [{
65+
span: {
66+
start: sourceFile.checkJsDirective ? sourceFile.checkJsDirective.pos : 0,
67+
length: sourceFile.checkJsDirective ? sourceFile.checkJsDirective.end - sourceFile.checkJsDirective.pos : 0
68+
},
69+
newText: `// @ts-nocheck${newLineCharacter}`
70+
}]
71+
}]
72+
}];
73+
}
74+
}
Lines changed: 84 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* @internal */
1+
/* @internal */
22
namespace ts.codefix {
33
registerCodeFix({
44
errorCodes: [Diagnostics.Property_0_does_not_exist_on_type_1.code],
@@ -13,55 +13,106 @@ namespace ts.codefix {
1313
// this.missing = 1;
1414
// ^^^^^^^
1515
const token = getTokenAtPosition(sourceFile, start);
16-
1716
if (token.kind != SyntaxKind.Identifier) {
1817
return undefined;
1918
}
2019

21-
const classDeclaration = getContainingClass(token);
22-
if (!classDeclaration) {
20+
if (!isPropertyAccessExpression(token.parent) || token.parent.expression.kind !== SyntaxKind.ThisKeyword) {
2321
return undefined;
2422
}
2523

26-
if (!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) {
24+
const classMemberDeclaration = getThisContainer(token, /*includeArrowFunctions*/ false);
25+
if (!isClassElement(classMemberDeclaration)) {
2726
return undefined;
2827
}
2928

30-
if ((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) {
29+
const classDeclaration = <ClassLikeDeclaration>classMemberDeclaration.parent;
30+
if (!classDeclaration || !isClassLike(classDeclaration)) {
3131
return undefined;
3232
}
3333

34-
let typeString = "any";
34+
const isStatic = hasModifier(getThisContainer(token, /*includeArrowFunctions*/ false), ModifierFlags.Static);
3535

36-
if (token.parent.parent.kind === SyntaxKind.BinaryExpression) {
37-
const binaryExpression = token.parent.parent as BinaryExpression;
36+
return isInJavaScriptFile(sourceFile) ? getActionsForAddMissingMemberInJavaScriptFile() : getActionsForAddMissingMemberInTypeScriptFile();
3837

39-
const checker = context.program.getTypeChecker();
40-
const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)));
41-
typeString = checker.typeToString(widenedType);
42-
}
38+
function getActionsForAddMissingMemberInTypeScriptFile(): CodeAction[] | undefined {
39+
let typeString = "any";
4340

44-
const startPos = classDeclaration.members.pos;
41+
if (token.parent.parent.kind === SyntaxKind.BinaryExpression) {
42+
const binaryExpression = token.parent.parent as BinaryExpression;
4543

46-
return [{
47-
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]),
48-
changes: [{
49-
fileName: sourceFile.fileName,
50-
textChanges: [{
51-
span: { start: startPos, length: 0 },
52-
newText: `${token.getFullText(sourceFile)}: ${typeString};`
53-
}]
54-
}]
55-
},
56-
{
57-
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_missing_property_0), [token.getText()]),
58-
changes: [{
59-
fileName: sourceFile.fileName,
60-
textChanges: [{
61-
span: { start: startPos, length: 0 },
62-
newText: `[name: string]: ${typeString};`
44+
const checker = context.program.getTypeChecker();
45+
const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)));
46+
typeString = checker.typeToString(widenedType);
47+
}
48+
49+
const startPos = classDeclaration.members.pos;
50+
51+
const actions = [{
52+
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]),
53+
changes: [{
54+
fileName: sourceFile.fileName,
55+
textChanges: [{
56+
span: { start: startPos, length: 0 },
57+
newText: `${isStatic ? "static " : ""}${token.getFullText(sourceFile)}: ${typeString};`
58+
}]
6359
}]
64-
}]
65-
}];
60+
}];
61+
62+
if (!isStatic) {
63+
actions.push({
64+
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_missing_property_0), [token.getText()]),
65+
changes: [{
66+
fileName: sourceFile.fileName,
67+
textChanges: [{
68+
span: { start: startPos, length: 0 },
69+
newText: `[x: string]: ${typeString};`
70+
}]
71+
}]
72+
});
73+
}
74+
75+
return actions;
76+
}
77+
78+
function getActionsForAddMissingMemberInJavaScriptFile(): CodeAction[] | undefined {
79+
const memberName = token.getText();
80+
81+
if (isStatic) {
82+
if (classDeclaration.kind === SyntaxKind.ClassExpression) {
83+
return undefined;
84+
}
85+
86+
const className = classDeclaration.name.getText();
87+
88+
return [{
89+
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Initialize_static_property_0), [memberName]),
90+
changes: [{
91+
fileName: sourceFile.fileName,
92+
textChanges: [{
93+
span: { start: classDeclaration.getEnd(), length: 0 },
94+
newText: `${context.newLineCharacter}${className}.${memberName} = undefined;${context.newLineCharacter}`
95+
}]
96+
}]
97+
}];
98+
}
99+
else {
100+
const classConstructor = getFirstConstructorWithBody(classDeclaration);
101+
if (!classConstructor) {
102+
return undefined;
103+
}
104+
105+
return [{
106+
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Initialize_property_0_in_the_constructor), [memberName]),
107+
changes: [{
108+
fileName: sourceFile.fileName,
109+
textChanges: [{
110+
span: { start: classConstructor.body.getEnd() - 1, length: 0 },
111+
newText: `this.${memberName} = undefined;${context.newLineCharacter}`
112+
}]
113+
}]
114+
}];
115+
}
116+
}
66117
}
67118
}

src/services/codefixes/fixes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
/// <reference path="fixForgottenThisPropertyAccess.ts" />
88
/// <reference path='unusedIdentifierFixes.ts' />
99
/// <reference path='importFixes.ts' />
10+
/// <reference path='disableJsDiagnostics.ts' />
1011
/// <reference path='helpers.ts' />

src/services/codefixes/unusedIdentifierFixes.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ namespace ts.codefix {
105105
else {
106106
// import |d,| * as ns from './file'
107107
const start = importClause.name.getStart();
108-
let end = findFirstNonSpaceCharPosStarting(importClause.name.end);
108+
const text = sourceFile.text;
109+
let end = getFirstNonSpaceCharacterPosition(text, importClause.name.end);
109110
if (sourceFile.text.charCodeAt(end) === CharacterCodes.comma) {
110-
end = findFirstNonSpaceCharPosStarting(end + 1);
111+
end = getFirstNonSpaceCharacterPosition(text, end + 1);
111112
}
112113

113114
return createCodeFix("", start, end - start);
@@ -166,13 +167,6 @@ namespace ts.codefix {
166167
return createCodeFix("", start, end - start);
167168
}
168169

169-
function findFirstNonSpaceCharPosStarting(start: number) {
170-
while (isWhiteSpace(sourceFile.text.charCodeAt(start))) {
171-
start += 1;
172-
}
173-
return start;
174-
}
175-
176170
function createCodeFix(newText: string, start: number, length: number): CodeAction[] {
177171
return [{
178172
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Remove_declaration_for_Colon_0), { 0: token.getText() }),

src/services/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"codefixes/fixes.ts",
9191
"codefixes/helpers.ts",
9292
"codefixes/importFixes.ts",
93-
"codefixes/unusedIdentifierFixes.ts"
93+
"codefixes/unusedIdentifierFixes.ts",
94+
"codefixes/disableJsDiagnostics.ts"
9495
]
9596
}

0 commit comments

Comments
 (0)