Skip to content

Commit 6bc1e70

Browse files
author
Andy Hanson
committed
fixUnusedIdentifier: Remove arguments corresponding to unused parameters
1 parent 657d011 commit 6bc1e70

8 files changed

+114
-27
lines changed

src/services/codeFixProvider.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,13 @@ namespace ts {
7979
return { fileName, textChanges };
8080
}
8181

82-
export function codeFixAll(context: CodeFixAllContext, errorCodes: number[], use: (changes: textChanges.ChangeTracker, error: DiagnosticWithLocation, commands: Push<CodeActionCommand>) => void): CombinedCodeActions {
82+
export function codeFixAll(
83+
context: CodeFixAllContext,
84+
errorCodes: number[],
85+
use: (changes: textChanges.ChangeTracker, error: DiagnosticWithLocation, commands: Push<CodeActionCommand>) => void,
86+
): CombinedCodeActions {
8387
const commands: CodeActionCommand[] = [];
84-
const changes = textChanges.ChangeTracker.with(context, t =>
85-
eachDiagnostic(context, errorCodes, diag => use(t, diag, commands)));
88+
const changes = textChanges.ChangeTracker.with(context, t => eachDiagnostic(context, errorCodes, diag => use(t, diag, commands)));
8689
return createCombinedCodeActions(changes, commands.length === 0 ? undefined : commands);
8790
}
8891

src/services/codefixes/fixUnusedIdentifier.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ namespace ts.codefix {
1616
getCodeActions(context) {
1717
const { errorCode, sourceFile, program } = context;
1818
const checker = program.getTypeChecker();
19+
const sourceFiles = program.getSourceFiles();
1920
const startToken = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false);
2021

2122
const importDecl = tryGetFullImport(startToken);
2223
if (importDecl) {
2324
const changes = textChanges.ChangeTracker.with(context, t => t.deleteNode(sourceFile, importDecl));
2425
return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
2526
}
26-
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, startToken, /*deleted*/ undefined, checker, /*isFixAll*/ false));
27+
const delDestructure = textChanges.ChangeTracker.with(context, t =>
28+
tryDeleteFullDestructure(t, sourceFile, startToken, /*deleted*/ undefined, checker, sourceFiles, /*isFixAll*/ false));
2729
if (delDestructure.length) {
2830
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
2931
}
@@ -35,7 +37,8 @@ namespace ts.codefix {
3537
const token = getToken(sourceFile, textSpanEnd(context.span));
3638
const result: CodeFixAction[] = [];
3739

38-
const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined, checker, /*isFixAll*/ false));
40+
const deletion = textChanges.ChangeTracker.with(context, t =>
41+
tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined, checker, sourceFiles, /*isFixAll*/ false));
3942
if (deletion.length) {
4043
result.push(createCodeFixAction(fixName, deletion, [Diagnostics.Remove_declaration_for_Colon_0, token.getText(sourceFile)], fixIdDelete, Diagnostics.Delete_all_unused_declarations));
4144
}
@@ -50,9 +53,10 @@ namespace ts.codefix {
5053
fixIds: [fixIdPrefix, fixIdDelete],
5154
getAllCodeActions: context => {
5255
// Track a set of deleted nodes that may be ancestors of other marked for deletion -- only delete the ancestors.
53-
const deleted = new NodeSet();
56+
const deletedAncestors = new NodeSet();
5457
const { sourceFile, program } = context;
5558
const checker = program.getTypeChecker();
59+
const sourceFiles = program.getSourceFiles();
5660
return codeFixAll(context, errorCodes, (changes, diag) => {
5761
const startToken = getTokenAtPosition(sourceFile, diag.start, /*includeJsDocComment*/ false);
5862
const token = findPrecedingToken(textSpanEnd(diag), diag.file)!;
@@ -64,15 +68,15 @@ namespace ts.codefix {
6468
break;
6569
case fixIdDelete:
6670
// Ignore if this range was already deleted.
67-
if (deleted.some(d => rangeContainsPosition(d, diag.start))) break;
71+
if (hasDeletedAncestor(deletedAncestors, token)) break;
6872

6973
const importDecl = tryGetFullImport(startToken);
7074
if (importDecl) {
7175
changes.deleteNode(sourceFile, importDecl);
7276
}
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);
77+
else if (!tryDeleteFullDestructure(changes, sourceFile, startToken, deletedAncestors, checker, sourceFiles, /*isFixAll*/ true) &&
78+
!tryDeleteFullVariableStatement(changes, sourceFile, startToken, deletedAncestors)) {
79+
tryDeleteDeclaration(changes, sourceFile, token, deletedAncestors, checker, sourceFiles, /*isFixAll*/ true);
7680
}
7781
break;
7882
default:
@@ -82,12 +86,16 @@ namespace ts.codefix {
8286
},
8387
});
8488

89+
function hasDeletedAncestor(deletedAncestors: ReadonlyNodeSet, node: Node): boolean {
90+
return deletedAncestors.some(d => rangeContainsPosition(d, node.pos));
91+
}
92+
8593
// Sometimes the diagnostic span is an entire ImportDeclaration, so we should remove the whole thing.
8694
function tryGetFullImport(startToken: Node): ImportDeclaration | undefined {
8795
return startToken.kind === SyntaxKind.ImportKeyword ? tryCast(startToken.parent, isImportDeclaration) : undefined;
8896
}
8997

90-
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, startToken: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): boolean {
98+
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, startToken: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, sourceFiles: ReadonlyArray<SourceFile>, isFixAll: boolean): boolean {
9199
if (startToken.kind !== SyntaxKind.OpenBraceToken || !isObjectBindingPattern(startToken.parent)) return false;
92100
const decl = cast(startToken.parent, isObjectBindingPattern).parent;
93101
switch (decl.kind) {
@@ -98,9 +106,10 @@ namespace ts.codefix {
98106
if (!mayDeleteParameter(decl, checker, isFixAll)) break;
99107
if (deletedAncestors) deletedAncestors.add(decl);
100108
changes.deleteNodeInList(sourceFile, decl);
109+
deleteUnusedArguments(changes, deletedAncestors, decl, sourceFiles, checker);
101110
break;
102111
case SyntaxKind.BindingElement:
103-
if (deletedAncestors) deletedAncestors.add(decl);
112+
if (deletedAncestors) deletedAncestors.add(decl);
104113
changes.deleteNode(sourceFile, decl);
105114
break;
106115
default:
@@ -148,8 +157,8 @@ namespace ts.codefix {
148157
return false;
149158
}
150159

151-
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean) {
152-
tryDeleteDeclarationWorker(changes, sourceFile, token, deletedAncestors, checker, isFixAll);
160+
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, sourceFiles: ReadonlyArray<SourceFile>, isFixAll: boolean) {
161+
tryDeleteDeclarationWorker(changes, sourceFile, token, deletedAncestors, checker, sourceFiles, isFixAll);
153162
if (isIdentifier(token)) deleteAssignments(changes, sourceFile, token, checker);
154163
}
155164

@@ -162,7 +171,7 @@ namespace ts.codefix {
162171
});
163172
}
164173

165-
function tryDeleteDeclarationWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): void {
174+
function tryDeleteDeclarationWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, sourceFiles: ReadonlyArray<SourceFile>, isFixAll: boolean): void {
166175
const parent = token.parent;
167176
switch (parent.kind) {
168177
case SyntaxKind.VariableDeclaration:
@@ -212,6 +221,7 @@ namespace ts.codefix {
212221
else {
213222
changes.deleteNodeInList(sourceFile, parent);
214223
}
224+
deleteUnusedArguments(changes, deletedAncestors, parent as ParameterDeclaration, sourceFiles, checker);
215225
break;
216226

217227
case SyntaxKind.BindingElement: {
@@ -341,20 +351,22 @@ namespace ts.codefix {
341351
}
342352
}
343353

344-
function mayDeleteParameter(p: ParameterDeclaration, checker: TypeChecker, isFixAll: boolean) {
354+
function mayDeleteParameter(p: ParameterDeclaration, checker: TypeChecker, isFixAll: boolean): boolean {
345355
const parent = p.parent;
346356
switch (parent.kind) {
347357
case SyntaxKind.MethodDeclaration:
348-
// Don't remove a parameter if this overrides something
358+
// Don't remove a parameter if this overrides something.
349359
const symbol = checker.getSymbolAtLocation(parent.name)!;
350360
if (isMemberSymbolInBaseType(symbol, checker)) return false;
351361
// falls through
352362

353363
case SyntaxKind.Constructor:
354364
case SyntaxKind.FunctionDeclaration:
365+
return true;
366+
355367
case SyntaxKind.FunctionExpression:
356368
case SyntaxKind.ArrowFunction: {
357-
// Can't remove a non-last parameter. Can remove a parameter in code-fix-all if future parameters are also unused.
369+
// Can't remove a non-last parameter in a callback. Can remove a parameter in code-fix-all if future parameters are also unused.
358370
const { parameters } = parent;
359371
const index = parameters.indexOf(p);
360372
Debug.assert(index !== -1);
@@ -371,4 +383,16 @@ namespace ts.codefix {
371383
return Debug.failBadSyntaxKind(parent);
372384
}
373385
}
386+
387+
function deleteUnusedArguments(changes: textChanges.ChangeTracker, deletedAncestors: NodeSet | undefined, deletedParameter: ParameterDeclaration, sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker): void {
388+
FindAllReferences.Core.eachSignatureCall(deletedParameter.parent, sourceFiles, checker, (sourceFile, call) => {
389+
if (deletedAncestors && hasDeletedAncestor(deletedAncestors, call)) return;
390+
const index = deletedParameter.parent.parameters.indexOf(deletedParameter);
391+
if (call.arguments.length > index) { // Just in case the call didn't provide enough arguments.
392+
const argument = call.arguments[index];
393+
changes.deleteNodeInList(sourceFile, argument);
394+
if (deletedAncestors) deletedAncestors.add(argument);
395+
}
396+
});
397+
}
374398
}

src/services/findAllReferences.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,25 @@ namespace ts.FindAllReferences.Core {
730730
}
731731
}
732732

733+
export function eachSignatureCall(signature: SignatureDeclaration, sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker, cb: (sourceFile: SourceFile, call: CallExpression) => void): void {
734+
if (!signature.name || !isIdentifier(signature.name)) return;
735+
736+
const symbol = Debug.assertDefined(checker.getSymbolAtLocation(signature.name));
737+
738+
for (const sourceFile of sourceFiles) {
739+
for (const name of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) {
740+
if (!isIdentifier(name) || name === signature.name || name.escapedText !== signature.name.escapedText) continue;
741+
const called = climbPastPropertyAccess(name);
742+
const call = called.parent;
743+
if (!isCallExpression(call) || call.expression !== called) continue;
744+
const referenceSymbol = checker.getSymbolAtLocation(name);
745+
if (referenceSymbol && checker.getRootSymbols(referenceSymbol).some(s => s === symbol)) {
746+
cb(sourceFile, call);
747+
}
748+
}
749+
}
750+
}
751+
733752
function getPossibleSymbolReferenceNodes(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): ReadonlyArray<Node> {
734753
return getPossibleSymbolReferencePositions(sourceFile, symbolName, container).map(pos => getTouchingPropertyName(sourceFile, pos));
735754
}

src/services/utilities.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1346,7 +1346,13 @@ namespace ts {
13461346
return getPropertySymbolsFromBaseTypes(memberSymbol.parent!, memberSymbol.name, checker, _ => true) || false;
13471347
}
13481348

1349-
export class NodeSet {
1349+
export interface ReadonlyNodeSet {
1350+
has(node: Node): boolean;
1351+
forEach(cb: (node: Node) => void): void;
1352+
some(pred: (node: Node) => boolean): boolean;
1353+
}
1354+
1355+
export class NodeSet implements ReadonlyNodeSet {
13501356
private map = createMap<Node>();
13511357

13521358
add(node: Node): void {

tests/cases/fourslash/codeFixUnusedIdentifier_all_delete.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
//// n(x: number): void {}
1818
////}
1919
////
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; });
20+
////declare function takesCb(cb: (x: number, y: string) => void): void;
21+
////takesCb((x, y) => {});
22+
////takesCb((x, y) => { x; });
23+
////takesCb((x, y) => { y; });
2424
////
2525
////{
2626
//// let a, b;
@@ -44,10 +44,10 @@ class C implements I {
4444
n(): void {}
4545
}
4646
47-
declare function f(cb: (x: number, y: string) => void): void;
48-
f(() => {});
49-
f((x) => { x; });
50-
f((x, y) => { y; });
47+
declare function takesCb(cb: (x: number, y: string) => void): void;
48+
takesCb(() => {});
49+
takesCb((x) => { x; });
50+
takesCb((x, y) => { y; });
5151
5252
{
5353
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @noUnusedLocals: true
4+
// @noUnusedParameters: true
5+
6+
////function f(a, b, { x, y }) { b; }
7+
////f(0, 1, 2);
8+
////
9+
////class C {
10+
//// m(a, b, c) { b; }
11+
////}
12+
////new C().m(0, 1, 2);
13+
14+
verify.codeFixAll({
15+
fixId: "unusedIdentifier_delete",
16+
fixAllDescription: "Delete all unused declarations",
17+
newFileContent:
18+
`function f(b) { b; }
19+
f(1);
20+
21+
class C {
22+
m(b) { b; }
23+
}
24+
new C().m(1);`,
25+
});
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 in a callback
9+
verify.codeFixAvailable([{ description: "Prefix 'x' with an underscore" }]);

tests/cases/fourslash/unusedParameterInConstructor1AddUnderscore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
////}
77

88
verify.codeFix({
9+
index: 1,
910
description: "Prefix 'p1' with an underscore",
1011
newFileContent:
1112
`class C1 {

0 commit comments

Comments
 (0)