fixUnusedIdentifier: Don't remove parameter in override or non-last parameter in callback (#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
This commit is contained in:
Andy
2018-05-29 12:39:45 -07:00
committed by GitHub
parent 816f1cee5a
commit 160b667846
15 changed files with 132 additions and 127 deletions
+46 -14
View File
@@ -14,7 +14,8 @@ namespace ts.codefix {
registerCodeFix({
errorCodes,
getCodeActions(context) {
const { errorCode, sourceFile } = context;
const { errorCode, sourceFile, program } = context;
const checker = program.getTypeChecker();
const startToken = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false);
const importDecl = tryGetFullImport(startToken);
@@ -22,7 +23,7 @@ namespace ts.codefix {
const changes = textChanges.ChangeTracker.with(context, t => t.deleteNode(sourceFile, importDecl));
return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
}
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, startToken, /*deleted*/ undefined));
const delDestructure = textChanges.ChangeTracker.with(context, t => tryDeleteFullDestructure(t, sourceFile, startToken, /*deleted*/ undefined, checker, /*isFixAll*/ false));
if (delDestructure.length) {
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
}
@@ -34,7 +35,7 @@ namespace ts.codefix {
const token = getToken(sourceFile, textSpanEnd(context.span));
const result: CodeFixAction[] = [];
const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined));
const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined, checker, /*isFixAll*/ false));
if (deletion.length) {
result.push(createCodeFixAction(fixName, deletion, [Diagnostics.Remove_declaration_for_Colon_0, token.getText(sourceFile)], fixIdDelete, Diagnostics.Delete_all_unused_declarations));
}
@@ -50,8 +51,9 @@ namespace ts.codefix {
getAllCodeActions: context => {
// Track a set of deleted nodes that may be ancestors of other marked for deletion -- only delete the ancestors.
const deleted = new NodeSet();
const { sourceFile, program } = context;
const checker = program.getTypeChecker();
return codeFixAll(context, errorCodes, (changes, diag) => {
const { sourceFile } = context;
const startToken = getTokenAtPosition(sourceFile, diag.start, /*includeJsDocComment*/ false);
const token = findPrecedingToken(textSpanEnd(diag), diag.file)!;
switch (context.fixId) {
@@ -68,8 +70,9 @@ namespace ts.codefix {
if (importDecl) {
changes.deleteNode(sourceFile, importDecl);
}
else if (!tryDeleteFullDestructure(changes, sourceFile, startToken, deleted) && !tryDeleteFullVariableStatement(changes, sourceFile, startToken, deleted)) {
tryDeleteDeclaration(changes, sourceFile, token, deleted);
else if (!tryDeleteFullDestructure(changes, sourceFile, startToken, deleted, checker, /*isFixAll*/ true) &&
!tryDeleteFullVariableStatement(changes, sourceFile, startToken, deleted)) {
tryDeleteDeclaration(changes, sourceFile, token, deleted, checker, /*isFixAll*/ true);
}
break;
default:
@@ -84,7 +87,7 @@ namespace ts.codefix {
return startToken.kind === SyntaxKind.ImportKeyword ? tryCast(startToken.parent, isImportDeclaration) : undefined;
}
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, startToken: Node, deletedAncestors: NodeSet | undefined): boolean {
function tryDeleteFullDestructure(changes: textChanges.ChangeTracker, sourceFile: SourceFile, startToken: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): boolean {
if (startToken.kind !== SyntaxKind.OpenBraceToken || !isObjectBindingPattern(startToken.parent)) return false;
const decl = cast(startToken.parent, isObjectBindingPattern).parent;
switch (decl.kind) {
@@ -92,6 +95,7 @@ namespace ts.codefix {
tryDeleteVariableDeclaration(changes, sourceFile, decl, deletedAncestors);
break;
case SyntaxKind.Parameter:
if (!mayDeleteParameter(decl, checker, isFixAll)) break;
if (deletedAncestors) deletedAncestors.add(decl);
changes.deleteNodeInList(sourceFile, decl);
break;
@@ -144,10 +148,10 @@ namespace ts.codefix {
return false;
}
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined): void {
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): void {
switch (token.kind) {
case SyntaxKind.Identifier:
tryDeleteIdentifier(changes, sourceFile, <Identifier>token, deletedAncestors);
tryDeleteIdentifier(changes, sourceFile, <Identifier>token, deletedAncestors, checker, isFixAll);
break;
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.NamespaceImport:
@@ -170,7 +174,7 @@ namespace ts.codefix {
}
}
function tryDeleteIdentifier(changes: textChanges.ChangeTracker, sourceFile: SourceFile, identifier: Identifier, deletedAncestors: NodeSet | undefined): void {
function tryDeleteIdentifier(changes: textChanges.ChangeTracker, sourceFile: SourceFile, identifier: Identifier, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): void {
const parent = identifier.parent;
switch (parent.kind) {
case SyntaxKind.VariableDeclaration:
@@ -194,11 +198,8 @@ namespace ts.codefix {
break;
case SyntaxKind.Parameter:
if (!mayDeleteParameter(parent as ParameterDeclaration, checker, isFixAll)) break;
const oldFunction = parent.parent;
if (isSetAccessor(oldFunction)) {
// Setter must have a parameter
break;
}
if (isArrowFunction(oldFunction) && oldFunction.parameters.length === 1) {
// Lambdas with exactly one parameter are special because, after removal, there
@@ -347,4 +348,35 @@ namespace ts.codefix {
}
}
}
function mayDeleteParameter(p: ParameterDeclaration, checker: TypeChecker, isFixAll: boolean) {
const parent = p.parent;
switch (parent.kind) {
case SyntaxKind.MethodDeclaration:
// Don't remove a parameter if this overrides something
const symbol = checker.getSymbolAtLocation(parent.name)!;
if (isMemberSymbolInBaseType(symbol, checker)) return false;
// falls through
case SyntaxKind.Constructor:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction: {
// Can't remove a non-last parameter. Can remove a parameter in code-fix-all if future parameters are also unused.
const { parameters } = parent;
const index = parameters.indexOf(p);
Debug.assert(index !== -1);
return isFixAll
? parameters.slice(index + 1).every(p => p.name.kind === SyntaxKind.Identifier && !p.symbol.isReferenced)
: index === parameters.length - 1;
}
case SyntaxKind.SetAccessor:
// Setter must have a parameter
return false;
default:
return Debug.failBadSyntaxKind(parent);
}
}
}
-28
View File
@@ -1399,34 +1399,6 @@ namespace ts.FindAllReferences.Core {
}
}
/**
* Find symbol of the given property-name and add the symbol to the given result array
* @param symbol a symbol to start searching for the given propertyName
* @param propertyName a name of property to search for
* @param result an array of symbol of found property symbols
* @param previousIterationSymbolsCache a cache of symbol from previous iterations of calling this function to prevent infinite revisiting of the same symbol.
* The value of previousIterationSymbol is undefined when the function is first called.
*/
function getPropertySymbolsFromBaseTypes<T>(symbol: Symbol, propertyName: string, checker: TypeChecker, cb: (symbol: Symbol) => T | undefined): T | undefined {
const seen = createMap<true>();
return recur(symbol);
function recur(symbol: Symbol): T | undefined {
// Use `addToSeen` to ensure we don't infinitely recurse in this situation:
// interface C extends C {
// /*findRef*/propName: string;
// }
if (!(symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) || !addToSeen(seen, getSymbolId(symbol))) return;
return firstDefined(symbol.declarations, declaration => firstDefined(getAllSuperTypeNodes(declaration), typeReference => {
const type = checker.getTypeAtLocation(typeReference);
const propertySymbol = type && type.symbol && checker.getPropertyOfType(type, propertyName);
// Visit the typeReference as well to see if it directly or indirectly uses that property
return propertySymbol && (firstDefined(checker.getRootSymbols(propertySymbol), cb) || recur(type!.symbol));
}));
}
}
function getRelatedSymbol(search: Search, referenceSymbol: Symbol, referenceLocation: Node, state: State): Symbol | undefined {
const { checker } = state;
return forEachRelatedSymbol(referenceSymbol, referenceLocation, checker,
+32
View File
@@ -1293,6 +1293,38 @@ namespace ts {
return propSymbol;
}
/**
* Find symbol of the given property-name and add the symbol to the given result array
* @param symbol a symbol to start searching for the given propertyName
* @param propertyName a name of property to search for
* @param result an array of symbol of found property symbols
* @param previousIterationSymbolsCache a cache of symbol from previous iterations of calling this function to prevent infinite revisiting of the same symbol.
* The value of previousIterationSymbol is undefined when the function is first called.
*/
export function getPropertySymbolsFromBaseTypes<T>(symbol: Symbol, propertyName: string, checker: TypeChecker, cb: (symbol: Symbol) => T | undefined): T | undefined {
const seen = createMap<true>();
return recur(symbol);
function recur(symbol: Symbol): T | undefined {
// Use `addToSeen` to ensure we don't infinitely recurse in this situation:
// interface C extends C {
// /*findRef*/propName: string;
// }
if (!(symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) || !addToSeen(seen, getSymbolId(symbol))) return;
return firstDefined(symbol.declarations, declaration => firstDefined(getAllSuperTypeNodes(declaration), typeReference => {
const type = checker.getTypeAtLocation(typeReference);
const propertySymbol = type && type.symbol && checker.getPropertyOfType(type, propertyName);
// Visit the typeReference as well to see if it directly or indirectly uses that property
return type && propertySymbol && (firstDefined(checker.getRootSymbols(propertySymbol), cb) || recur(type.symbol));
}));
}
}
export function isMemberSymbolInBaseType(memberSymbol: Symbol, checker: TypeChecker): boolean {
return getPropertySymbolsFromBaseTypes(memberSymbol.parent!, memberSymbol.name, checker, _ => true) || false;
}
export class NodeSet {
private map = createMap<Node>();
@@ -7,6 +7,21 @@
//// const x = 0;
////}
////function g(a, b, c) { return a; }
////
////interface I {
//// m(x: number): void;
////}
////
////class C implements I {
//// m(x: number): void {} // Does not remove 'x', which is inherited
//// n(x: number): void {}
////}
////
////declare function f(cb: (x: number, y: string) => void): void;
////f((x, y) => {});
////f((x, y) => { x; });
////f((x, y) => { y; });
////
////{
//// let a, b;
////}
@@ -19,6 +34,21 @@ verify.codeFixAll({
`function f() {
}
function g(a) { return a; }
interface I {
m(x: number): void;
}
class C implements I {
m(x: number): void {} // Does not remove 'x', which is inherited
n(): void {}
}
declare function f(cb: (x: number, y: string) => void): void;
f(() => {});
f((x) => { x; });
f((x, y) => { y; });
{
}
for (; ;) {}`,
@@ -6,7 +6,7 @@
////const { x, y } = o;
////const { a, b } = o;
////a;
////export function f({ x, y }, { a, b }) {
////export function f({ a, b }, { x, y }) {
//// a;
////}
@@ -0,0 +1,9 @@
/// <reference path='fourslash.ts' />
// @noUnusedParameters: true
////declare function f(cb: (x: number, y: string) => void): void;
////f((x, y) => { y; });
// No codefix to remove a non-last parameter
verify.codeFixAvailable([{ description: "Prefix 'x' with an underscore" }]);
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
// @noUnusedParameters: true
////interface I {
//// m(x: number): void;
////}
////
////class C implements I {
//// m(x: number): void {}
////}
// No codefix to remove the parameter, it's inherited
verify.codeFixAvailable([{ description: "Prefix 'x' with an underscore" }]);
@@ -1,12 +0,0 @@
/// <reference path='fourslash.ts' />
// @noUnusedLocals: true
//// class C1 {
//// [|constructor(private p1: string, public p2: boolean, public p3: any, p5)|] { p5; }
//// }
verify.codeFix({
description: "Remove declaration for: 'p1'",
index: 0,
newRangeContent: "constructor(public p2: boolean, public p3: any, p5)",
});
@@ -7,7 +7,6 @@
verify.codeFix({
description: "Prefix 'p1' with an underscore",
index: 1,
newFileContent:
`class C1 {
constructor(_p1: string, public p2: boolean, public p3: any, p5) { p5; }
@@ -1,12 +0,0 @@
/// <reference path='fourslash.ts' />
// @noUnusedLocals: true
//// class C1 {
//// [|constructor(public p1: string, private p2: boolean, public p3: any, p5)|] { p5; }
//// }
verify.codeFix({
description: "Remove declaration for: 'p2'",
index: 0,
newRangeContent: "constructor(public p1: string, public p3: any, p5)",
});
@@ -1,12 +0,0 @@
/// <reference path='fourslash.ts' />
// @noUnusedLocals: true
//// class C1 {
//// [|constructor(public p1: string, public p2: boolean, private p3: any, p5)|] { p5; }
//// }
verify.codeFix({
description: "Remove declaration for: 'p3'",
index: 0,
newRangeContent: "constructor(public p1: string, public p2: boolean, p5)",
});
@@ -1,12 +0,0 @@
/// <reference path='fourslash.ts' />
// @noUnusedLocals: true
//// class C1 {
//// [|constructor(private readonly p2: boolean, p5)|] { p5; }
//// }
verify.codeFix({
description: "Remove declaration for: 'p2'",
index: 0,
newRangeContent: "constructor(p5)",
});
@@ -1,12 +0,0 @@
/// <reference path='fourslash.ts' />
// @noUnusedParameters: true
////function [|greeter(x,y)|] {
//// y++;
////}
verify.codeFix({
description: "Remove declaration for: 'x'",
index: 0,
newRangeContent: "greeter(y)",
});
@@ -1,12 +0,0 @@
/// <reference path='fourslash.ts' />
// @noUnusedParameters: true
////[|function greeter(x,y,z) |] {
//// use(x, z);
////}
verify.codeFix({
description: "Remove declaration for: 'y'",
index: 0,
newRangeContent: "function greeter(x,z) ",
});
@@ -1,11 +0,0 @@
/// <reference path='fourslash.ts' />
// @noUnusedLocals: true
// @noUnusedParameters: true
////[|/*~a*/(/*~b*/x/*~c*/,/*~d*/y/*~e*/)/*~f*/ => /*~g*/y/*~h*/|]
verify.codeFix({
description: "Remove declaration for: 'x'",
index: 0,
newRangeContent: "/*~a*/(/*~d*/y/*~e*/)/*~f*/ => /*~g*/y/*~h*/",
});