Skip to content

Commit 6bda0cd

Browse files
author
Andy Hanson
committed
fixUnusedIdentifier: Handle destructure with all bindings unused
1 parent 5ea4d3b commit 6bda0cd

15 files changed

+316
-80
lines changed

src/compiler/checker.ts

Lines changed: 77 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -22307,10 +22307,6 @@ namespace ts {
2230722307
function checkUnusedIdentifiers(potentiallyUnusedIdentifiers: ReadonlyArray<PotentiallyUnusedIdentifier>, addDiagnostic: AddUnusedDiagnostic) {
2230822308
for (const node of potentiallyUnusedIdentifiers) {
2230922309
switch (node.kind) {
22310-
case SyntaxKind.SourceFile:
22311-
case SyntaxKind.ModuleDeclaration:
22312-
checkUnusedModuleMembers(node, addDiagnostic);
22313-
break;
2231422310
case SyntaxKind.ClassDeclaration:
2231522311
case SyntaxKind.ClassExpression:
2231622312
checkUnusedClassMembers(node, addDiagnostic);
@@ -22319,6 +22315,8 @@ namespace ts {
2231922315
case SyntaxKind.InterfaceDeclaration:
2232022316
checkUnusedTypeParameters(node, addDiagnostic);
2232122317
break;
22318+
case SyntaxKind.SourceFile:
22319+
case SyntaxKind.ModuleDeclaration:
2232222320
case SyntaxKind.Block:
2232322321
case SyntaxKind.CaseBlock:
2232422322
case SyntaxKind.ForStatement:
@@ -22352,35 +22350,6 @@ namespace ts {
2235222350
}
2235322351
}
2235422352

22355-
function checkUnusedLocalsAndParameters(node: Node, addDiagnostic: AddUnusedDiagnostic): void {
22356-
if (!(node.flags & NodeFlags.Ambient)) {
22357-
node.locals.forEach(local => {
22358-
// If it's purely a type parameter, ignore, will be checked in `checkUnusedTypeParameters`.
22359-
// If it's a type parameter merged with a parameter, check if the parameter-side is used.
22360-
if (local.flags & SymbolFlags.TypeParameter ? (local.flags & SymbolFlags.Variable && !(local.isReferenced & SymbolFlags.Variable)) : !local.isReferenced) {
22361-
if (local.valueDeclaration && getRootDeclaration(local.valueDeclaration).kind === SyntaxKind.Parameter) {
22362-
const parameter = <ParameterDeclaration>getRootDeclaration(local.valueDeclaration);
22363-
const name = getNameOfDeclaration(local.valueDeclaration);
22364-
if (!isParameterPropertyDeclaration(parameter) && !parameterIsThisKeyword(parameter) && !parameterNameStartsWithUnderscore(name)) {
22365-
addDiagnostic(UnusedKind.Parameter, createDiagnosticForNode(name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(local)));
22366-
}
22367-
}
22368-
else {
22369-
forEach(local.declarations, d => errorUnusedLocal(d, symbolName(local), addDiagnostic));
22370-
}
22371-
}
22372-
});
22373-
}
22374-
}
22375-
22376-
function isRemovedPropertyFromObjectSpread(node: Node) {
22377-
if (isBindingElement(node) && isObjectBindingPattern(node.parent)) {
22378-
const lastElement = lastOrUndefined(node.parent.elements);
22379-
return lastElement !== node && !!lastElement.dotDotDotToken;
22380-
}
22381-
return false;
22382-
}
22383-
2238422353
function errorUnusedLocal(declaration: Declaration, name: string, addDiagnostic: AddUnusedDiagnostic) {
2238522354
const node = getNameOfDeclaration(declaration) || declaration;
2238622355
if (isIdentifierThatStartsWithUnderScore(node)) {
@@ -22391,10 +22360,8 @@ namespace ts {
2239122360
}
2239222361
}
2239322362

22394-
if (!isRemovedPropertyFromObjectSpread(node.kind === SyntaxKind.Identifier ? node.parent : node)) {
22395-
const message = isTypeDeclaration(declaration) ? Diagnostics._0_is_declared_but_never_used : Diagnostics._0_is_declared_but_its_value_is_never_read;
22396-
addDiagnostic(UnusedKind.Local, createDiagnosticForNodeSpan(getSourceFileOfNode(declaration), declaration, node, message, name));
22397-
}
22363+
const message = isTypeDeclaration(declaration) ? Diagnostics._0_is_declared_but_never_used : Diagnostics._0_is_declared_but_its_value_is_never_read;
22364+
addDiagnostic(UnusedKind.Local, createDiagnosticForNodeSpan(getSourceFileOfNode(declaration), declaration, node, message, name));
2239822365
}
2239922366

2240022367
function parameterNameStartsWithUnderscore(parameterName: DeclarationName) {
@@ -22456,44 +22423,86 @@ namespace ts {
2245622423
}
2245722424
}
2245822425

22459-
function checkUnusedModuleMembers(node: ModuleDeclaration | SourceFile, addDiagnostic: AddUnusedDiagnostic): void {
22460-
if (!(node.flags & NodeFlags.Ambient)) {
22461-
// 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.
22462-
const unusedImports = createMap<[ImportClause, ImportedDeclaration[]]>();
22463-
node.locals.forEach(local => {
22464-
if (local.isReferenced || local.exportSymbol) return;
22465-
for (const declaration of local.declarations) {
22466-
if (isAmbientModule(declaration)) continue;
22467-
if (isImportedDeclaration(declaration)) {
22468-
const importClause = importClauseFromImported(declaration);
22469-
const key = String(getNodeId(importClause));
22470-
const group = unusedImports.get(key);
22471-
if (group) {
22472-
group[1].push(declaration);
22473-
}
22474-
else {
22475-
unusedImports.set(key, [importClause, [declaration]]);
22426+
function addToGroup<K, V>(map: Map<[K, V[]]>, key: K, value: V, getKey: (key: K) => number | string): void {
22427+
const keyString = String(getKey(key));
22428+
const group = map.get(keyString);
22429+
if (group) {
22430+
group[1].push(value);
22431+
}
22432+
else {
22433+
map.set(keyString, [key, [value]]);
22434+
}
22435+
}
22436+
22437+
function tryGetRootParameterDeclaration(node: Node): ParameterDeclaration | undefined {
22438+
return tryCast(getRootDeclaration(node), isParameter);
22439+
}
22440+
22441+
function checkUnusedLocalsAndParameters(nodeWithLocals: Node, addDiagnostic: AddUnusedDiagnostic): void {
22442+
if (nodeWithLocals.flags & NodeFlags.Ambient) return;
22443+
22444+
// 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.
22445+
const unusedImports = createMap<[ImportClause, ImportedDeclaration[]]>();
22446+
const unusedDestructures = createMap<[ObjectBindingPattern, BindingElement[]]>();
22447+
nodeWithLocals.locals.forEach(local => {
22448+
// If it's purely a type parameter, ignore, will be checked in `checkUnusedTypeParameters`.
22449+
// If it's a type parameter merged with a parameter, check if the parameter-side is used.
22450+
if (local.flags & SymbolFlags.TypeParameter ? !(local.flags & SymbolFlags.Variable && !(local.isReferenced & SymbolFlags.Variable)) : local.isReferenced || local.exportSymbol) {
22451+
return;
22452+
}
22453+
22454+
for (const declaration of local.declarations) {
22455+
if (isAmbientModule(declaration)) continue;
22456+
if (isImportedDeclaration(declaration)) {
22457+
addToGroup(unusedImports, importClauseFromImported(declaration), declaration, getNodeId);
22458+
}
22459+
else if (isBindingElement(declaration) && isObjectBindingPattern(declaration.parent)) {
22460+
// In `{ a, ...b }, `a` is considered used since it removes a property from `b`. `b` may still be unused though.
22461+
const lastElement = last(declaration.parent.elements);
22462+
if (declaration === lastElement || !last(declaration.parent.elements).dotDotDotToken) {
22463+
addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId);
22464+
}
22465+
}
22466+
else {
22467+
const parameter = local.valueDeclaration && tryGetRootParameterDeclaration(local.valueDeclaration);
22468+
if (parameter) {
22469+
const name = getNameOfDeclaration(local.valueDeclaration);
22470+
if (!isParameterPropertyDeclaration(parameter) && !parameterIsThisKeyword(parameter) && !parameterNameStartsWithUnderscore(name)) {
22471+
addDiagnostic(UnusedKind.Parameter, createDiagnosticForNode(name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(local)));
2247622472
}
2247722473
}
2247822474
else {
2247922475
errorUnusedLocal(declaration, symbolName(local), addDiagnostic);
2248022476
}
2248122477
}
22482-
});
22483-
22484-
unusedImports.forEach(([importClause, unuseds]) => {
22485-
const importDecl = importClause.parent;
22486-
if (forEachImportedDeclaration(importClause, d => !contains(unuseds, d))) {
22487-
for (const unused of unuseds) errorUnusedLocal(unused, idText(unused.name), addDiagnostic);
22488-
}
22489-
else if (unuseds.length === 1) {
22490-
addDiagnostic(UnusedKind.Local, createDiagnosticForNode(importDecl, Diagnostics._0_is_declared_but_its_value_is_never_read, idText(first(unuseds).name)));
22491-
}
22492-
else {
22493-
addDiagnostic(UnusedKind.Local, createDiagnosticForNode(importDecl, Diagnostics.All_imports_in_import_declaration_are_unused, showModuleSpecifier(importDecl)));
22478+
}
22479+
});
22480+
unusedImports.forEach(([importClause, unuseds]) => {
22481+
const importDecl = importClause.parent;
22482+
if (forEachImportedDeclaration(importClause, d => !contains(unuseds, d))) {
22483+
for (const unused of unuseds) errorUnusedLocal(unused, idText(unused.name), addDiagnostic);
22484+
}
22485+
else if (unuseds.length === 1) {
22486+
addDiagnostic(UnusedKind.Local, createDiagnosticForNode(importDecl, Diagnostics._0_is_declared_but_its_value_is_never_read, idText(first(unuseds).name)));
22487+
}
22488+
else {
22489+
addDiagnostic(UnusedKind.Local, createDiagnosticForNode(importDecl, Diagnostics.All_imports_in_import_declaration_are_unused));
22490+
}
22491+
});
22492+
unusedDestructures.forEach(([bindingPattern, bindingElements]) => {
22493+
const kind = tryGetRootParameterDeclaration(bindingPattern.parent) ? UnusedKind.Parameter : UnusedKind.Local;
22494+
if (!bindingPattern.elements.every(e => contains(bindingElements, e))) {
22495+
for (const e of bindingElements) {
22496+
addDiagnostic(kind, createDiagnosticForNode(e, Diagnostics._0_is_declared_but_its_value_is_never_read, getBindingElementNameText(e)));
2249422497
}
22495-
});
22496-
}
22498+
}
22499+
else if (bindingElements.length === 1) {
22500+
addDiagnostic(kind, createDiagnosticForNode(bindingPattern, Diagnostics._0_is_declared_but_its_value_is_never_read, getBindingElementNameText(first(bindingElements))));
22501+
}
22502+
else {
22503+
addDiagnostic(kind, createDiagnosticForNode(bindingPattern, Diagnostics.All_destructured_elements_are_unused));
22504+
}
22505+
});
2249722506
}
2249822507

2249922508
type ImportedDeclaration = ImportClause | ImportSpecifier | NamespaceImport;

src/compiler/diagnosticMessages.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3543,6 +3543,11 @@
35433543
"code": 6196,
35443544
"reportsUnnecessary": true
35453545
},
3546+
"All destructured elements are unused.": {
3547+
"category": "Error",
3548+
"code": 6197,
3549+
"reportsUnnecessary": true
3550+
},
35463551
"Variable '{0}' implicitly has an '{1}' type.": {
35473552
"category": "Error",
35483553
"code": 7005
@@ -3935,6 +3940,10 @@
39353940
"category": "Message",
39363941
"code": 90008
39373942
},
3943+
"Remove destructuring": {
3944+
"category": "Message",
3945+
"code": 90009
3946+
},
39383947
"Import '{0}' from module \"{1}\"": {
39393948
"category": "Message",
39403949
"code": 90013

src/services/codefixes/fixUnusedIdentifier.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace ts.codefix {
88
Diagnostics._0_is_declared_but_never_used.code,
99
Diagnostics.Property_0_is_declared_but_its_value_is_never_read.code,
1010
Diagnostics.All_imports_in_import_declaration_are_unused.code,
11+
Diagnostics.All_destructured_elements_are_unused.code,
1112
];
1213
registerCodeFix({
1314
errorCodes,
@@ -18,6 +19,10 @@ namespace ts.codefix {
1819
const changes = textChanges.ChangeTracker.with(context, t => t.deleteNode(sourceFile, importDecl));
1920
return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
2021
}
22+
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, context.span.start));
23+
if (delDestructure.length) {
24+
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
25+
}
2126

2227
const token = getToken(sourceFile, textSpanEnd(context.span));
2328
const result: CodeFixAction[] = [];
@@ -50,7 +55,9 @@ namespace ts.codefix {
5055
changes.deleteNode(sourceFile, importDecl);
5156
}
5257
else {
53-
tryDeleteDeclaration(changes, sourceFile, token);
58+
if (!tryDeleteFullDestructure(changes, sourceFile, diag.start!)) {
59+
tryDeleteDeclaration(changes, sourceFile, token);
60+
}
5461
}
5562
break;
5663
default:
@@ -65,6 +72,26 @@ namespace ts.codefix {
6572
return startToken.kind === SyntaxKind.ImportKeyword ? tryCast(startToken.parent, isImportDeclaration) : undefined;
6673
}
6774

75+
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number): boolean {
76+
const startToken = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false);
77+
if (startToken.kind !== SyntaxKind.OpenBraceToken || !isObjectBindingPattern(startToken.parent)) return false;
78+
const decl = startToken.parent.parent;
79+
switch (decl.kind) {
80+
case SyntaxKind.VariableDeclaration:
81+
tryDeleteVariableDeclaration(changes, sourceFile, decl);
82+
break;
83+
case SyntaxKind.Parameter:
84+
changes.deleteNodeInList(sourceFile, decl);
85+
break;
86+
case SyntaxKind.BindingElement:
87+
changes.deleteNode(sourceFile, decl);
88+
break;
89+
default:
90+
return Debug.assertNever(decl);
91+
}
92+
return true;
93+
}
94+
6895
function getToken(sourceFile: SourceFile, pos: number): Node {
6996
const token = findPrecedingToken(pos, sourceFile);
7097
// this handles var ["computed"] = 12;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
tests/cases/compiler/unusedDestructuring.ts(3,7): error TS6197: All destructured elements are unused.
2+
tests/cases/compiler/unusedDestructuring.ts(4,9): error TS6133: 'c' is declared but its value is never read.
3+
tests/cases/compiler/unusedDestructuring.ts(6,7): error TS6133: 'e' is declared but its value is never read.
4+
tests/cases/compiler/unusedDestructuring.ts(8,1): error TS6133: 'f' is declared but its value is never read.
5+
tests/cases/compiler/unusedDestructuring.ts(8,12): error TS6197: All destructured elements are unused.
6+
tests/cases/compiler/unusedDestructuring.ts(8,24): error TS6133: 'c' is declared but its value is never read.
7+
tests/cases/compiler/unusedDestructuring.ts(8,32): error TS6133: 'e' is declared but its value is never read.
8+
9+
10+
==== tests/cases/compiler/unusedDestructuring.ts (7 errors) ====
11+
export {};
12+
declare const o: any;
13+
const { a, b } = o;
14+
~~~~~~~~
15+
!!! error TS6197: All destructured elements are unused.
16+
const { c, d } = o;
17+
~
18+
!!! error TS6133: 'c' is declared but its value is never read.
19+
d;
20+
const { e } = o;
21+
~~~~~
22+
!!! error TS6133: 'e' is declared but its value is never read.
23+
24+
function f({ a, b }, { c, d }, { e }) {
25+
~~~~~~~~~~
26+
!!! error TS6133: 'f' is declared but its value is never read.
27+
~~~~~~~~
28+
!!! error TS6197: All destructured elements are unused.
29+
~
30+
!!! error TS6133: 'c' is declared but its value is never read.
31+
~~~~~
32+
!!! error TS6133: 'e' is declared but its value is never read.
33+
d;
34+
}
35+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//// [unusedDestructuring.ts]
2+
export {};
3+
declare const o: any;
4+
const { a, b } = o;
5+
const { c, d } = o;
6+
d;
7+
const { e } = o;
8+
9+
function f({ a, b }, { c, d }, { e }) {
10+
d;
11+
}
12+
13+
14+
//// [unusedDestructuring.js]
15+
"use strict";
16+
exports.__esModule = true;
17+
var a = o.a, b = o.b;
18+
var c = o.c, d = o.d;
19+
d;
20+
var e = o.e;
21+
function f(_a, _b, _c) {
22+
var a = _a.a, b = _a.b;
23+
var c = _b.c, d = _b.d;
24+
var e = _c.e;
25+
d;
26+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
=== tests/cases/compiler/unusedDestructuring.ts ===
2+
export {};
3+
declare const o: any;
4+
>o : Symbol(o, Decl(unusedDestructuring.ts, 1, 13))
5+
6+
const { a, b } = o;
7+
>a : Symbol(a, Decl(unusedDestructuring.ts, 2, 7))
8+
>b : Symbol(b, Decl(unusedDestructuring.ts, 2, 10))
9+
>o : Symbol(o, Decl(unusedDestructuring.ts, 1, 13))
10+
11+
const { c, d } = o;
12+
>c : Symbol(c, Decl(unusedDestructuring.ts, 3, 7))
13+
>d : Symbol(d, Decl(unusedDestructuring.ts, 3, 10))
14+
>o : Symbol(o, Decl(unusedDestructuring.ts, 1, 13))
15+
16+
d;
17+
>d : Symbol(d, Decl(unusedDestructuring.ts, 3, 10))
18+
19+
const { e } = o;
20+
>e : Symbol(e, Decl(unusedDestructuring.ts, 5, 7))
21+
>o : Symbol(o, Decl(unusedDestructuring.ts, 1, 13))
22+
23+
function f({ a, b }, { c, d }, { e }) {
24+
>f : Symbol(f, Decl(unusedDestructuring.ts, 5, 16))
25+
>a : Symbol(a, Decl(unusedDestructuring.ts, 7, 12))
26+
>b : Symbol(b, Decl(unusedDestructuring.ts, 7, 15))
27+
>c : Symbol(c, Decl(unusedDestructuring.ts, 7, 22))
28+
>d : Symbol(d, Decl(unusedDestructuring.ts, 7, 25))
29+
>e : Symbol(e, Decl(unusedDestructuring.ts, 7, 32))
30+
31+
d;
32+
>d : Symbol(d, Decl(unusedDestructuring.ts, 7, 25))
33+
}
34+

0 commit comments

Comments
 (0)