fixUnusedIdentifier: Remove arguments corresponding to unused parameters (#25011)

* fixUnusedIdentifier: Remove arguments corresponding to unused parameters

* Update API (#24966)

* Fix handling of deletions: Make a list of things to delete and don't delete until the end

* Remove dummy test

* Bug fixes

* Update API (#24966)

* Move code to textChanges
This commit is contained in:
Andy 2018-06-27 09:26:47 -07:00 committed by GitHub
parent d7713f4305
commit d957b1c8c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 371 additions and 246 deletions

View file

@ -79,10 +79,13 @@ namespace ts {
return { fileName, textChanges };
}
export function codeFixAll(context: CodeFixAllContext, errorCodes: number[], use: (changes: textChanges.ChangeTracker, error: DiagnosticWithLocation, commands: Push<CodeActionCommand>) => void): CombinedCodeActions {
export function codeFixAll(
context: CodeFixAllContext,
errorCodes: number[],
use: (changes: textChanges.ChangeTracker, error: DiagnosticWithLocation, commands: Push<CodeActionCommand>) => void,
): CombinedCodeActions {
const commands: CodeActionCommand[] = [];
const changes = textChanges.ChangeTracker.with(context, t =>
eachDiagnostic(context, errorCodes, diag => use(t, diag, commands)));
const changes = textChanges.ChangeTracker.with(context, t => eachDiagnostic(context, errorCodes, diag => use(t, diag, commands)));
return createCombinedCodeActions(changes, commands.length === 0 ? undefined : commands);
}

View file

@ -11,33 +11,37 @@ namespace ts.codefix {
Diagnostics.All_destructured_elements_are_unused.code,
Diagnostics.All_variables_are_unused.code,
];
registerCodeFix({
errorCodes,
getCodeActions(context) {
const { errorCode, sourceFile, program } = context;
const checker = program.getTypeChecker();
const startToken = getTokenAtPosition(sourceFile, context.span.start);
const sourceFiles = program.getSourceFiles();
const token = getTokenAtPosition(sourceFile, context.span.start);
const importDecl = tryGetFullImport(startToken);
const importDecl = tryGetFullImport(token);
if (importDecl) {
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, checker, /*isFixAll*/ false));
const delDestructure = textChanges.ChangeTracker.with(context, t =>
tryDeleteFullDestructure(token, t, sourceFile, checker, sourceFiles, /*isFixAll*/ false));
if (delDestructure.length) {
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
}
const delVar = textChanges.ChangeTracker.with(context, t => tryDeleteFullVariableStatement(t, sourceFile, startToken, /*deleted*/ undefined));
const delVar = textChanges.ChangeTracker.with(context, t => tryDeleteFullVariableStatement(sourceFile, token, t));
if (delVar.length) {
return [createCodeFixAction(fixName, delVar, Diagnostics.Remove_variable_statement, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
}
const token = getToken(sourceFile, textSpanEnd(context.span));
const result: CodeFixAction[] = [];
const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(t, sourceFile, token, /*deleted*/ undefined, checker, /*isFixAll*/ false));
const deletion = textChanges.ChangeTracker.with(context, t =>
tryDeleteDeclaration(sourceFile, token, t, checker, sourceFiles, /*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));
const name = isComputedPropertyName(token.parent) ? token.parent : token;
result.push(createCodeFixAction(fixName, deletion, [Diagnostics.Remove_declaration_for_Colon_0, name.getText(sourceFile)], fixIdDelete, Diagnostics.Delete_all_unused_declarations));
}
const prefix = textChanges.ChangeTracker.with(context, t => tryPrefixDeclaration(t, errorCode, sourceFile, token));
@ -49,32 +53,28 @@ namespace ts.codefix {
},
fixIds: [fixIdPrefix, fixIdDelete],
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();
const sourceFiles = program.getSourceFiles();
return codeFixAll(context, errorCodes, (changes, diag) => {
const startToken = getTokenAtPosition(sourceFile, diag.start);
const token = findPrecedingToken(textSpanEnd(diag), diag.file)!;
const token = getTokenAtPosition(sourceFile, diag.start);
switch (context.fixId) {
case fixIdPrefix:
if (isIdentifier(token) && canPrefix(token)) {
tryPrefixDeclaration(changes, diag.code, sourceFile, token);
}
break;
case fixIdDelete:
// Ignore if this range was already deleted.
if (deleted.some(d => rangeContainsPosition(d, diag.start))) break;
const importDecl = tryGetFullImport(startToken);
case fixIdDelete: {
const importDecl = tryGetFullImport(token);
if (importDecl) {
changes.deleteNode(sourceFile, importDecl);
changes.deleteDeclaration(sourceFile, importDecl);
}
else if (!tryDeleteFullDestructure(changes, sourceFile, startToken, deleted, checker, /*isFixAll*/ true) &&
!tryDeleteFullVariableStatement(changes, sourceFile, startToken, deleted)) {
tryDeleteDeclaration(changes, sourceFile, token, deleted, checker, /*isFixAll*/ true);
else if (!tryDeleteFullDestructure(token, changes, sourceFile, checker, sourceFiles, /*isFixAll*/ true) &&
!tryDeleteFullVariableStatement(sourceFile, token, changes)) {
tryDeleteDeclaration(sourceFile, token, changes, checker, sourceFiles, /*isFixAll*/ true);
}
break;
}
default:
Debug.fail(JSON.stringify(context.fixId));
}
@ -83,48 +83,31 @@ namespace ts.codefix {
});
// Sometimes the diagnostic span is an entire ImportDeclaration, so we should remove the whole thing.
function tryGetFullImport(startToken: Node): ImportDeclaration | undefined {
return startToken.kind === SyntaxKind.ImportKeyword ? tryCast(startToken.parent, isImportDeclaration) : undefined;
function tryGetFullImport(token: Node): ImportDeclaration | undefined {
return token.kind === SyntaxKind.ImportKeyword ? tryCast(token.parent, isImportDeclaration) : undefined;
}
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) {
case SyntaxKind.VariableDeclaration:
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;
case SyntaxKind.BindingElement:
if (deletedAncestors) deletedAncestors.add(decl);
changes.deleteNode(sourceFile, decl);
break;
default:
return Debug.assertNever(decl);
function tryDeleteFullDestructure(token: Node, changes: textChanges.ChangeTracker, sourceFile: SourceFile, checker: TypeChecker, sourceFiles: ReadonlyArray<SourceFile>, isFixAll: boolean): boolean {
if (token.kind !== SyntaxKind.OpenBraceToken || !isObjectBindingPattern(token.parent)) return false;
const decl = token.parent.parent;
if (decl.kind === SyntaxKind.Parameter) {
tryDeleteParameter(changes, sourceFile, decl, checker, sourceFiles, isFixAll);
}
else {
changes.deleteDeclaration(sourceFile, decl);
}
return true;
}
function tryDeleteFullVariableStatement(changes: textChanges.ChangeTracker, sourceFile: SourceFile, startToken: Node, deletedAncestors: NodeSet | undefined) {
const declarationList = tryCast(startToken.parent, isVariableDeclarationList);
if (declarationList && declarationList.getChildren(sourceFile)[0] === startToken) {
if (deletedAncestors) deletedAncestors.add(declarationList);
changes.deleteNode(sourceFile, declarationList.parent.kind === SyntaxKind.VariableStatement ? declarationList.parent : declarationList);
function tryDeleteFullVariableStatement(sourceFile: SourceFile, token: Node, changes: textChanges.ChangeTracker): boolean {
const declarationList = tryCast(token.parent, isVariableDeclarationList);
if (declarationList && declarationList.getChildren(sourceFile)[0] === token) {
changes.deleteDeclaration(sourceFile, declarationList.parent.kind === SyntaxKind.VariableStatement ? declarationList.parent : declarationList);
return true;
}
return false;
}
function getToken(sourceFile: SourceFile, pos: number): Node {
const token = findPrecedingToken(pos, sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true)!;
// this handles var ["computed"] = 12;
return token.kind === SyntaxKind.CloseBracketToken ? findPrecedingToken(pos - 1, sourceFile)! : token;
}
function tryPrefixDeclaration(changes: textChanges.ChangeTracker, errorCode: number, sourceFile: SourceFile, token: Node): void {
// Don't offer to prefix a property.
if (errorCode !== Diagnostics.Property_0_is_declared_but_its_value_is_never_read.code && isIdentifier(token) && canPrefix(token)) {
@ -148,8 +131,8 @@ namespace ts.codefix {
return false;
}
function tryDeleteDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean) {
tryDeleteDeclarationWorker(changes, sourceFile, token, deletedAncestors, checker, isFixAll);
function tryDeleteDeclaration(sourceFile: SourceFile, token: Node, changes: textChanges.ChangeTracker, checker: TypeChecker, sourceFiles: ReadonlyArray<SourceFile>, isFixAll: boolean) {
tryDeleteDeclarationWorker(token, changes, sourceFile, checker, sourceFiles, isFixAll);
if (isIdentifier(token)) deleteAssignments(changes, sourceFile, token, checker);
}
@ -157,204 +140,44 @@ namespace ts.codefix {
FindAllReferences.Core.eachSymbolReferenceInFile(token, checker, sourceFile, (ref: Node) => {
if (ref.parent.kind === SyntaxKind.PropertyAccessExpression) ref = ref.parent;
if (ref.parent.kind === SyntaxKind.BinaryExpression && ref.parent.parent.kind === SyntaxKind.ExpressionStatement) {
changes.deleteNode(sourceFile, ref.parent.parent);
changes.deleteDeclaration(sourceFile, ref.parent.parent);
}
});
}
function tryDeleteDeclarationWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined, checker: TypeChecker, isFixAll: boolean): void {
const parent = token.parent;
switch (parent.kind) {
case SyntaxKind.VariableDeclaration:
tryDeleteVariableDeclaration(changes, sourceFile, <VariableDeclaration>parent, deletedAncestors);
break;
case SyntaxKind.TypeParameter:
const typeParameters = getEffectiveTypeParameterDeclarations(<DeclarationWithTypeParameters>parent.parent);
if (typeParameters.length === 1) {
const { pos, end } = cast(typeParameters, isNodeArray);
const previousToken = getTokenAtPosition(sourceFile, pos - 1);
const nextToken = getTokenAtPosition(sourceFile, end);
Debug.assert(previousToken.kind === SyntaxKind.LessThanToken);
Debug.assert(nextToken.kind === SyntaxKind.GreaterThanToken);
changes.deleteNodeRange(sourceFile, previousToken, nextToken);
}
else {
changes.deleteNodeInList(sourceFile, parent);
}
break;
case SyntaxKind.Parameter:
if (!mayDeleteParameter(parent as ParameterDeclaration, checker, isFixAll)) break;
const oldFunction = parent.parent;
if (isArrowFunction(oldFunction) && oldFunction.parameters.length === 1) {
// Lambdas with exactly one parameter are special because, after removal, there
// must be an empty parameter list (i.e. `()`) and this won't necessarily be the
// case if the parameter is simply removed (e.g. in `x => 1`).
const newFunction = updateArrowFunction(
oldFunction,
oldFunction.modifiers,
oldFunction.typeParameters,
/*parameters*/ undefined!, // TODO: GH#18217
oldFunction.type,
oldFunction.equalsGreaterThanToken,
oldFunction.body);
// Drop leading and trailing trivia of the new function because we're only going
// to replace the span (vs the full span) of the old function - the old leading
// and trailing trivia will remain.
suppressLeadingAndTrailingTrivia(newFunction);
changes.replaceNode(sourceFile, oldFunction, newFunction);
}
else {
changes.deleteNodeInList(sourceFile, parent);
}
break;
case SyntaxKind.BindingElement: {
const pattern = (parent as BindingElement).parent;
const preserveComma = pattern.kind === SyntaxKind.ArrayBindingPattern && parent !== last(pattern.elements);
if (preserveComma) {
changes.deleteNode(sourceFile, parent);
}
else {
changes.deleteNodeInList(sourceFile, parent);
}
break;
}
// handle case where 'import a = A;'
case SyntaxKind.ImportEqualsDeclaration:
const importEquals = getAncestor(token, SyntaxKind.ImportEqualsDeclaration)!;
changes.deleteNode(sourceFile, importEquals);
break;
case SyntaxKind.ImportSpecifier:
const namedImports = <NamedImports>parent.parent;
if (namedImports.elements.length === 1) {
tryDeleteNamedImportBinding(changes, sourceFile, namedImports);
}
else {
// delete import specifier
changes.deleteNodeInList(sourceFile, parent);
}
break;
case SyntaxKind.ImportClause: // this covers both 'import |d|' and 'import |d,| *'
const importClause = <ImportClause>parent;
if (!importClause.namedBindings) { // |import d from './file'|
changes.deleteNode(sourceFile, getAncestor(importClause, SyntaxKind.ImportDeclaration)!);
}
else {
// import |d,| * as ns from './file'
const start = importClause.name!.getStart(sourceFile);
const nextToken = getTokenAtPosition(sourceFile, importClause.name!.end);
if (nextToken && nextToken.kind === SyntaxKind.CommaToken) {
// shift first non-whitespace position after comma to the start position of the node
const end = skipTrivia(sourceFile.text, nextToken.end, /*stopAfterLineBreaks*/ false, /*stopAtComments*/ true);
changes.deleteRange(sourceFile, { pos: start, end });
}
else {
changes.deleteNode(sourceFile, importClause.name!);
}
}
break;
case SyntaxKind.NamespaceImport:
tryDeleteNamedImportBinding(changes, sourceFile, <NamespaceImport>parent);
break;
default:
if (isDeclarationName(token)) {
if (deletedAncestors) deletedAncestors.add(token.parent);
changes.deleteNode(sourceFile, token.parent);
}
else if (isLiteralComputedPropertyDeclarationName(token)) {
if (deletedAncestors) deletedAncestors.add(token.parent.parent);
changes.deleteNode(sourceFile, token.parent.parent);
}
break;
}
}
function tryDeleteNamedImportBinding(changes: textChanges.ChangeTracker, sourceFile: SourceFile, namedBindings: NamedImportBindings): void {
if (namedBindings.parent.name) {
// Delete named imports while preserving the default import
// import d|, * as ns| from './file'
// import d|, { a }| from './file'
const previousToken = getTokenAtPosition(sourceFile, namedBindings.pos - 1);
if (previousToken && previousToken.kind === SyntaxKind.CommaToken) {
changes.deleteRange(sourceFile, { pos: previousToken.getStart(), end: namedBindings.end });
}
function tryDeleteDeclarationWorker(token: Node, changes: textChanges.ChangeTracker, sourceFile: SourceFile, checker: TypeChecker, sourceFiles: ReadonlyArray<SourceFile>, isFixAll: boolean): void {
const { parent } = token;
if (isParameter(parent)) {
tryDeleteParameter(changes, sourceFile, parent, checker, sourceFiles, isFixAll);
}
else {
// Delete the entire import declaration
// |import * as ns from './file'|
// |import { a } from './file'|
const importDecl = getAncestor(namedBindings, SyntaxKind.ImportDeclaration)!;
changes.deleteNode(sourceFile, importDecl);
changes.deleteDeclaration(sourceFile, isImportClause(parent) ? token : isComputedPropertyName(parent) ? parent.parent : parent);
}
}
// token.parent is a variableDeclaration
function tryDeleteVariableDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, varDecl: VariableDeclaration, deletedAncestors: NodeSet | undefined): void {
switch (varDecl.parent.parent.kind) {
case SyntaxKind.ForStatement: {
const forStatement = varDecl.parent.parent;
const forInitializer = <VariableDeclarationList>forStatement.initializer;
if (forInitializer.declarations.length === 1) {
if (deletedAncestors) deletedAncestors.add(forInitializer);
changes.deleteNode(sourceFile, forInitializer);
}
else {
if (deletedAncestors) deletedAncestors.add(varDecl);
changes.deleteNodeInList(sourceFile, varDecl);
}
break;
}
case SyntaxKind.ForOfStatement:
const forOfStatement = varDecl.parent.parent;
Debug.assert(forOfStatement.initializer.kind === SyntaxKind.VariableDeclarationList);
const forOfInitializer = <VariableDeclarationList>forOfStatement.initializer;
if (deletedAncestors) deletedAncestors.add(forOfInitializer.declarations[0]);
changes.replaceNode(sourceFile, forOfInitializer.declarations[0], createObjectLiteral());
break;
case SyntaxKind.ForInStatement:
case SyntaxKind.TryStatement:
break;
default:
const variableStatement = varDecl.parent.parent;
if (variableStatement.declarationList.declarations.length === 1) {
if (deletedAncestors) deletedAncestors.add(variableStatement);
changes.deleteNode(sourceFile, variableStatement);
}
else {
if (deletedAncestors) deletedAncestors.add(varDecl);
changes.deleteNodeInList(sourceFile, varDecl);
}
function tryDeleteParameter(changes: textChanges.ChangeTracker, sourceFile: SourceFile, p: ParameterDeclaration, checker: TypeChecker, sourceFiles: ReadonlyArray<SourceFile>, isFixAll: boolean): void {
if (mayDeleteParameter(p, checker, isFixAll)) {
changes.deleteDeclaration(sourceFile, p);
deleteUnusedArguments(changes, sourceFile, p, sourceFiles, checker);
}
}
function mayDeleteParameter(p: ParameterDeclaration, checker: TypeChecker, isFixAll: boolean) {
const parent = p.parent;
function mayDeleteParameter(p: ParameterDeclaration, checker: TypeChecker, isFixAll: boolean): boolean {
const { parent } = p;
switch (parent.kind) {
case SyntaxKind.MethodDeclaration:
// Don't remove a parameter if this overrides something
// 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:
return true;
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.
// Can't remove a non-last parameter in a callback. 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);
@ -371,4 +194,13 @@ namespace ts.codefix {
return Debug.failBadSyntaxKind(parent);
}
}
function deleteUnusedArguments(changes: textChanges.ChangeTracker, sourceFile: SourceFile, deletedParameter: ParameterDeclaration, sourceFiles: ReadonlyArray<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.deleteDeclaration(sourceFile, call.arguments[index]);
}
});
}
}

View file

@ -754,6 +754,25 @@ namespace ts.FindAllReferences.Core {
}
}
export function eachSignatureCall(signature: SignatureDeclaration, sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker, cb: (call: CallExpression) => void): void {
if (!signature.name || !isIdentifier(signature.name)) return;
const symbol = Debug.assertDefined(checker.getSymbolAtLocation(signature.name));
for (const sourceFile of sourceFiles) {
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 referenceSymbol = checker.getSymbolAtLocation(name);
if (referenceSymbol && checker.getRootSymbols(referenceSymbol).some(s => s === symbol)) {
cb(call);
}
}
}
}
function getPossibleSymbolReferenceNodes(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): ReadonlyArray<Node> {
return getPossibleSymbolReferencePositions(sourceFile, symbolName, container).map(pos => getTouchingPropertyName(sourceFile, pos));
}

View file

@ -214,6 +214,7 @@ namespace ts.textChanges {
private readonly newFiles: { readonly oldFile: SourceFile, readonly fileName: string, readonly statements: ReadonlyArray<Statement> }[] = [];
private readonly deletedNodesInLists = new NodeSet(); // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
private readonly classesWithNodesInsertedAtStart = createMap<ClassDeclaration>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
private readonly deletedDeclarations: { readonly sourceFile: SourceFile, readonly node: Node }[] = [];
public static fromContext(context: TextChangesContext): ChangeTracker {
return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext);
@ -233,6 +234,10 @@ namespace ts.textChanges {
return this;
}
deleteDeclaration(sourceFile: SourceFile, node: Node): void {
this.deletedDeclarations.push({ sourceFile, node });
}
/** Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`. */
public deleteNode(sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = {}) {
const startPosition = getAdjustedStartPosition(sourceFile, node, options, Position.FullStart);
@ -699,6 +704,14 @@ namespace ts.textChanges {
});
}
private finishDeleteDeclarations(): void {
for (const { sourceFile, node } of this.deletedDeclarations) {
if (!this.deletedDeclarations.some(d => d.sourceFile === sourceFile && rangeContainsRangeExclusive(d.node, node))) {
deleteDeclaration.deleteDeclaration(this, sourceFile, node);
}
}
}
/**
* Note: after calling this, the TextChanges object must be discarded!
* @param validate only for tests
@ -706,6 +719,7 @@ namespace ts.textChanges {
* so we can only call this once and can't get the non-formatted text separately.
*/
public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] {
this.finishDeleteDeclarations();
this.finishClassesWithNodesInsertedAtStart();
this.finishTrailingCommaAfterDeletingNodesInList();
const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate);
@ -1021,4 +1035,170 @@ namespace ts.textChanges {
return (isPropertySignature(a) || isPropertyDeclaration(a)) && isClassOrTypeElement(b) && b.name!.kind === SyntaxKind.ComputedPropertyName
|| isStatementButNotDeclaration(a) && isStatementButNotDeclaration(b); // TODO: only if b would start with a `(` or `[`
}
namespace deleteDeclaration {
export function deleteDeclaration(changes: ChangeTracker, sourceFile: SourceFile, node: Node): void {
switch (node.kind) {
case SyntaxKind.Parameter: {
const oldFunction = node.parent;
if (isArrowFunction(oldFunction) && oldFunction.parameters.length === 1) {
// Lambdas with exactly one parameter are special because, after removal, there
// must be an empty parameter list (i.e. `()`) and this won't necessarily be the
// case if the parameter is simply removed (e.g. in `x => 1`).
const newFunction = updateArrowFunction(
oldFunction,
oldFunction.modifiers,
oldFunction.typeParameters,
/*parameters*/ undefined!, // TODO: GH#18217
oldFunction.type,
oldFunction.equalsGreaterThanToken,
oldFunction.body);
// Drop leading and trailing trivia of the new function because we're only going
// to replace the span (vs the full span) of the old function - the old leading
// and trailing trivia will remain.
suppressLeadingAndTrailingTrivia(newFunction);
changes.replaceNode(sourceFile, oldFunction, newFunction);
}
else {
changes.deleteNodeInList(sourceFile, node);
}
break;
}
case SyntaxKind.ImportDeclaration:
changes.deleteNode(sourceFile, node);
break;
case SyntaxKind.BindingElement:
const pattern = (node as BindingElement).parent;
const preserveComma = pattern.kind === SyntaxKind.ArrayBindingPattern && node !== last(pattern.elements);
if (preserveComma) {
changes.deleteNode(sourceFile, node);
}
else {
changes.deleteNodeInList(sourceFile, node);
}
break;
case SyntaxKind.VariableDeclaration:
deleteVariableDeclaration(changes, sourceFile, node as VariableDeclaration);
break;
case SyntaxKind.TypeParameter: {
const typeParameters = getEffectiveTypeParameterDeclarations(<DeclarationWithTypeParameters>node.parent);
if (typeParameters.length === 1) {
const { pos, end } = cast(typeParameters, isNodeArray);
const previousToken = getTokenAtPosition(sourceFile, pos - 1);
const nextToken = getTokenAtPosition(sourceFile, end);
Debug.assert(previousToken.kind === SyntaxKind.LessThanToken);
Debug.assert(nextToken.kind === SyntaxKind.GreaterThanToken);
changes.deleteNodeRange(sourceFile, previousToken, nextToken);
}
else {
changes.deleteNodeInList(sourceFile, node);
}
break;
}
case SyntaxKind.ImportSpecifier:
const namedImports = (node as ImportSpecifier).parent;
if (namedImports.elements.length === 1) {
deleteImportBinding(changes, sourceFile, namedImports);
}
else {
changes.deleteNodeInList(sourceFile, node);
}
break;
case SyntaxKind.NamespaceImport:
deleteImportBinding(changes, sourceFile, node as NamespaceImport);
break;
default:
if (isImportClause(node.parent) && node.parent.name === node) {
deleteDefaultImport(changes, sourceFile, node.parent);
}
else if (isCallLikeExpression(node.parent)) {
changes.deleteNodeInList(sourceFile, node);
}
else {
changes.deleteNode(sourceFile, node);
}
}
}
function deleteDefaultImport(changes: ChangeTracker, sourceFile: SourceFile, importClause: ImportClause): void {
if (!importClause.namedBindings) {
// Delete the whole import
changes.deleteNode(sourceFile, importClause.parent);
}
else {
// import |d,| * as ns from './file'
const start = importClause.name!.getStart(sourceFile);
const nextToken = getTokenAtPosition(sourceFile, importClause.name!.end);
if (nextToken && nextToken.kind === SyntaxKind.CommaToken) {
// shift first non-whitespace position after comma to the start position of the node
const end = skipTrivia(sourceFile.text, nextToken.end, /*stopAfterLineBreaks*/ false, /*stopAtComments*/ true);
changes.deleteRange(sourceFile, { pos: start, end });
}
else {
changes.deleteNode(sourceFile, importClause.name!);
}
}
}
function deleteImportBinding(changes: ChangeTracker, sourceFile: SourceFile, node: NamedImportBindings): void {
if (node.parent.name) {
// Delete named imports while preserving the default import
// import d|, * as ns| from './file'
// import d|, { a }| from './file'
const previousToken = Debug.assertDefined(getTokenAtPosition(sourceFile, node.pos - 1));
changes.deleteRange(sourceFile, { pos: previousToken.getStart(sourceFile), end: node.end });
}
else {
// Delete the entire import declaration
// |import * as ns from './file'|
// |import { a } from './file'|
const importDecl = getAncestor(node, SyntaxKind.ImportDeclaration)!;
changes.deleteNode(sourceFile, importDecl);
}
}
function deleteVariableDeclaration(changes: ChangeTracker, sourceFile: SourceFile, node: VariableDeclaration): void {
const { parent } = node;
if (parent.kind === SyntaxKind.CatchClause) {
// TODO: There's currently no unused diagnostic for this, could be a suggestion
changes.deleteNodeRange(sourceFile, findChildOfKind(parent, SyntaxKind.OpenParenToken, sourceFile)!, findChildOfKind(parent, SyntaxKind.CloseParenToken, sourceFile)!);
return;
}
if (parent.declarations.length !== 1) {
changes.deleteNodeInList(sourceFile, node);
return;
}
const gp = parent.parent;
switch (gp.kind) {
case SyntaxKind.ForOfStatement:
case SyntaxKind.ForInStatement:
changes.replaceNode(sourceFile, node, createObjectLiteral());
break;
case SyntaxKind.ForStatement:
changes.deleteNode(sourceFile, parent);
break;
case SyntaxKind.VariableStatement:
changes.deleteNode(sourceFile, gp);
break;
default:
Debug.assertNever(gp);
}
}
}
}

View file

@ -419,6 +419,10 @@ namespace ts {
return startEndContainsRange(r1.pos, r1.end, r2);
}
export function rangeContainsRangeExclusive(r1: TextRange, r2: TextRange): boolean {
return rangeContainsPositionExclusive(r1, r2.pos) && rangeContainsPositionExclusive(r1, r2.end);
}
export function rangeContainsPosition(r: TextRange, pos: number): boolean {
return r.pos <= pos && pos <= r.end;
}
@ -1340,7 +1344,13 @@ namespace ts {
return getPropertySymbolsFromBaseTypes(memberSymbol.parent!, memberSymbol.name, checker, _ => true) || false;
}
export class NodeSet {
export interface ReadonlyNodeSet {
has(node: Node): boolean;
forEach(cb: (node: Node) => void): void;
some(pred: (node: Node) => boolean): boolean;
}
export class NodeSet implements ReadonlyNodeSet {
private map = createMap<Node>();
add(node: Node): void {

View file

@ -10717,6 +10717,7 @@ declare namespace ts {
}
function getLineStartPositionForPosition(position: number, sourceFile: SourceFileLike): number;
function rangeContainsRange(r1: TextRange, r2: TextRange): boolean;
function rangeContainsRangeExclusive(r1: TextRange, r2: TextRange): boolean;
function rangeContainsPosition(r: TextRange, pos: number): boolean;
function rangeContainsPositionExclusive(r: TextRange, pos: number): boolean;
function startEndContainsRange(start: number, end: number, range: TextRange): boolean;
@ -10834,7 +10835,12 @@ declare namespace ts {
*/
function getPropertySymbolsFromBaseTypes<T>(symbol: Symbol, propertyName: string, checker: TypeChecker, cb: (symbol: Symbol) => T | undefined): T | undefined;
function isMemberSymbolInBaseType(memberSymbol: Symbol, checker: TypeChecker): boolean;
class NodeSet {
interface ReadonlyNodeSet {
has(node: Node): boolean;
forEach(cb: (node: Node) => void): void;
some(pred: (node: Node) => boolean): boolean;
}
class NodeSet implements ReadonlyNodeSet {
private map;
add(node: Node): void;
has(node: Node): boolean;
@ -11127,6 +11133,7 @@ declare namespace ts.FindAllReferences.Core {
/** Used as a quick check for whether a symbol is used at all in a file (besides its definition). */
function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile): boolean;
function eachSymbolReferenceInFile<T>(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile, cb: (token: Identifier) => T): T | undefined;
function eachSignatureCall(signature: SignatureDeclaration, sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker, cb: (call: CallExpression) => void): void;
/**
* Given an initial searchMeaning, extracted from a location, widen the search scope based on the declarations
* of the corresponding symbol. e.g. if we are searching for "Foo" in value position, but "Foo" references a class
@ -11505,11 +11512,13 @@ declare namespace ts.textChanges {
private readonly newFiles;
private readonly deletedNodesInLists;
private readonly classesWithNodesInsertedAtStart;
private readonly deletedDeclarations;
static fromContext(context: TextChangesContext): ChangeTracker;
static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): FileTextChanges[];
/** Public for tests only. Other callers should use `ChangeTracker.with`. */
constructor(newLineCharacter: string, formatContext: formatting.FormatContext);
deleteRange(sourceFile: SourceFile, range: TextRange): this;
deleteDeclaration(sourceFile: SourceFile, node: Node): void;
/** Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`. */
deleteNode(sourceFile: SourceFile, node: Node, options?: ConfigurableStartEnd): this;
deleteModifier(sourceFile: SourceFile, modifier: Modifier): void;
@ -11559,6 +11568,7 @@ declare namespace ts.textChanges {
insertNodeInListAfter(sourceFile: SourceFile, after: Node, newNode: Node, containingList?: NodeArray<Node> | undefined): this;
private finishClassesWithNodesInsertedAtStart;
private finishTrailingCommaAfterDeletingNodesInList;
private finishDeleteDeclarations;
/**
* Note: after calling this, the TextChanges object must be discarded!
* @param validate only for tests

View file

@ -3,10 +3,17 @@
// @noUnusedLocals: true
// @noUnusedParameters: true
////import d from "foo";
////import d2, { used1 } from "foo";
////import { x } from "foo";
////import { x2, used2 } from "foo";
////used1; used2;
////
////function f(a, b) {
//// const x = 0;
////}
////function g(a, b, c) { return a; }
////f; g;
////
////interface I {
//// m(x: number): void;
@ -15,25 +22,34 @@
////class C implements I {
//// m(x: number): void {} // Does not remove 'x', which is inherited
//// n(x: number): void {}
//// private ["o"](): void {}
////}
////C;
////
////declare function f(cb: (x: number, y: string) => void): void;
////f((x, y) => {});
////f((x, y) => { x; });
////f((x, y) => { y; });
////declare function takesCb(cb: (x: number, y: string) => void): void;
////takesCb((x, y) => {});
////takesCb((x, y) => { x; });
////takesCb((x, y) => { y; });
////
////{
//// let a, b;
////}
////for (let i = 0, j = 0; ;) {}
////for (const x of []) {}
////for (const y in {}) {}
verify.codeFixAll({
fixId: "unusedIdentifier_delete",
fixAllDescription: "Delete all unused declarations",
newFileContent:
`function f() {
`import { used1 } from "foo";
import { used2 } from "foo";
used1; used2;
function f() {
}
function g(a) { return a; }
f; g;
interface I {
m(x: number): void;
@ -43,13 +59,16 @@ class C implements I {
m(x: number): void {} // Does not remove 'x', which is inherited
n(): void {}
}
C;
declare function f(cb: (x: number, y: string) => void): void;
f(() => {});
f((x) => { x; });
f((x, y) => { y; });
declare function takesCb(cb: (x: number, y: string) => void): void;
takesCb(() => {});
takesCb((x) => { x; });
takesCb((x, y) => { y; });
{
}
for (; ;) {}`,
for (; ;) {}
for (const {} of []) {}
for (const {} in {}) {}`,
});

View file

@ -0,0 +1,41 @@
/// <reference path='fourslash.ts' />
// @noUnusedLocals: true
// @noUnusedParameters: true
////function f(a, b, { x, y }) { b; }
////f(0, 1, 2);
////
////class C {
//// m(a, b, c) { b; }
////}
////new C().m(0, 1, 2);
////
////// Test of deletedAncestors
////function a(a: any, unused: any) { a; }
////function b(a: any, unused: any) { a; }
////
////b(1, {
//// prop: a(2, [
//// b(3, a(4, undefined)),
//// ]),
////});
verify.codeFixAll({
fixId: "unusedIdentifier_delete",
fixAllDescription: "Delete all unused declarations",
newFileContent:
`function f(b) { b; }
f(1);
class C {
m(b) { b; }
}
new C().m(1);
// Test of deletedAncestors
function a(a: any) { a; }
function b(a: any) { a; }
b(1);`,
});

View file

@ -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 in a callback
verify.codeFixAvailable([{ description: "Prefix 'x' with an underscore" }]);

View file

@ -5,6 +5,6 @@
verify.codeFix({
description: "Prefix 'C' with an underscore",
index: 1,
index: 2,
newFileContent: "function f(new _C(100, 3, undefined)",
});

View file

@ -6,6 +6,6 @@
////}
verify.codeFix({
description: `Remove declaration for: '"string"'`,
description: `Remove declaration for: '["string"]'`,
newFileContent: "class C {\n}",
});

View file

@ -6,6 +6,7 @@
////}
verify.codeFix({
index: 1,
description: "Prefix 'p1' with an underscore",
newFileContent:
`class C1 {

View file

@ -4,6 +4,7 @@
////for (const elem in ["a", "b", "c"]) {}
verify.codeFix({
index: 1,
description: "Prefix 'elem' with an underscore",
newFileContent: 'for (const _elem in ["a", "b", "c"]) {}',
});