Remove-unused-identifiers codefix skips assigned identifiers (#41168)

* Remove-all-unused-identifiers skips assigned identifiers

Previously, fixUnusedIdentifier worked the same in fix-all mode as for a
single fix: identifiers with assignments would be deleted:

```ts
function f(a) { }
f(1)
```

becomes

```ts
function f() { }
f()
```

But any kind of argument will be deleted, even one with side effects.
For a single codefix invocation, this is probably OK.
But for fix-all, this could lead to multiple changes
spread throughout a large file.

Now fix-all will only delete parameters and variable declarations with
no assignments:

```ts
function f(a) { }
function g(a) { }
f(1)
g
let x = 1
let y
```

becomes

```
function f(a) { }
function g() { }
f(1)
g
let x = 1
```

* Don't remove assigned parameters for single codefix either

* add optional parameter test case

* Skip initialised params and binding elements

Based on PR feedback from @amcasey

* fixAll removes unused binding patterns completely

* Fixes from comments

Thanks @amcasey for the thorough review

* fix trailing space lint

* correctly remove-all array binding
This commit is contained in:
Nathan Shively-Sanders
2020-11-21 09:57:17 -08:00
committed by GitHub
parent f4157b41db
commit d057f7a992
11 changed files with 165 additions and 83 deletions

View File

@@ -33401,7 +33401,7 @@ namespace ts {
function checkUnusedLocalsAndParameters(nodeWithLocals: Node, addDiagnostic: AddUnusedDiagnostic): void {
// 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 = new Map<string, [ImportClause, ImportedDeclaration[]]>();
const unusedDestructures = new Map<string, [ObjectBindingPattern, BindingElement[]]>();
const unusedDestructures = new Map<string, [BindingPattern, BindingElement[]]>();
const unusedVariables = new Map<string, [VariableDeclarationList, VariableDeclaration[]]>();
nodeWithLocals.locals!.forEach(local => {
// If it's purely a type parameter, ignore, will be checked in `checkUnusedTypeParameters`.
@@ -33433,7 +33433,12 @@ namespace ts {
const name = local.valueDeclaration && getNameOfDeclaration(local.valueDeclaration);
if (parameter && name) {
if (!isParameterPropertyDeclaration(parameter, parameter.parent) && !parameterIsThisKeyword(parameter) && !isIdentifierThatStartsWithUnderscore(name)) {
addDiagnostic(parameter, UnusedKind.Parameter, createDiagnosticForNode(name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(local)));
if (isBindingElement(declaration) && isArrayBindingPattern(declaration.parent)) {
addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId);
}
else {
addDiagnostic(parameter, UnusedKind.Parameter, createDiagnosticForNode(name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(local)));
}
}
}
else {

View File

@@ -42,7 +42,7 @@ namespace ts.codefix {
}
}
if (isObjectBindingPattern(token.parent)) {
if (isObjectBindingPattern(token.parent) || isArrayBindingPattern(token.parent)) {
if (isParameter(token.parent.parent)) {
const elements = token.parent.elements;
const diagnostic: [DiagnosticMessage, string] = [
@@ -51,7 +51,7 @@ namespace ts.codefix {
];
return [
createDeleteFix(textChanges.ChangeTracker.with(context, t =>
deleteDestructuringElements(t, sourceFile, <ObjectBindingPattern>token.parent)), diagnostic)
deleteDestructuringElements(t, sourceFile, token.parent as ObjectBindingPattern | ArrayBindingPattern)), diagnostic)
];
}
return [
@@ -121,13 +121,16 @@ namespace ts.codefix {
deleteTypeParameters(changes, sourceFile, token);
}
else if (isObjectBindingPattern(token.parent)) {
if (isParameter(token.parent.parent)) {
deleteDestructuringElements(changes, sourceFile, token.parent);
if (token.parent.parent.initializer) {
break;
}
else {
else if (!isParameter(token.parent.parent) || isNotProvidedArguments(token.parent.parent, checker, sourceFiles)) {
changes.delete(sourceFile, token.parent.parent);
}
}
else if (isArrayBindingPattern(token.parent.parent) && token.parent.parent.parent.initializer) {
break;
}
else if (canDeleteEntireVariableStatement(sourceFile, token)) {
deleteEntireVariableStatement(changes, sourceFile, <VariableDeclarationList>token.parent);
}
@@ -165,7 +168,7 @@ namespace ts.codefix {
|| token.kind === SyntaxKind.Identifier && (token.parent.kind === SyntaxKind.ImportSpecifier || token.parent.kind === SyntaxKind.ImportClause);
}
// Sometimes the diagnostic span is an entire ImportDeclaration, so we should remove the whole thing.
/** Sometimes the diagnostic span is an entire ImportDeclaration, so we should remove the whole thing. */
function tryGetFullImport(token: Node): ImportDeclaration | undefined {
return token.kind === SyntaxKind.ImportKeyword ? tryCast(token.parent, isImportDeclaration) : undefined;
}
@@ -178,7 +181,7 @@ namespace ts.codefix {
changes.delete(sourceFile, node.parent.kind === SyntaxKind.VariableStatement ? node.parent : node);
}
function deleteDestructuringElements(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: ObjectBindingPattern) {
function deleteDestructuringElements(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: ObjectBindingPattern | ArrayBindingPattern) {
forEach(node.elements, n => changes.delete(sourceFile, n));
}
@@ -219,16 +222,14 @@ namespace ts.codefix {
function tryDeleteDeclaration(sourceFile: SourceFile, token: Node, changes: textChanges.ChangeTracker, checker: TypeChecker, sourceFiles: readonly SourceFile[], program: Program, cancellationToken: CancellationToken, isFixAll: boolean) {
tryDeleteDeclarationWorker(token, changes, sourceFile, checker, sourceFiles, program, cancellationToken, isFixAll);
if (isIdentifier(token)) deleteAssignments(changes, sourceFile, token, checker);
}
function deleteAssignments(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Identifier, checker: TypeChecker) {
FindAllReferences.Core.eachSymbolReferenceInFile(token, checker, sourceFile, (ref: Node) => {
if (isPropertyAccessExpression(ref.parent) && ref.parent.name === ref) ref = ref.parent;
if (isBinaryExpression(ref.parent) && isExpressionStatement(ref.parent.parent) && ref.parent.left === ref) {
changes.delete(sourceFile, ref.parent.parent);
}
});
if (isIdentifier(token)) {
FindAllReferences.Core.eachSymbolReferenceInFile(token, checker, sourceFile, (ref: Node) => {
if (isPropertyAccessExpression(ref.parent) && ref.parent.name === ref) ref = ref.parent;
if (!isFixAll && isBinaryExpression(ref.parent) && isExpressionStatement(ref.parent.parent) && ref.parent.left === ref) {
changes.delete(sourceFile, ref.parent.parent);
}
});
}
}
function tryDeleteDeclarationWorker(token: Node, changes: textChanges.ChangeTracker, sourceFile: SourceFile, checker: TypeChecker, sourceFiles: readonly SourceFile[], program: Program, cancellationToken: CancellationToken, isFixAll: boolean): void {
@@ -236,24 +237,37 @@ namespace ts.codefix {
if (isParameter(parent)) {
tryDeleteParameter(changes, sourceFile, parent, checker, sourceFiles, program, cancellationToken, isFixAll);
}
else {
else if (!isFixAll || !(isIdentifier(token) && FindAllReferences.Core.isSymbolReferencedInFile(token, checker, sourceFile))) {
changes.delete(sourceFile, isImportClause(parent) ? token : isComputedPropertyName(parent) ? parent.parent : parent);
}
}
function tryDeleteParameter(changes: textChanges.ChangeTracker, sourceFile: SourceFile, p: ParameterDeclaration, checker: TypeChecker, sourceFiles: readonly SourceFile[], program: Program, cancellationToken: CancellationToken, isFixAll = false): void {
if (mayDeleteParameter(checker, sourceFile, p, sourceFiles, program, cancellationToken, isFixAll)) {
if (p.modifiers && p.modifiers.length > 0 &&
(!isIdentifier(p.name) || FindAllReferences.Core.isSymbolReferencedInFile(p.name, checker, sourceFile))) {
p.modifiers.forEach(modifier => changes.deleteModifier(sourceFile, modifier));
function tryDeleteParameter(
changes: textChanges.ChangeTracker,
sourceFile: SourceFile,
parameter: ParameterDeclaration,
checker: TypeChecker,
sourceFiles: readonly SourceFile[],
program: Program,
cancellationToken: CancellationToken,
isFixAll = false): void {
if (mayDeleteParameter(checker, sourceFile, parameter, sourceFiles, program, cancellationToken, isFixAll)) {
if (parameter.modifiers && parameter.modifiers.length > 0 &&
(!isIdentifier(parameter.name) || FindAllReferences.Core.isSymbolReferencedInFile(parameter.name, checker, sourceFile))) {
parameter.modifiers.forEach(modifier => changes.deleteModifier(sourceFile, modifier));
}
else {
changes.delete(sourceFile, p);
deleteUnusedArguments(changes, sourceFile, p, sourceFiles, checker);
else if (!parameter.initializer && isNotProvidedArguments(parameter, checker, sourceFiles)) {
changes.delete(sourceFile, parameter);
}
}
}
function isNotProvidedArguments(parameter: ParameterDeclaration, checker: TypeChecker, sourceFiles: readonly SourceFile[]) {
const index = parameter.parent.parameters.indexOf(parameter);
// Just in case the call didn't provide enough arguments.
return !FindAllReferences.Core.someSignatureUsage(parameter.parent, sourceFiles, checker, (_, call) => !call || call.arguments.length > index);
}
function mayDeleteParameter(checker: TypeChecker, sourceFile: SourceFile, parameter: ParameterDeclaration, sourceFiles: readonly SourceFile[], program: Program, cancellationToken: CancellationToken, isFixAll: boolean): boolean {
const { parent } = parameter;
switch (parent.kind) {
@@ -305,15 +319,6 @@ namespace ts.codefix {
}
}
function deleteUnusedArguments(changes: textChanges.ChangeTracker, sourceFile: SourceFile, deletedParameter: ParameterDeclaration, sourceFiles: readonly SourceFile[], checker: TypeChecker): void {
FindAllReferences.Core.eachSignatureCall(deletedParameter.parent, sourceFiles, checker, call => {
const index = deletedParameter.parent.parameters.indexOf(deletedParameter);
if (call.arguments.length > index) { // Just in case the call didn't provide enough arguments.
changes.delete(sourceFile, call.arguments[index]);
}
});
}
function isCallbackLike(checker: TypeChecker, sourceFile: SourceFile, name: Identifier): boolean {
return !!FindAllReferences.Core.eachSymbolReferenceInFile(name, checker, sourceFile, reference =>
isIdentifier(reference) && isCallExpression(reference.parent) && reference.parent.arguments.indexOf(reference) >= 0);

View File

@@ -1240,8 +1240,13 @@ namespace ts.FindAllReferences {
}
}
export function eachSignatureCall(signature: SignatureDeclaration, sourceFiles: readonly SourceFile[], checker: TypeChecker, cb: (call: CallExpression) => void): void {
if (!signature.name || !isIdentifier(signature.name)) return;
export function someSignatureUsage(
signature: SignatureDeclaration,
sourceFiles: readonly SourceFile[],
checker: TypeChecker,
cb: (name: Identifier, call?: CallExpression) => boolean
): boolean {
if (!signature.name || !isIdentifier(signature.name)) return false;
const symbol = Debug.checkDefined(checker.getSymbolAtLocation(signature.name));
@@ -1249,14 +1254,16 @@ namespace ts.FindAllReferences {
for (const name of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) {
if (!isIdentifier(name) || name === signature.name || name.escapedText !== signature.name.escapedText) continue;
const called = climbPastPropertyAccess(name);
const call = called.parent;
if (!isCallExpression(call) || call.expression !== called) continue;
const call = isCallExpression(called.parent) && called.parent.expression === called ? called.parent : undefined;
const referenceSymbol = checker.getSymbolAtLocation(name);
if (referenceSymbol && checker.getRootSymbols(referenceSymbol).some(s => s === symbol)) {
cb(call);
if (cb(name, call)) {
return true;
}
}
}
}
return false;
}
function getPossibleSymbolReferenceNodes(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): readonly Node[] {