Skip to content

Commit 160b667

Browse files
author
Andy
authored
fixUnusedIdentifier: Don't remove parameter in override or non-last parameter in callback (microsoft#24306)
* fixUnusedIdentifier: Don't remove parameter in override or non-last parameter in callback * Only allow removing last parameters; don't care about contextual type
1 parent 816f1ce commit 160b667

15 files changed

+132
-127
lines changed

src/services/codefixes/fixUnusedIdentifier.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ namespace ts.codefix {
1414
registerCodeFix({
1515
errorCodes,
1616
getCodeActions(context) {
17-
const { errorCode, sourceFile } = context;
17+
const { errorCode, sourceFile, program } = context;
18+
const checker = program.getTypeChecker();
1819
const startToken = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false);
1920

2021
const importDecl = tryGetFullImport(startToken);
2122
if (importDecl) {
2223
const changes = textChanges.ChangeTracker.with(context, t => t.deleteNode(sourceFile, importDecl));
2324
return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
2425
}
25-
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, startToken, /*deleted*/ undefined));
26+
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, startToken, /*deleted*/ undefined, checker, /*isFixAll*/ false));
2627
if (delDestructure.length) {
2728
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
2829
}
@@ -34,7 +35,7 @@ namespace ts.codefix {
3435
const token = getToken(sourceFile, textSpanEnd(context.span));
3536
const result: CodeFixAction[] = [];
3637

37-
const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined));
38+
const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined, checker, /*isFixAll*/ false));
3839
if (deletion.length) {
3940
result.push(createCodeFixAction(fixName, deletion, [Diagnostics.Remove_declaration_for_Colon_0, token.getText(sourceFile)], fixIdDelete, Diagnostics.Delete_all_unused_declarations));
4041
}
@@ -50,8 +51,9 @@ namespace ts.codefix {
5051
getAllCodeActions: context => {
5152
// Track a set of deleted nodes that may be ancestors of other marked for deletion -- only delete the ancestors.
5253
const deleted = new NodeSet();
54+
const { sourceFile, program } = context;
55+
const checker = program.getTypeChecker();
5356
return codeFixAll(context, errorCodes, (changes, diag) => {
54-
const { sourceFile } = context;
5557
const startToken = getTokenAtPosition(sourceFile, diag.start, /*includeJsDocComment*/ false);
5658
const token = findPrecedingToken(textSpanEnd(diag), diag.file)!;
5759
switch (context.fixId) {
@@ -68,8 +70,9 @@ namespace ts.codefix {
6870
if (importDecl) {
6971
changes.deleteNode(sourceFile, importDecl);
7072
}
71-
else if (!tryDeleteFullDestructure(changes, sourceFile, startToken, deleted) && !tryDeleteFullVariableStatement(changes, sourceFile, startToken, deleted)) {
72-
tryDeleteDeclaration(changes, sourceFile, token, deleted);
73+
else if (!tryDeleteFullDestructure(changes, sourceFile, startToken, deleted, checker, /*isFixAll*/ true) &&
74+
!tryDeleteFullVariableStatement(changes, sourceFile, startToken, deleted)) {
75+
tryDeleteDeclaration(changes, sourceFile, token, deleted, checker, /*isFixAll*/ true);
7376
}
7477
break;
7578
default:
@@ -84,14 +87,15 @@ namespace ts.codefix {
8487
return startToken.kind === SyntaxKind.ImportKeyword ? tryCast(startToken.parent, isImportDeclaration) : undefined;
8588
}
8689

87-
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, startToken: Node, deletedAncestors: NodeSet | undefined): boolean {
90+
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, startToken: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): boolean {
8891
if (startToken.kind !== SyntaxKind.OpenBraceToken || !isObjectBindingPattern(startToken.parent)) return false;
8992
const decl = cast(startToken.parent, isObjectBindingPattern).parent;
9093
switch (decl.kind) {
9194
case SyntaxKind.VariableDeclaration:
9295
tryDeleteVariableDeclaration(changes, sourceFile, decl, deletedAncestors);
9396
break;
9497
case SyntaxKind.Parameter:
98+
if (!mayDeleteParameter(decl, checker, isFixAll)) break;
9599
if (deletedAncestors) deletedAncestors.add(decl);
96100
changes.deleteNodeInList(sourceFile, decl);
97101
break;
@@ -144,10 +148,10 @@ namespace ts.codefix {
144148
return false;
145149
}
146150

147-
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined): void {
151+
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): void {
148152
switch (token.kind) {
149153
case SyntaxKind.Identifier:
150-
tryDeleteIdentifier(changes, sourceFile, <Identifier>token, deletedAncestors);
154+
tryDeleteIdentifier(changes, sourceFile, <Identifier>token, deletedAncestors, checker, isFixAll);
151155
break;
152156
case SyntaxKind.PropertyDeclaration:
153157
case SyntaxKind.NamespaceImport:
@@ -170,7 +174,7 @@ namespace ts.codefix {
170174
}
171175
}
172176

173-
function tryDeleteIdentifier(changes: textChanges.ChangeTracker, sourceFile: SourceFile, identifier: Identifier, deletedAncestors: NodeSet | undefined): void {
177+
function tryDeleteIdentifier(changes: textChanges.ChangeTracker, sourceFile: SourceFile, identifier: Identifier, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): void {
174178
const parent = identifier.parent;
175179
switch (parent.kind) {
176180
case SyntaxKind.VariableDeclaration:
@@ -194,11 +198,8 @@ namespace ts.codefix {
194198
break;
195199

196200
case SyntaxKind.Parameter:
201+
if (!mayDeleteParameter(parent as ParameterDeclaration, checker, isFixAll)) break;
197202
const oldFunction = parent.parent;
198-
if (isSetAccessor(oldFunction)) {
199-
// Setter must have a parameter
200-
break;
201-
}
202203

203204
if (isArrowFunction(oldFunction) && oldFunction.parameters.length === 1) {
204205
// Lambdas with exactly one parameter are special because, after removal, there
@@ -347,4 +348,35 @@ namespace ts.codefix {
347348
}
348349
}
349350
}
351+
352+
function mayDeleteParameter(p: ParameterDeclaration, checker: TypeChecker, isFixAll: boolean) {
353+
const parent = p.parent;
354+
switch (parent.kind) {
355+
case SyntaxKind.MethodDeclaration:
356+
// Don't remove a parameter if this overrides something
357+
const symbol = checker.getSymbolAtLocation(parent.name)!;
358+
if (isMemberSymbolInBaseType(symbol, checker)) return false;
359+
// falls through
360+
361+
case SyntaxKind.Constructor:
362+
case SyntaxKind.FunctionDeclaration:
363+
case SyntaxKind.FunctionExpression:
364+
case SyntaxKind.ArrowFunction: {
365+
// Can't remove a non-last parameter. Can remove a parameter in code-fix-all if future parameters are also unused.
366+
const { parameters } = parent;
367+
const index = parameters.indexOf(p);
368+
Debug.assert(index !== -1);
369+
return isFixAll
370+
? parameters.slice(index + 1).every(p => p.name.kind === SyntaxKind.Identifier && !p.symbol.isReferenced)
371+
: index === parameters.length - 1;
372+
}
373+
374+
case SyntaxKind.SetAccessor:
375+
// Setter must have a parameter
376+
return false;
377+
378+
default:
379+
return Debug.failBadSyntaxKind(parent);
380+
}
381+
}
350382
}

src/services/findAllReferences.ts

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

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

src/services/utilities.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,38 @@ namespace ts {
12931293
return propSymbol;
12941294
}
12951295

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

tests/cases/fourslash/codeFixUnusedIdentifier_all_delete.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@
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; });
24+
////
1025
////{
1126
//// let a, b;
1227
////}
@@ -19,6 +34,21 @@ verify.codeFixAll({
1934
`function f() {
2035
}
2136
function g(a) { return a; }
37+
38+
interface I {
39+
m(x: number): void;
40+
}
41+
42+
class C implements I {
43+
m(x: number): void {} // Does not remove 'x', which is inherited
44+
n(): void {}
45+
}
46+
47+
declare function f(cb: (x: number, y: string) => void): void;
48+
f(() => {});
49+
f((x) => { x; });
50+
f((x, y) => { y; });
51+
2252
{
2353
}
2454
for (; ;) {}`,

tests/cases/fourslash/codeFixUnusedIdentifier_destructure_allUnused_all.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
////const { x, y } = o;
77
////const { a, b } = o;
88
////a;
9-
////export function f({ x, y }, { a, b }) {
9+
////export function f({ a, b }, { x, y }) {
1010
//// a;
1111
////}
1212

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" }]);

tests/cases/fourslash/unusedParameterInConstructor1.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

tests/cases/fourslash/unusedParameterInConstructor1AddUnderscore.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
verify.codeFix({
99
description: "Prefix 'p1' with an underscore",
10-
index: 1,
1110
newFileContent:
1211
`class C1 {
1312
constructor(_p1: string, public p2: boolean, public p3: any, p5) { p5; }

tests/cases/fourslash/unusedParameterInConstructor2.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

tests/cases/fourslash/unusedParameterInConstructor3.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

tests/cases/fourslash/unusedParameterInConstructor4.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

tests/cases/fourslash/unusedParameterInFunction3.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

tests/cases/fourslash/unusedParameterInFunction4.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

tests/cases/fourslash/unusedParameterInLambda4.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)