diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index c3396a70731..16b16732694 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -5079,6 +5079,14 @@ "category": "Message", "code": 95080 }, + "Add 'const' to unresolved variable": { + "category": "Message", + "code": 95081 + }, + "Add 'const' to all unresolved variables": { + "category": "Message", + "code": 95082 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", "code": 18004 diff --git a/src/services/codefixes/addMissingConst.ts b/src/services/codefixes/addMissingConst.ts new file mode 100644 index 00000000000..37739d01eee --- /dev/null +++ b/src/services/codefixes/addMissingConst.ts @@ -0,0 +1,109 @@ +/* @internal */ +namespace ts.codefix { + const fixId = "addMissingConst"; + const errorCodes = [ + Diagnostics.Cannot_find_name_0.code, + Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer.code + ]; + + registerCodeFix({ + errorCodes, + getCodeActions: (context) => { + const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span.start, context.program)); + if (changes.length > 0) { + return [createCodeFixAction(fixId, changes, Diagnostics.Add_const_to_unresolved_variable, fixId, Diagnostics.Add_const_to_all_unresolved_variables)]; + } + }, + fixIds: [fixId], + getAllCodeActions: context => { + const fixedNodes = new NodeSet(); + return codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag.start, context.program, fixedNodes)); + }, + }); + + function makeChange(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number, program: Program, fixedNodes?: NodeSet) { + const token = getTokenAtPosition(sourceFile, pos); + const forInitializer = findAncestor(token, node => + isForInOrOfStatement(node.parent) ? node.parent.initializer === node : + isPossiblyPartOfDestructuring(node) ? false : "quit" + ); + if (forInitializer) return applyChange(changeTracker, forInitializer, sourceFile, fixedNodes); + + const parent = token.parent; + if (isBinaryExpression(parent) && isExpressionStatement(parent.parent)) { + return applyChange(changeTracker, token, sourceFile, fixedNodes); + } + + if (isArrayLiteralExpression(parent)) { + const checker = program.getTypeChecker(); + if (!every(parent.elements, element => arrayElementCouldBeVariableDeclaration(element, checker))) { + return; + } + + return applyChange(changeTracker, parent, sourceFile, fixedNodes); + } + + const commaExpression = findAncestor(token, node => + isExpressionStatement(node.parent) ? true : + isPossiblyPartOfCommaSeperatedInitializer(node) ? false : "quit" + ); + if (commaExpression) { + const checker = program.getTypeChecker(); + if (!expressionCouldBeVariableDeclaration(commaExpression, checker)) { + return; + } + + return applyChange(changeTracker, commaExpression, sourceFile, fixedNodes); + } + } + + function applyChange(changeTracker: textChanges.ChangeTracker, initializer: Node, sourceFile: SourceFile, fixedNodes?: NodeSet) { + if (!fixedNodes || fixedNodes.tryAdd(initializer)) { + changeTracker.insertModifierBefore(sourceFile, SyntaxKind.ConstKeyword, initializer); + } + } + + function isPossiblyPartOfDestructuring(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.ArrayLiteralExpression: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + return true; + default: + return false; + } + } + + function arrayElementCouldBeVariableDeclaration(expression: Expression, checker: TypeChecker): boolean { + const identifier = + isIdentifier(expression) ? expression : + isAssignmentExpression(expression, /*excludeCompoundAssignment*/ true) && isIdentifier(expression.left) ? expression.left : + undefined; + return !!identifier && !checker.getSymbolAtLocation(identifier); + } + + function isPossiblyPartOfCommaSeperatedInitializer(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.BinaryExpression: + case SyntaxKind.CommaToken: + return true; + default: + return false; + } + } + + function expressionCouldBeVariableDeclaration(expression: Node, checker: TypeChecker): boolean { + if (!isBinaryExpression(expression)) { + return false; + } + + if (expression.operatorToken.kind === SyntaxKind.CommaToken) { + return every([expression.left, expression.right], expression => expressionCouldBeVariableDeclaration(expression, checker)); + } + + return isIdentifier(expression.left) && !checker.getSymbolAtLocation(expression.left); + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 681a9fc4b5e..5e6de0f15ca 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -45,6 +45,7 @@ "codeFixProvider.ts", "refactorProvider.ts", "codefixes/addConvertToUnknownForNonOverlappingTypes.ts", + "codefixes/addMissingConst.ts", "codefixes/addMissingInvocationForDecorator.ts", "codefixes/addNameToNamelessParameter.ts", "codefixes/annotateWithTypeFromJSDoc.ts", diff --git a/tests/cases/fourslash/codeFixAddMissingConstInForInLoop1.ts b/tests/cases/fourslash/codeFixAddMissingConstInForInLoop1.ts new file mode 100644 index 00000000000..76b752465a9 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstInForInLoop1.ts @@ -0,0 +1,8 @@ +/// + +////for (x in []) {} + +verify.codeFix({ + description: "Add 'const' to unresolved variable", + newFileContent: "for (const x in []) {}" +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstInForInLoop2.ts b/tests/cases/fourslash/codeFixAddMissingConstInForInLoop2.ts new file mode 100644 index 00000000000..03fb12f5d7e --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstInForInLoop2.ts @@ -0,0 +1,12 @@ +/// + +////for (x in []) {} +////for (y in []) {} + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: +`for (const x in []) {} +for (const y in []) {}` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithArrayDestructuring1.ts b/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithArrayDestructuring1.ts new file mode 100644 index 00000000000..791615f5ea0 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithArrayDestructuring1.ts @@ -0,0 +1,8 @@ +/// + +////for ([x] of [[1,2]]) {} + +verify.codeFix({ + description: "Add 'const' to unresolved variable", + newFileContent: "for (const [x] of [[1,2]]) {}" +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithArrayDestructuring2.ts b/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithArrayDestructuring2.ts new file mode 100644 index 00000000000..c0f8b733aec --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithArrayDestructuring2.ts @@ -0,0 +1,12 @@ +/// + +////for ([x, y] of [[1,2]]) {} +////for ([x] of [[1,2]]) {} + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: +`for (const [x, y] of [[1,2]]) {} +for (const [x] of [[1,2]]) {}` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithObjectDestructuring1.ts b/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithObjectDestructuring1.ts new file mode 100644 index 00000000000..f926091da80 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithObjectDestructuring1.ts @@ -0,0 +1,8 @@ +/// + +////for ({ x } of [{ x: 0 }]) { } + +verify.codeFix({ + description: "Add 'const' to unresolved variable", + newFileContent: "for (const { x } of [{ x: 0 }]) { }" +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithObjectDestructuring2.ts b/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithObjectDestructuring2.ts new file mode 100644 index 00000000000..aa8ec294972 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstInForLoopWithObjectDestructuring2.ts @@ -0,0 +1,12 @@ +/// + +////for ({ x, y } of [{ x: 0, y: 1 }]) { } +////for ({ x } of [{ x: 0 }]) { } + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: +`for (const { x, y } of [{ x: 0, y: 1 }]) { } +for (const { x } of [{ x: 0 }]) { }` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstInForOfLoop1.ts b/tests/cases/fourslash/codeFixAddMissingConstInForOfLoop1.ts new file mode 100644 index 00000000000..49efe2bc9a6 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstInForOfLoop1.ts @@ -0,0 +1,8 @@ +/// + +////for (x of []) {} + +verify.codeFix({ + description: "Add 'const' to unresolved variable", + newFileContent: "for (const x of []) {}" +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstInForOfLoop2.ts b/tests/cases/fourslash/codeFixAddMissingConstInForOfLoop2.ts new file mode 100644 index 00000000000..24f8b48dfc9 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstInForOfLoop2.ts @@ -0,0 +1,12 @@ +/// + +////for (x of []) {} +////for (y of []) {} + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: +`for (const x of []) {} +for (const y of []) {}` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstPreservingIndentation1.ts b/tests/cases/fourslash/codeFixAddMissingConstPreservingIndentation1.ts new file mode 100644 index 00000000000..10818a621bb --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstPreservingIndentation1.ts @@ -0,0 +1,19 @@ +/// + +////a = () => { +//// x = 0; +//// [y] = [1]; +//// weirdlyIndented = 2; +////}; +////b = 3; + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: `const a = () => { + const x = 0; + const [y] = [1]; + const weirdlyIndented = 2; +}; +const b = 3;` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstPreservingIndentation2.ts b/tests/cases/fourslash/codeFixAddMissingConstPreservingIndentation2.ts new file mode 100644 index 00000000000..1411dc357e1 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstPreservingIndentation2.ts @@ -0,0 +1,19 @@ +/// + +////a = () => { +//// for (x in []) { +//// y = 0; +//// } +////}; +////b = 3; + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: `const a = () => { + for (const x in []) { + const y = 0; + } +}; +const b = 3;` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring1.ts b/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring1.ts new file mode 100644 index 00000000000..8e40466c3ba --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring1.ts @@ -0,0 +1,8 @@ +/// + +////[x] = [0]; + +verify.codeFix({ + description: "Add 'const' to unresolved variable", + newFileContent: "const [x] = [0];" +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring2.ts b/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring2.ts new file mode 100644 index 00000000000..57a89076cb2 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring2.ts @@ -0,0 +1,9 @@ +/// + +////[x, y] = [0, 1]; + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: "const [x, y] = [0, 1];" +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring3.ts b/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring3.ts new file mode 100644 index 00000000000..4099e1fe473 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring3.ts @@ -0,0 +1,6 @@ +/// + +////let x: any; +////[x, y] = [0, 1]; + +verify.not.codeFixAvailable(); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring4.ts b/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring4.ts new file mode 100644 index 00000000000..2c077e4c149 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToArrayDestructuring4.ts @@ -0,0 +1,5 @@ +/// + +////[x, y()] = [0, () => 1]; + +verify.not.codeFixAvailable(); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer1.ts b/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer1.ts new file mode 100644 index 00000000000..c5bb309698d --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer1.ts @@ -0,0 +1,9 @@ +/// + +////x = 0, y = 0; + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: "const x = 0, y = 0;" +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer2.ts b/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer2.ts new file mode 100644 index 00000000000..1a60b16489d --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer2.ts @@ -0,0 +1,11 @@ +/// + +////x = 0, +////y = 0; + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: `const x = 0, +y = 0;` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer3.ts b/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer3.ts new file mode 100644 index 00000000000..d39388af335 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer3.ts @@ -0,0 +1,15 @@ +/// + +////function f() { return 42; } +////x = 0, y = f() +//// +////, z = 0; + +verify.codeFixAll({ + fixId: "addMissingConst", + fixAllDescription: "Add 'const' to all unresolved variables", + newFileContent: `function f() { return 42; } +const x = 0, y = f() + +, z = 0;` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer4.ts b/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer4.ts new file mode 100644 index 00000000000..263ed33231a --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToCommaSeparatedInitializer4.ts @@ -0,0 +1,6 @@ +/// + +////let y: any; +////x = 0, y = 0; + +verify.not.codeFixAvailable(); diff --git a/tests/cases/fourslash/codeFixAddMissingConstToStandaloneIdentifier1.ts b/tests/cases/fourslash/codeFixAddMissingConstToStandaloneIdentifier1.ts new file mode 100644 index 00000000000..0a603dced30 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingConstToStandaloneIdentifier1.ts @@ -0,0 +1,8 @@ +/// + +////x = 0; + +verify.codeFix({ + description: "Add 'const' to unresolved variable", + newFileContent: "const x = 0;" +});