Merge pull request #14568 from Microsoft/checkJSFiles_QuickFixes

Allow skipping diagnostics in .js file using comments and quick fixes to add them
This commit is contained in:
Mohamed Hegazy 2017-03-13 23:04:53 -07:00 committed by GitHub
commit 1fbbeadaf7
31 changed files with 595 additions and 49 deletions

View file

@ -945,7 +945,7 @@ task("generate-code-coverage", ["tests", builtLocalDirectory], function () {
// Browser tests
var nodeServerOutFile = "tests/webTestServer.js";
var nodeServerInFile = "tests/webTestServer.ts";
compileFile(nodeServerOutFile, [nodeServerInFile], [builtLocalDirectory, tscFile], [], /*useBuiltCompiler:*/ true, { noOutFile: true });
compileFile(nodeServerOutFile, [nodeServerInFile], [builtLocalDirectory, tscFile], [], /*useBuiltCompiler:*/ true, { noOutFile: true, lib: "es6" });
desc("Runs browserify on run.js to produce a file suitable for running tests in the browser");
task("browserify", ["tests", builtLocalDirectory, nodeServerOutFile], function() {

View file

@ -1,4 +1,4 @@
{
{
"Unterminated string literal.": {
"category": "Error",
"code": 1002
@ -3355,6 +3355,24 @@
"category": "Message",
"code": 90017
},
"Disable checking for this file.": {
"category": "Message",
"code": 90018
},
"Suppress this error message.": {
"category": "Message",
"code": 90019
},
"Initialize property '{0}' in the constructor.": {
"category": "Message",
"code": 90020
},
"Initialize static property '{0}'.": {
"category": "Message",
"code": 90021
},
"Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": {
"category": "Error",
"code": 8017

View file

@ -4,6 +4,7 @@
namespace ts {
const emptyArray: any[] = [];
const suppressDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-suppress)?)/;
export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string {
while (true) {
@ -923,10 +924,36 @@ namespace ts {
const fileProcessingDiagnosticsInFile = fileProcessingDiagnostics.getDiagnostics(sourceFile.fileName);
const programDiagnosticsInFile = programDiagnostics.getDiagnostics(sourceFile.fileName);
return bindDiagnostics.concat(checkDiagnostics, fileProcessingDiagnosticsInFile, programDiagnosticsInFile);
const diagnostics = bindDiagnostics.concat(checkDiagnostics, fileProcessingDiagnosticsInFile, programDiagnosticsInFile);
return isSourceFileJavaScript(sourceFile)
? filter(diagnostics, shouldReportDiagnostic)
: diagnostics;
});
}
/**
* Skip errors if previous line start with '// @ts-suppress' comment, not counting non-empty non-comment lines
*/
function shouldReportDiagnostic(diagnostic: Diagnostic) {
const { file, start } = diagnostic;
const lineStarts = getLineStarts(file);
let { line } = computeLineAndCharacterOfPosition(lineStarts, start);
while (line > 0) {
const previousLineText = file.text.slice(lineStarts[line - 1], lineStarts[line]);
const result = suppressDiagnosticCommentRegEx.exec(previousLineText);
if (!result) {
// non-empty line
return true;
}
if (result[3]) {
// @ts-suppress
return false;
}
line--;
}
return true;
}
function getJavaScriptSyntacticDiagnosticsForFile(sourceFile: SourceFile): Diagnostic[] {
return runWithCancellationToken(() => {
const diagnostics: Diagnostic[] = [];

View file

@ -1,4 +1,4 @@
//
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -2550,6 +2550,11 @@ namespace FourSlash {
}
}
public printAvailableCodeFixes() {
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
Harness.IO.log(stringify(codeFixes));
}
// Get the text of the entire line the caret is currently at
private getCurrentLineContent() {
const text = this.getFileContent(this.activeFile.fileName);
@ -3738,6 +3743,10 @@ namespace FourSlashInterface {
this.state.printCompletionListMembers();
}
public printAvailableCodeFixes() {
this.state.printAvailableCodeFixes();
}
public printBreakpointLocation(pos: number) {
this.state.printBreakpointLocation(pos);
}

View file

@ -1,4 +1,4 @@
{
{
"extends": "../tsconfig-base",
"compilerOptions": {
"removeComments": false,
@ -81,6 +81,7 @@
"../services/codefixes/helpers.ts",
"../services/codefixes/importFixes.ts",
"../services/codefixes/unusedIdentifierFixes.ts",
"../services/codefixes/disableJsDiagnostics.ts",
"harness.ts",
"sourceMapRecorder.ts",

View file

@ -0,0 +1,74 @@
/* @internal */
namespace ts.codefix {
registerCodeFix({
errorCodes: getApplicableDiagnosticCodes(),
getCodeActions: getDisableJsDiagnosticsCodeActions
});
function getApplicableDiagnosticCodes(): number[] {
const allDiagnostcs = <MapLike<DiagnosticMessage>>Diagnostics;
return Object.keys(allDiagnostcs)
.filter(d => allDiagnostcs[d] && allDiagnostcs[d].category === DiagnosticCategory.Error)
.map(d => allDiagnostcs[d].code);
}
function shouldCheckJsFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) {
return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs;
}
function getSuppressCommentLocationForLocation(sourceFile: SourceFile, position: number, newLineCharacter: string) {
let { line } = getLineAndCharacterOfPosition(sourceFile, position);
const lineStartPosition = getStartPositionOfLine(line, sourceFile);
const startPosition = getFirstNonSpaceCharacterPosition(sourceFile.text, lineStartPosition);
// First try to see if we can put the '// @ts-suppress' on the previous line.
// We need to make sure that we are not in the middle of a string literal or a comment.
// We also want to check if the previous line holds a comment for a node on the next line
// if so, we do not want to separate the node from its comment if we can.
if (!isInComment(sourceFile, startPosition) && !isInString(sourceFile, startPosition) && !isInTemplateString(sourceFile, startPosition)) {
const token = getTouchingToken(sourceFile, startPosition);
const tokenLeadingCommnets = getLeadingCommentRangesOfNode(token, sourceFile);
if (!tokenLeadingCommnets || !tokenLeadingCommnets.length || tokenLeadingCommnets[0].pos >= startPosition) {
return {
span: { start: startPosition, length: 0 },
newText: `// @ts-suppress${newLineCharacter}`
};
}
}
// If all fails, add an extra new line immediatlly before the error span.
return {
span: { start: position, length: 0 },
newText: `${position === startPosition ? "" : newLineCharacter}// @ts-suppress${newLineCharacter}`
};
}
function getDisableJsDiagnosticsCodeActions(context: CodeFixContext): CodeAction[] | undefined {
const { sourceFile, program, newLineCharacter, span } = context;
if (!isInJavaScriptFile(sourceFile) || !shouldCheckJsFile(sourceFile, program.getCompilerOptions())) {
return undefined;
}
return [{
description: getLocaleSpecificMessage(Diagnostics.Suppress_this_error_message),
changes: [{
fileName: sourceFile.fileName,
textChanges: [getSuppressCommentLocationForLocation(sourceFile, span.start, newLineCharacter)]
}]
},
{
description: getLocaleSpecificMessage(Diagnostics.Disable_checking_for_this_file),
changes: [{
fileName: sourceFile.fileName,
textChanges: [{
span: {
start: sourceFile.checkJsDirective ? sourceFile.checkJsDirective.pos : 0,
length: sourceFile.checkJsDirective ? sourceFile.checkJsDirective.end - sourceFile.checkJsDirective.pos : 0
},
newText: `// @ts-nocheck${newLineCharacter}`
}]
}]
}];
}
}

View file

@ -1,4 +1,4 @@
/* @internal */
/* @internal */
namespace ts.codefix {
registerCodeFix({
errorCodes: [Diagnostics.Property_0_does_not_exist_on_type_1.code],
@ -13,55 +13,106 @@ namespace ts.codefix {
// this.missing = 1;
// ^^^^^^^
const token = getTokenAtPosition(sourceFile, start);
if (token.kind != SyntaxKind.Identifier) {
return undefined;
}
const classDeclaration = getContainingClass(token);
if (!classDeclaration) {
if (!isPropertyAccessExpression(token.parent) || token.parent.expression.kind !== SyntaxKind.ThisKeyword) {
return undefined;
}
if (!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) {
const classMemberDeclaration = getThisContainer(token, /*includeArrowFunctions*/ false);
if (!isClassElement(classMemberDeclaration)) {
return undefined;
}
if ((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) {
const classDeclaration = <ClassLikeDeclaration>classMemberDeclaration.parent;
if (!classDeclaration || !isClassLike(classDeclaration)) {
return undefined;
}
let typeString = "any";
const isStatic = hasModifier(getThisContainer(token, /*includeArrowFunctions*/ false), ModifierFlags.Static);
if (token.parent.parent.kind === SyntaxKind.BinaryExpression) {
const binaryExpression = token.parent.parent as BinaryExpression;
return isInJavaScriptFile(sourceFile) ? getActionsForAddMissingMemberInJavaScriptFile() : getActionsForAddMissingMemberInTypeScriptFile();
const checker = context.program.getTypeChecker();
const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)));
typeString = checker.typeToString(widenedType);
}
function getActionsForAddMissingMemberInTypeScriptFile(): CodeAction[] | undefined {
let typeString = "any";
const startPos = classDeclaration.members.pos;
if (token.parent.parent.kind === SyntaxKind.BinaryExpression) {
const binaryExpression = token.parent.parent as BinaryExpression;
return [{
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]),
changes: [{
fileName: sourceFile.fileName,
textChanges: [{
span: { start: startPos, length: 0 },
newText: `${token.getFullText(sourceFile)}: ${typeString};`
const checker = context.program.getTypeChecker();
const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)));
typeString = checker.typeToString(widenedType);
}
const startPos = classDeclaration.members.pos;
const actions = [{
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]),
changes: [{
fileName: sourceFile.fileName,
textChanges: [{
span: { start: startPos, length: 0 },
newText: `${isStatic ? "static " : ""}${token.getFullText(sourceFile)}: ${typeString};`
}]
}]
}]
},
{
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_missing_property_0), [token.getText()]),
changes: [{
fileName: sourceFile.fileName,
textChanges: [{
span: { start: startPos, length: 0 },
newText: `[name: string]: ${typeString};`
}]
}]
}];
}];
if (!isStatic) {
actions.push({
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_missing_property_0), [token.getText()]),
changes: [{
fileName: sourceFile.fileName,
textChanges: [{
span: { start: startPos, length: 0 },
newText: `[x: string]: ${typeString};`
}]
}]
});
}
return actions;
}
function getActionsForAddMissingMemberInJavaScriptFile(): CodeAction[] | undefined {
const memberName = token.getText();
if (isStatic) {
if (classDeclaration.kind === SyntaxKind.ClassExpression) {
return undefined;
}
const className = classDeclaration.name.getText();
return [{
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Initialize_static_property_0), [memberName]),
changes: [{
fileName: sourceFile.fileName,
textChanges: [{
span: { start: classDeclaration.getEnd(), length: 0 },
newText: `${context.newLineCharacter}${className}.${memberName} = undefined;${context.newLineCharacter}`
}]
}]
}];
}
else {
const classConstructor = getFirstConstructorWithBody(classDeclaration);
if (!classConstructor) {
return undefined;
}
return [{
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Initialize_property_0_in_the_constructor), [memberName]),
changes: [{
fileName: sourceFile.fileName,
textChanges: [{
span: { start: classConstructor.body.getEnd() - 1, length: 0 },
newText: `this.${memberName} = undefined;${context.newLineCharacter}`
}]
}]
}];
}
}
}
}

View file

@ -7,4 +7,5 @@
/// <reference path="fixForgottenThisPropertyAccess.ts" />
/// <reference path='unusedIdentifierFixes.ts' />
/// <reference path='importFixes.ts' />
/// <reference path='disableJsDiagnostics.ts' />
/// <reference path='helpers.ts' />

View file

@ -105,9 +105,10 @@ namespace ts.codefix {
else {
// import |d,| * as ns from './file'
const start = importClause.name.getStart();
let end = findFirstNonSpaceCharPosStarting(importClause.name.end);
const text = sourceFile.text;
let end = getFirstNonSpaceCharacterPosition(text, importClause.name.end);
if (sourceFile.text.charCodeAt(end) === CharacterCodes.comma) {
end = findFirstNonSpaceCharPosStarting(end + 1);
end = getFirstNonSpaceCharacterPosition(text, end + 1);
}
return createCodeFix("", start, end - start);
@ -166,13 +167,6 @@ namespace ts.codefix {
return createCodeFix("", start, end - start);
}
function findFirstNonSpaceCharPosStarting(start: number) {
while (isWhiteSpace(sourceFile.text.charCodeAt(start))) {
start += 1;
}
return start;
}
function createCodeFix(newText: string, start: number, length: number): CodeAction[] {
return [{
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Remove_declaration_for_Colon_0), { 0: token.getText() }),

View file

@ -90,6 +90,7 @@
"codefixes/fixes.ts",
"codefixes/helpers.ts",
"codefixes/importFixes.ts",
"codefixes/unusedIdentifierFixes.ts"
"codefixes/unusedIdentifierFixes.ts",
"codefixes/disableJsDiagnostics.ts"
]
}

View file

@ -1388,4 +1388,11 @@ namespace ts {
// First token is the open curly, this is where we want to put the 'super' call.
return constructor.body.getFirstToken(sourceFile).getEnd();
}
export function getFirstNonSpaceCharacterPosition(text: string, position: number) {
while (isWhiteSpace(text.charCodeAt(position))) {
position += 1;
}
return position;
}
}

View file

@ -0,0 +1,38 @@
=== tests/cases/compiler/a.js ===
var x = 0;
>x : Symbol(x, Decl(a.js, 1, 3))
/// @ts-suppress
x();
>x : Symbol(x, Decl(a.js, 1, 3))
/// @ts-suppress
x();
>x : Symbol(x, Decl(a.js, 1, 3))
/// @ts-suppress
x(
>x : Symbol(x, Decl(a.js, 1, 3))
2,
3);
// @ts-suppress
// come comment
// some other comment
// @anohter
x();
>x : Symbol(x, Decl(a.js, 1, 3))
// @ts-suppress: no call signature
x();
>x : Symbol(x, Decl(a.js, 1, 3))

View file

@ -0,0 +1,47 @@
=== tests/cases/compiler/a.js ===
var x = 0;
>x : number
>0 : 0
/// @ts-suppress
x();
>x() : any
>x : number
/// @ts-suppress
x();
>x() : any
>x : number
/// @ts-suppress
x(
>x( 2, 3) : any
>x : number
2,
>2 : 2
3);
>3 : 3
// @ts-suppress
// come comment
// some other comment
// @anohter
x();
>x() : any
>x : number
// @ts-suppress: no call signature
x();
>x() : any
>x : number

View file

@ -0,0 +1,33 @@
// @allowJs: true
// @checkJs: true
// @noEmit: true
// @fileName: a.js
var x = 0;
/// @ts-suppress
x();
/// @ts-suppress
x();
/// @ts-suppress
x(
2,
3);
// @ts-suppress
// come comment
// some other comment
// @anohter
x();
// @ts-suppress: no call signature
x();

View file

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
////[|class C {
//// method() {
//// this.foo = 10;
//// }
////}|]
verify.rangeAfterCodeFix(`class C {
foo: number;
method() {
this.foo = 10;
}
}`, /*includeWhiteSpace*/false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
////[|class C {
//// method() {
//// this.foo = 10;
//// }
////}|]
verify.rangeAfterCodeFix(`class C {
[x:string]: number;
method() {
this.foo = 10;
}
}`, /*includeWhiteSpace*/false, /*errorCode*/ undefined, /*index*/ 1);

View file

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
////[|class C {
//// static method() {
//// this.foo = 10;
//// }
////}|]
verify.rangeAfterCodeFix(`class C {
static foo: number;
static method() {
this.foo = 10;
}
}`);

View file

@ -0,0 +1,22 @@
/// <reference path='fourslash.ts' />
// @checkJs: true
// @allowJs: true
// @Filename: a.js
////[|class C {
//// constructor() {
//// }
//// method() {
//// this.foo === 10;
//// }
////}|]
verify.rangeAfterCodeFix(`class C {
constructor() {
this.foo = undefined;
}
method() {
this.foo === 10;
}
}`, /*includeWhiteSpace*/false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />
// @checkJs: true
// @allowJs: true
// @Filename: a.js
////[|class C {
//// static method() {
//// ()=>{ this.foo === 10 };
//// }
////}
////|]
verify.rangeAfterCodeFix(`class C {
static method() {
()=>{ this.foo === 10 };
}
}
C.foo = undefined;`, /*includeWhiteSpace*/false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// @checkJs: true
// @allowJs: true
// @Filename: a.js
////[|class C {
//// constructor() {
//// }
//// prop = ()=>{ this.foo === 10 };
////}|]
verify.rangeAfterCodeFix(`class C {
constructor() {
this.foo = undefined;
}
prop = ()=>{ this.foo === 10 };
}`, /*includeWhiteSpace*/false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @checkJs: true
// @allowJs: true
// @Filename: a.js
////[|class C {
//// static p = ()=>{ this.foo === 10 };
////}
////|]
verify.rangeAfterCodeFix(`class C {
static p = ()=>{ this.foo === 10 };
}
C.foo = undefined;`, /*includeWhiteSpace*/false, /*errorCode*/ undefined, /*index*/ 2);

View file

@ -0,0 +1,11 @@
/// <reference path='fourslash.ts' />
// @allowjs: true
// @noEmit: true
// @Filename: a.js
////[|// @ts-check|]
////var x = "";
////x = 1;
verify.rangeAfterCodeFix("// @ts-nocheck", /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 1);

View file

@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @allowjs: true
// @noEmit: true
// @checkJs: true
// @Filename: a.js
////[|var x = "";
////x = 1;|]
// Disable checking for the whole file
verify.rangeAfterCodeFix(`// @ts-nocheck
var x = "";
x = 1;`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 1);

View file

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
// @allowjs: true
// @noEmit: true
// @checkJs: true
// @Filename: a.js
////[|var x = "";
////x = 1;|]
// Disable checking for next line
verify.rangeAfterCodeFix(`var x = "";
// @ts-suppress
x = 1;`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// @allowjs: true
// @noEmit: true
// @checkJs: true
// @Filename: a.js
////var x = "";
////
////[|"test \
////"; x = 1;|]
// Disable checking for next line
verify.rangeAfterCodeFix(`"test \\
";
// @ts-suppress
x = 1;`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />
// @allowjs: true
// @noEmit: true
// @checkJs: true
// @Filename: a.js
////var x = "";
////
////[|/** comment */
////x = 1;|]
// Disable checking for next line
verify.rangeAfterCodeFix(`/** comment */
// @ts-suppress
x = 1;`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />
// @allowjs: true
// @noEmit: true
// @checkJs: true
// @Filename: a.js
////var x = 0;
////
////function f(_a) {
//// [|f(x());|]
////}
// Disable checking for next line
verify.rangeAfterCodeFix(`// @ts-suppress
f(x());`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />
// @allowjs: true
// @noEmit: true
// @checkJs: true
// @Filename: a.js
////var x = 0;
////
////function f(_a) {
//// [|x();|]
////}
// Disable checking for next line
verify.rangeAfterCodeFix(`// @ts-suppress
x();`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />
// @allowjs: true
// @noEmit: true
// @checkJs: true
// @Filename: a.js
////var x = 0;
////
////function f(_a) {
//// /** comment for f */
//// [|f(x());|]
////}
// Disable checking for next line
verify.rangeAfterCodeFix(`f(
// @ts-suppress
x());`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);

View file

@ -8,7 +8,7 @@
verify.rangeAfterCodeFix(`
class A {
[name: string]: number;
[x: string]: number;
constructor() {
this.x = 10;

View file

@ -296,6 +296,7 @@ declare namespace FourSlashInterface {
printCurrentQuickInfo(): void;
printCurrentSignatureHelp(): void;
printCompletionListMembers(): void;
printAvailableCodeFixes(): void;
printBreakpointLocation(pos: number): void;
printBreakpointAtCurrentLocation(): void;
printNameOrDottedNameSpans(pos: number): void;