Skip to content

fixUnusedIdentifier: Handle destructure with all bindings unused #23805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
5 commits merged into from
May 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 77 additions & 68 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22352,10 +22352,6 @@ namespace ts {
function checkUnusedIdentifiers(potentiallyUnusedIdentifiers: ReadonlyArray<PotentiallyUnusedIdentifier>, addDiagnostic: AddUnusedDiagnostic) {
for (const node of potentiallyUnusedIdentifiers) {
switch (node.kind) {
case SyntaxKind.SourceFile:
case SyntaxKind.ModuleDeclaration:
checkUnusedModuleMembers(node, addDiagnostic);
break;
case SyntaxKind.ClassDeclaration:
case SyntaxKind.ClassExpression:
checkUnusedClassMembers(node, addDiagnostic);
Expand All @@ -22364,6 +22360,8 @@ namespace ts {
case SyntaxKind.InterfaceDeclaration:
checkUnusedTypeParameters(node, addDiagnostic);
break;
case SyntaxKind.SourceFile:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but even with this, we still have a comma probelm.. like:

declare var o: any;

var { x, y } = o;
y;

var { a, b } = o;
a;

still convert to:

declare var o: any;

var {, y } = o;
y;


var { a, } = o;
a;

The first one is a syntax error. the second is not what a human would have written either.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so were you intending this change to address that, or this will be in a separate change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and if this is handled with a different change, then do we still need this one?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, even if we correctly delete every element from the list, that's different from deleting the entire list.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is another issue.. deleting the entire list may not be correct in the general case.. for instance var { x } = foo() if foo had side effects, then deleting it would be wrong..

One option is to convert it to var {} = foo() which is what i was thinking about when i asked about commas earlier. but i do agree that this solution would be ugly.

Alternatively, we gonna just make it foo(), and if the expression was a single identifier or a property access we can remove it. We have in the checker a function isSideEffectFree, we could leverage this to decide whether to delete the whole statement or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another question, how does this apply to functions. I see in this change you are only handling variable statements..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for instance var { x } = foo() if foo had side effects, then deleting it would be wrong..

But that's exactly what we do for var x = foo()... I think it's better to assume that any side effects are unwanted, since the function was being called for a result before and not merely for a side effect -- the situation is similar to #17624.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mind filing a bug for removing nodes with possible side-effects

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case SyntaxKind.ModuleDeclaration:
case SyntaxKind.Block:
case SyntaxKind.CaseBlock:
case SyntaxKind.ForStatement:
Expand Down Expand Up @@ -22397,35 +22395,6 @@ namespace ts {
}
}

function checkUnusedLocalsAndParameters(node: Node, addDiagnostic: AddUnusedDiagnostic): void {
if (!(node.flags & NodeFlags.Ambient)) {
node.locals.forEach(local => {
// If it's purely a type parameter, ignore, will be checked in `checkUnusedTypeParameters`.
// If it's a type parameter merged with a parameter, check if the parameter-side is used.
if (local.flags & SymbolFlags.TypeParameter ? (local.flags & SymbolFlags.Variable && !(local.isReferenced & SymbolFlags.Variable)) : !local.isReferenced) {
if (local.valueDeclaration && getRootDeclaration(local.valueDeclaration).kind === SyntaxKind.Parameter) {
const parameter = <ParameterDeclaration>getRootDeclaration(local.valueDeclaration);
const name = getNameOfDeclaration(local.valueDeclaration);
if (!isParameterPropertyDeclaration(parameter) && !parameterIsThisKeyword(parameter) && !parameterNameStartsWithUnderscore(name)) {
addDiagnostic(UnusedKind.Parameter, createDiagnosticForNode(name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(local)));
}
}
else {
forEach(local.declarations, d => errorUnusedLocal(d, symbolName(local), addDiagnostic));
}
}
});
}
}

function isRemovedPropertyFromObjectSpread(node: Node) {
if (isBindingElement(node) && isObjectBindingPattern(node.parent)) {
const lastElement = lastOrUndefined(node.parent.elements);
return lastElement !== node && !!lastElement.dotDotDotToken;
}
return false;
}

function errorUnusedLocal(declaration: Declaration, name: string, addDiagnostic: AddUnusedDiagnostic) {
const node = getNameOfDeclaration(declaration) || declaration;
if (isIdentifierThatStartsWithUnderScore(node)) {
Expand All @@ -22436,10 +22405,8 @@ namespace ts {
}
}

if (!isRemovedPropertyFromObjectSpread(node.kind === SyntaxKind.Identifier ? node.parent : node)) {
const message = isTypeDeclaration(declaration) ? Diagnostics._0_is_declared_but_never_used : Diagnostics._0_is_declared_but_its_value_is_never_read;
addDiagnostic(UnusedKind.Local, createDiagnosticForNodeSpan(getSourceFileOfNode(declaration), declaration, node, message, name));
}
const message = isTypeDeclaration(declaration) ? Diagnostics._0_is_declared_but_never_used : Diagnostics._0_is_declared_but_its_value_is_never_read;
addDiagnostic(UnusedKind.Local, createDiagnosticForNodeSpan(getSourceFileOfNode(declaration), declaration, node, message, name));
}

function parameterNameStartsWithUnderscore(parameterName: DeclarationName) {
Expand Down Expand Up @@ -22501,44 +22468,86 @@ namespace ts {
}
}

function checkUnusedModuleMembers(node: ModuleDeclaration | SourceFile, addDiagnostic: AddUnusedDiagnostic): void {
if (!(node.flags & NodeFlags.Ambient)) {
// Ideally we could use the ImportClause directly as a key, but must wait until we have full ES6 maps. So must store key along with value.
const unusedImports = createMap<[ImportClause, ImportedDeclaration[]]>();
node.locals.forEach(local => {
if (local.isReferenced || local.exportSymbol) return;
for (const declaration of local.declarations) {
if (isAmbientModule(declaration)) continue;
if (isImportedDeclaration(declaration)) {
const importClause = importClauseFromImported(declaration);
const key = String(getNodeId(importClause));
const group = unusedImports.get(key);
if (group) {
group[1].push(declaration);
}
else {
unusedImports.set(key, [importClause, [declaration]]);
function addToGroup<K, V>(map: Map<[K, V[]]>, key: K, value: V, getKey: (key: K) => number | string): void {
const keyString = String(getKey(key));
const group = map.get(keyString);
if (group) {
group[1].push(value);
}
else {
map.set(keyString, [key, [value]]);
}
}

function tryGetRootParameterDeclaration(node: Node): ParameterDeclaration | undefined {
return tryCast(getRootDeclaration(node), isParameter);
}

function checkUnusedLocalsAndParameters(nodeWithLocals: Node, addDiagnostic: AddUnusedDiagnostic): void {
if (nodeWithLocals.flags & NodeFlags.Ambient) return;

// Ideally we could use the ImportClause directly as a key, but must wait until we have full ES6 maps. So must store key along with value.
const unusedImports = createMap<[ImportClause, ImportedDeclaration[]]>();
const unusedDestructures = createMap<[ObjectBindingPattern, BindingElement[]]>();
nodeWithLocals.locals.forEach(local => {
// If it's purely a type parameter, ignore, will be checked in `checkUnusedTypeParameters`.
// If it's a type parameter merged with a parameter, check if the parameter-side is used.
if (local.flags & SymbolFlags.TypeParameter ? !(local.flags & SymbolFlags.Variable && !(local.isReferenced & SymbolFlags.Variable)) : local.isReferenced || local.exportSymbol) {
return;
}

for (const declaration of local.declarations) {
if (isAmbientModule(declaration)) continue;
if (isImportedDeclaration(declaration)) {
addToGroup(unusedImports, importClauseFromImported(declaration), declaration, getNodeId);
}
else if (isBindingElement(declaration) && isObjectBindingPattern(declaration.parent)) {
// In `{ a, ...b }, `a` is considered used since it removes a property from `b`. `b` may still be unused though.
const lastElement = last(declaration.parent.elements);
if (declaration === lastElement || !last(declaration.parent.elements).dotDotDotToken) {
addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId);
}
}
else {
const parameter = local.valueDeclaration && tryGetRootParameterDeclaration(local.valueDeclaration);
if (parameter) {
const name = getNameOfDeclaration(local.valueDeclaration);
if (!isParameterPropertyDeclaration(parameter) && !parameterIsThisKeyword(parameter) && !parameterNameStartsWithUnderscore(name)) {
addDiagnostic(UnusedKind.Parameter, createDiagnosticForNode(name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(local)));
}
}
else {
errorUnusedLocal(declaration, symbolName(local), addDiagnostic);
}
}
});

unusedImports.forEach(([importClause, unuseds]) => {
const importDecl = importClause.parent;
if (forEachImportedDeclaration(importClause, d => !contains(unuseds, d))) {
for (const unused of unuseds) errorUnusedLocal(unused, idText(unused.name), addDiagnostic);
}
else if (unuseds.length === 1) {
addDiagnostic(UnusedKind.Local, createDiagnosticForNode(importDecl, Diagnostics._0_is_declared_but_its_value_is_never_read, idText(first(unuseds).name)));
}
else {
addDiagnostic(UnusedKind.Local, createDiagnosticForNode(importDecl, Diagnostics.All_imports_in_import_declaration_are_unused, showModuleSpecifier(importDecl)));
}
});
unusedImports.forEach(([importClause, unuseds]) => {
const importDecl = importClause.parent;
if (forEachImportedDeclaration(importClause, d => !contains(unuseds, d))) {
for (const unused of unuseds) errorUnusedLocal(unused, idText(unused.name), addDiagnostic);
}
else if (unuseds.length === 1) {
addDiagnostic(UnusedKind.Local, createDiagnosticForNode(importDecl, Diagnostics._0_is_declared_but_its_value_is_never_read, idText(first(unuseds).name)));
}
else {
addDiagnostic(UnusedKind.Local, createDiagnosticForNode(importDecl, Diagnostics.All_imports_in_import_declaration_are_unused));
}
});
unusedDestructures.forEach(([bindingPattern, bindingElements]) => {
const kind = tryGetRootParameterDeclaration(bindingPattern.parent) ? UnusedKind.Parameter : UnusedKind.Local;
if (!bindingPattern.elements.every(e => contains(bindingElements, e))) {
for (const e of bindingElements) {
addDiagnostic(kind, createDiagnosticForNode(e, Diagnostics._0_is_declared_but_its_value_is_never_read, getBindingElementNameText(e)));
}
});
}
}
else if (bindingElements.length === 1) {
addDiagnostic(kind, createDiagnosticForNode(bindingPattern, Diagnostics._0_is_declared_but_its_value_is_never_read, getBindingElementNameText(first(bindingElements))));
}
else {
addDiagnostic(kind, createDiagnosticForNode(bindingPattern, Diagnostics.All_destructured_elements_are_unused));
}
});
}

type ImportedDeclaration = ImportClause | ImportSpecifier | NamespaceImport;
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3551,6 +3551,11 @@
"category": "Message",
"code": 6197
},
"All destructured elements are unused.": {
"category": "Error",
"code": 6198,
"reportsUnnecessary": true
},

"Projects to reference": {
"category": "Message",
Expand Down Expand Up @@ -3985,6 +3990,10 @@
"category": "Message",
"code": 90008
},
"Remove destructuring": {
"category": "Message",
"code": 90009
},
"Import '{0}' from module \"{1}\"": {
"category": "Message",
"code": 90013
Expand Down
29 changes: 28 additions & 1 deletion src/services/codefixes/fixUnusedIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace ts.codefix {
Diagnostics._0_is_declared_but_never_used.code,
Diagnostics.Property_0_is_declared_but_its_value_is_never_read.code,
Diagnostics.All_imports_in_import_declaration_are_unused.code,
Diagnostics.All_destructured_elements_are_unused.code,
];
registerCodeFix({
errorCodes,
Expand All @@ -18,6 +19,10 @@ namespace ts.codefix {
const changes = textChanges.ChangeTracker.with(context, t => t.deleteNode(sourceFile, importDecl));
return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
}
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, context.span.start));
if (delDestructure.length) {
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
}

const token = getToken(sourceFile, textSpanEnd(context.span));
const result: CodeFixAction[] = [];
Expand Down Expand Up @@ -50,7 +55,9 @@ namespace ts.codefix {
changes.deleteNode(sourceFile, importDecl);
}
else {
tryDeleteDeclaration(changes, sourceFile, token);
if (!tryDeleteFullDestructure(changes, sourceFile, diag.start!)) {
tryDeleteDeclaration(changes, sourceFile, token);
}
}
break;
default:
Expand All @@ -65,6 +72,26 @@ namespace ts.codefix {
return startToken.kind === SyntaxKind.ImportKeyword ? tryCast(startToken.parent, isImportDeclaration) : undefined;
}

function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a test for for(var {} of o) ;

const startToken = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false);
if (startToken.kind !== SyntaxKind.OpenBraceToken || !isObjectBindingPattern(startToken.parent)) return false;
const decl = startToken.parent.parent;
switch (decl.kind) {
case SyntaxKind.VariableDeclaration:
tryDeleteVariableDeclaration(changes, sourceFile, decl);
break;
case SyntaxKind.Parameter:
changes.deleteNodeInList(sourceFile, decl);
break;
case SyntaxKind.BindingElement:
changes.deleteNode(sourceFile, decl);
break;
default:
return Debug.assertNever(decl);
}
return true;
}

function getToken(sourceFile: SourceFile, pos: number): Node {
const token = findPrecedingToken(pos, sourceFile);
// this handles var ["computed"] = 12;
Expand Down
35 changes: 35 additions & 0 deletions tests/baselines/reference/unusedDestructuring.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
tests/cases/compiler/unusedDestructuring.ts(3,7): error TS6198: All destructured elements are unused.
tests/cases/compiler/unusedDestructuring.ts(4,9): error TS6133: 'c' is declared but its value is never read.
tests/cases/compiler/unusedDestructuring.ts(6,7): error TS6133: 'e' is declared but its value is never read.
tests/cases/compiler/unusedDestructuring.ts(8,1): error TS6133: 'f' is declared but its value is never read.
tests/cases/compiler/unusedDestructuring.ts(8,12): error TS6198: All destructured elements are unused.
tests/cases/compiler/unusedDestructuring.ts(8,24): error TS6133: 'c' is declared but its value is never read.
tests/cases/compiler/unusedDestructuring.ts(8,32): error TS6133: 'e' is declared but its value is never read.


==== tests/cases/compiler/unusedDestructuring.ts (7 errors) ====
export {};
declare const o: any;
const { a, b } = o;
~~~~~~~~
!!! error TS6198: All destructured elements are unused.
const { c, d } = o;
~
!!! error TS6133: 'c' is declared but its value is never read.
d;
const { e } = o;
~~~~~
!!! error TS6133: 'e' is declared but its value is never read.

function f({ a, b }, { c, d }, { e }) {
~~~~~~~~~~
!!! error TS6133: 'f' is declared but its value is never read.
~~~~~~~~
!!! error TS6198: All destructured elements are unused.
~
!!! error TS6133: 'c' is declared but its value is never read.
~~~~~
!!! error TS6133: 'e' is declared but its value is never read.
d;
}

26 changes: 26 additions & 0 deletions tests/baselines/reference/unusedDestructuring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//// [unusedDestructuring.ts]
export {};
declare const o: any;
const { a, b } = o;
const { c, d } = o;
d;
const { e } = o;

function f({ a, b }, { c, d }, { e }) {
d;
}


//// [unusedDestructuring.js]
"use strict";
exports.__esModule = true;
var a = o.a, b = o.b;
var c = o.c, d = o.d;
d;
var e = o.e;
function f(_a, _b, _c) {
var a = _a.a, b = _a.b;
var c = _b.c, d = _b.d;
var e = _c.e;
d;
}
34 changes: 34 additions & 0 deletions tests/baselines/reference/unusedDestructuring.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
=== tests/cases/compiler/unusedDestructuring.ts ===
export {};
declare const o: any;
>o : Symbol(o, Decl(unusedDestructuring.ts, 1, 13))

const { a, b } = o;
>a : Symbol(a, Decl(unusedDestructuring.ts, 2, 7))
>b : Symbol(b, Decl(unusedDestructuring.ts, 2, 10))
>o : Symbol(o, Decl(unusedDestructuring.ts, 1, 13))

const { c, d } = o;
>c : Symbol(c, Decl(unusedDestructuring.ts, 3, 7))
>d : Symbol(d, Decl(unusedDestructuring.ts, 3, 10))
>o : Symbol(o, Decl(unusedDestructuring.ts, 1, 13))

d;
>d : Symbol(d, Decl(unusedDestructuring.ts, 3, 10))

const { e } = o;
>e : Symbol(e, Decl(unusedDestructuring.ts, 5, 7))
>o : Symbol(o, Decl(unusedDestructuring.ts, 1, 13))

function f({ a, b }, { c, d }, { e }) {
>f : Symbol(f, Decl(unusedDestructuring.ts, 5, 16))
>a : Symbol(a, Decl(unusedDestructuring.ts, 7, 12))
>b : Symbol(b, Decl(unusedDestructuring.ts, 7, 15))
>c : Symbol(c, Decl(unusedDestructuring.ts, 7, 22))
>d : Symbol(d, Decl(unusedDestructuring.ts, 7, 25))
>e : Symbol(e, Decl(unusedDestructuring.ts, 7, 32))

d;
>d : Symbol(d, Decl(unusedDestructuring.ts, 7, 25))
}

Loading