Skip to content

Commit 4aadf21

Browse files
author
Andy Hanson
committed
fixUnusedIdentifier: Don't remove parameter in override or non-last parameter in callback
1 parent d2f6f6a commit 4aadf21

File tree

6 files changed

+125
-43
lines changed

6 files changed

+125
-43
lines changed

src/services/codefixes/fixUnusedIdentifier.ts

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,22 @@ namespace ts.codefix {
1313
registerCodeFix({
1414
errorCodes,
1515
getCodeActions(context) {
16-
const { errorCode, sourceFile } = context;
16+
const { errorCode, sourceFile, program } = context;
17+
const checker = program.getTypeChecker();
1718
const importDecl = tryGetFullImport(sourceFile, context.span.start);
1819
if (importDecl) {
1920
const changes = textChanges.ChangeTracker.with(context, t => t.deleteNode(sourceFile, importDecl));
2021
return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
2122
}
22-
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, context.span.start, /*deleted*/ undefined));
23+
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, context.span.start, /*deleted*/ undefined, checker));
2324
if (delDestructure.length) {
2425
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
2526
}
2627

2728
const token = getToken(sourceFile, textSpanEnd(context.span));
2829
const result: CodeFixAction[] = [];
2930

30-
const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined));
31+
const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined, checker));
3132
if (deletion.length) {
3233
result.push(createCodeFixAction(fixName, deletion, [Diagnostics.Remove_declaration_for_Colon_0, token.getText(sourceFile)], fixIdDelete, Diagnostics.Delete_all_unused_declarations));
3334
}
@@ -43,8 +44,9 @@ namespace ts.codefix {
4344
getAllCodeActions: context => {
4445
// Track a set of deleted nodes that may be ancestors of other marked for deletion -- only delete the ancestors.
4546
const deleted = new NodeSet();
47+
const { sourceFile, program } = context;
48+
const checker = program.getTypeChecker();
4649
return codeFixAll(context, errorCodes, (changes, diag) => {
47-
const { sourceFile } = context;
4850
const token = findPrecedingToken(textSpanEnd(diag), diag.file!);
4951
switch (context.fixId) {
5052
case fixIdPrefix:
@@ -61,8 +63,8 @@ namespace ts.codefix {
6163
changes.deleteNode(sourceFile, importDecl);
6264
}
6365
else {
64-
if (!tryDeleteFullDestructure(changes, sourceFile, diag.start!, deleted)) {
65-
tryDeleteDeclaration(changes, sourceFile, token, deleted);
66+
if (!tryDeleteFullDestructure(changes, sourceFile, diag.start!, deleted, checker)) {
67+
tryDeleteDeclaration(changes, sourceFile, token, deleted, checker);
6668
}
6769
}
6870
break;
@@ -79,7 +81,7 @@ namespace ts.codefix {
7981
return startToken.kind === SyntaxKind.ImportKeyword ? tryCast(startToken.parent, isImportDeclaration) : undefined;
8082
}
8183

82-
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number, deletedAncestors: NodeSet | undefined): boolean {
84+
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number, deletedAncestors: NodeSet | undefined, checker: TypeChecker): boolean {
8385
const startToken = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false);
8486
if (startToken.kind !== SyntaxKind.OpenBraceToken || !isObjectBindingPattern(startToken.parent)) return false;
8587
const decl = startToken.parent.parent;
@@ -88,6 +90,7 @@ namespace ts.codefix {
8890
tryDeleteVariableDeclaration(changes, sourceFile, decl, deletedAncestors);
8991
break;
9092
case SyntaxKind.Parameter:
93+
if (!mayDeleteParameter(decl, checker)) break;
9194
if (deletedAncestors) deletedAncestors.add(decl);
9295
changes.deleteNodeInList(sourceFile, decl);
9396
break;
@@ -130,10 +133,10 @@ namespace ts.codefix {
130133
return false;
131134
}
132135

133-
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined): void {
136+
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker): void {
134137
switch (token.kind) {
135138
case SyntaxKind.Identifier:
136-
tryDeleteIdentifier(changes, sourceFile, <Identifier>token, deletedAncestors);
139+
tryDeleteIdentifier(changes, sourceFile, <Identifier>token, deletedAncestors, checker);
137140
break;
138141
case SyntaxKind.PropertyDeclaration:
139142
case SyntaxKind.NamespaceImport:
@@ -156,7 +159,7 @@ namespace ts.codefix {
156159
}
157160
}
158161

159-
function tryDeleteIdentifier(changes: textChanges.ChangeTracker, sourceFile: SourceFile, identifier: Identifier, deletedAncestors: NodeSet | undefined): void {
162+
function tryDeleteIdentifier(changes: textChanges.ChangeTracker, sourceFile: SourceFile, identifier: Identifier, deletedAncestors: NodeSet | undefined, checker: TypeChecker): void {
160163
const parent = identifier.parent;
161164
switch (parent.kind) {
162165
case SyntaxKind.VariableDeclaration:
@@ -180,11 +183,8 @@ namespace ts.codefix {
180183
break;
181184

182185
case SyntaxKind.Parameter:
186+
if (!mayDeleteParameter(parent as ParameterDeclaration, checker)) break;
183187
const oldFunction = parent.parent;
184-
if (isSetAccessor(oldFunction)) {
185-
// Setter must have a parameter
186-
break;
187-
}
188188

189189
if (isArrowFunction(oldFunction) && oldFunction.parameters.length === 1) {
190190
// Lambdas with exactly one parameter are special because, after removal, there
@@ -334,6 +334,33 @@ namespace ts.codefix {
334334
}
335335
}
336336

337+
function mayDeleteParameter(p: ParameterDeclaration, checker: TypeChecker) {
338+
const parent = p.parent;
339+
switch (parent.kind) {
340+
case SyntaxKind.FunctionDeclaration:
341+
return true;
342+
343+
case SyntaxKind.FunctionExpression:
344+
case SyntaxKind.ArrowFunction:
345+
// Can't remove a non-last parameter in a callback. (Can if future parameters are also unused.)
346+
const index = parent.parameters.indexOf(p);
347+
Debug.assert(index !== -1);
348+
return parent.parameters.slice(index).every(p => p.name.kind === SyntaxKind.Identifier && !p.symbol.isReferenced) || !checker.getContextualType(parent);
349+
350+
case SyntaxKind.SetAccessor:
351+
// Setter must have a parameter
352+
return false;
353+
354+
case SyntaxKind.MethodDeclaration:
355+
// Don't remove a parameter if this overrides something
356+
const symbol = checker.getSymbolAtLocation(parent.name);
357+
return !isMemberSymbolInBaseType(symbol, checker);
358+
359+
default:
360+
return Debug.failBadSyntaxKind(parent);
361+
}
362+
}
363+
337364
class NodeSet {
338365
private map = createMap<Node>();
339366

src/services/findAllReferences.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,34 +1398,6 @@ namespace ts.FindAllReferences.Core {
13981398
}
13991399
}
14001400

1401-
/**
1402-
* Find symbol of the given property-name and add the symbol to the given result array
1403-
* @param symbol a symbol to start searching for the given propertyName
1404-
* @param propertyName a name of property to search for
1405-
* @param result an array of symbol of found property symbols
1406-
* @param previousIterationSymbolsCache a cache of symbol from previous iterations of calling this function to prevent infinite revisiting of the same symbol.
1407-
* The value of previousIterationSymbol is undefined when the function is first called.
1408-
*/
1409-
function getPropertySymbolsFromBaseTypes<T>(symbol: Symbol, propertyName: string, checker: TypeChecker, cb: (symbol: Symbol) => T | undefined): T | undefined {
1410-
const seen = createMap<true>();
1411-
return recur(symbol);
1412-
1413-
function recur(symbol: Symbol): T | undefined {
1414-
// Use `addToSeen` to ensure we don't infinitely recurse in this situation:
1415-
// interface C extends C {
1416-
// /*findRef*/propName: string;
1417-
// }
1418-
if (!(symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) || !addToSeen(seen, getSymbolId(symbol))) return;
1419-
1420-
return firstDefined(symbol.declarations, declaration => firstDefined(getAllSuperTypeNodes(declaration), typeReference => {
1421-
const type = checker.getTypeAtLocation(typeReference);
1422-
const propertySymbol = type && type.symbol && checker.getPropertyOfType(type, propertyName);
1423-
// Visit the typeReference as well to see if it directly or indirectly uses that property
1424-
return propertySymbol && (firstDefined(checker.getRootSymbols(propertySymbol), cb) || recur(type.symbol));
1425-
}));
1426-
}
1427-
}
1428-
14291401
function getRelatedSymbol(search: Search, referenceSymbol: Symbol, referenceLocation: Node, state: State): Symbol | undefined {
14301402
const { checker } = state;
14311403
return forEachRelatedSymbol(referenceSymbol, referenceLocation, checker,

src/services/utilities.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,38 @@ namespace ts {
12811281
return propSymbol;
12821282
}
12831283

1284+
/**
1285+
* Find symbol of the given property-name and add the symbol to the given result array
1286+
* @param symbol a symbol to start searching for the given propertyName
1287+
* @param propertyName a name of property to search for
1288+
* @param result an array of symbol of found property symbols
1289+
* @param previousIterationSymbolsCache a cache of symbol from previous iterations of calling this function to prevent infinite revisiting of the same symbol.
1290+
* The value of previousIterationSymbol is undefined when the function is first called.
1291+
*/
1292+
export function getPropertySymbolsFromBaseTypes<T>(symbol: Symbol, propertyName: string, checker: TypeChecker, cb: (symbol: Symbol) => T | undefined): T | undefined {
1293+
const seen = createMap<true>();
1294+
return recur(symbol);
1295+
1296+
function recur(symbol: Symbol): T | undefined {
1297+
// Use `addToSeen` to ensure we don't infinitely recurse in this situation:
1298+
// interface C extends C {
1299+
// /*findRef*/propName: string;
1300+
// }
1301+
if (!(symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) || !addToSeen(seen, getSymbolId(symbol))) return;
1302+
1303+
return firstDefined(symbol.declarations, declaration => firstDefined(getAllSuperTypeNodes(declaration), typeReference => {
1304+
const type = checker.getTypeAtLocation(typeReference);
1305+
const propertySymbol = type && type.symbol && checker.getPropertyOfType(type, propertyName);
1306+
// Visit the typeReference as well to see if it directly or indirectly uses that property
1307+
return propertySymbol && (firstDefined(checker.getRootSymbols(propertySymbol), cb) || recur(type.symbol));
1308+
}));
1309+
}
1310+
}
1311+
1312+
export function isMemberSymbolInBaseType(memberSymbol: Symbol, checker: TypeChecker): boolean {
1313+
return getPropertySymbolsFromBaseTypes(memberSymbol.parent, memberSymbol.name, checker, _ => true) || false;
1314+
}
1315+
12841316
export class NodeSet {
12851317
private map = createMap<Node>();
12861318

tests/cases/fourslash/codeFixUnusedIdentifier_all_delete.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,40 @@
77
//// const x = 0;
88
////}
99
////function g(a, b, c) { return a; }
10+
////
11+
////interface I {
12+
//// m(x: number): void;
13+
////}
14+
////
15+
////class C implements I {
16+
//// m(x: number): void {} // Does not remove 'x', which is inherited
17+
//// n(x: number): void {}
18+
////}
19+
////
20+
////declare function f(cb: (x: number, y: string) => void): void;
21+
////f((x, y) => {});
22+
////f((x, y) => { x; });
23+
////f((x, y) => { y; });
1024

1125
verify.codeFixAll({
1226
fixId: "unusedIdentifier_delete",
1327
fixAllDescription: "Delete all unused declarations",
1428
newFileContent:
1529
`function f() {
1630
}
17-
function g(a) { return a; }`,
31+
function g(a) { return a; }
32+
33+
interface I {
34+
m(x: number): void;
35+
}
36+
37+
class C implements I {
38+
m(x: number): void {} // Does not remove 'x', which is inherited
39+
n(): void {}
40+
}
41+
42+
declare function f(cb: (x: number, y: string) => void): void;
43+
f(() => {});
44+
f((x) => { x; });
45+
f((x, y) => { y; });`,
1846
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @noUnusedParameters: true
4+
5+
////declare function f(cb: (x: number, y: string) => void): void;
6+
////f((x, y) => { y; });
7+
8+
// No codefix to remove a non-last parameter
9+
verify.codeFixAvailable([{ description: "Prefix 'x' with an underscore" }]);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @noUnusedParameters: true
4+
5+
////interface I {
6+
//// m(x: number): void;
7+
////}
8+
////
9+
////class C implements I {
10+
//// m(x: number): void {}
11+
////}
12+
13+
// No codefix to remove the parameter, it's inherited
14+
verify.codeFixAvailable([{ description: "Prefix 'x' with an underscore" }]);

0 commit comments

Comments
 (0)