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:
parent
d7713f4305
commit
d957b1c8c2
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {}) {}`,
|
||||
});
|
||||
|
|
|
@ -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);`,
|
||||
});
|
|
@ -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" }]);
|
|
@ -5,6 +5,6 @@
|
|||
|
||||
verify.codeFix({
|
||||
description: "Prefix 'C' with an underscore",
|
||||
index: 1,
|
||||
index: 2,
|
||||
newFileContent: "function f(new _C(100, 3, undefined)",
|
||||
});
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
////}
|
||||
|
||||
verify.codeFix({
|
||||
description: `Remove declaration for: '"string"'`,
|
||||
description: `Remove declaration for: '["string"]'`,
|
||||
newFileContent: "class C {\n}",
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
////}
|
||||
|
||||
verify.codeFix({
|
||||
index: 1,
|
||||
description: "Prefix 'p1' with an underscore",
|
||||
newFileContent:
|
||||
`class C1 {
|
||||
|
|
|
@ -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"]) {}',
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue