• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.codefix {
3    const fixName = "unusedIdentifier";
4    const fixIdPrefix = "unusedIdentifier_prefix";
5    const fixIdDelete = "unusedIdentifier_delete";
6    const fixIdDeleteImports = "unusedIdentifier_deleteImports";
7    const fixIdInfer = "unusedIdentifier_infer";
8    const errorCodes = [
9        Diagnostics._0_is_declared_but_its_value_is_never_read.code,
10        Diagnostics._0_is_declared_but_never_used.code,
11        Diagnostics.Property_0_is_declared_but_its_value_is_never_read.code,
12        Diagnostics.All_imports_in_import_declaration_are_unused.code,
13        Diagnostics.All_destructured_elements_are_unused.code,
14        Diagnostics.All_variables_are_unused.code,
15        Diagnostics.All_type_parameters_are_unused.code,
16    ];
17
18    registerCodeFix({
19        errorCodes,
20        getCodeActions(context) {
21            const { errorCode, sourceFile, program, cancellationToken } = context;
22            const checker = program.getTypeChecker();
23            const sourceFiles = program.getSourceFiles();
24            const token = getTokenAtPosition(sourceFile, context.span.start);
25
26            if (isJSDocTemplateTag(token)) {
27                return [createDeleteFix(textChanges.ChangeTracker.with(context, t => t.delete(sourceFile, token)), Diagnostics.Remove_template_tag)];
28            }
29            if (token.kind === SyntaxKind.LessThanToken) {
30                const changes = textChanges.ChangeTracker.with(context, t => deleteTypeParameters(t, sourceFile, token));
31                return [createDeleteFix(changes, Diagnostics.Remove_type_parameters)];
32            }
33            const importDecl = tryGetFullImport(token);
34            if (importDecl) {
35                const changes = textChanges.ChangeTracker.with(context, t => t.delete(sourceFile, importDecl));
36                return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDeleteImports, Diagnostics.Delete_all_unused_imports)];
37            }
38            else if (isImport(token)) {
39                const deletion = textChanges.ChangeTracker.with(context, t => tryDeleteDeclaration(sourceFile, token, t, checker, sourceFiles, program, cancellationToken, /*isFixAll*/ false));
40                if (deletion.length) {
41                    return [createCodeFixAction(fixName, deletion, [Diagnostics.Remove_unused_declaration_for_Colon_0, token.getText(sourceFile)], fixIdDeleteImports, Diagnostics.Delete_all_unused_imports)];
42                }
43            }
44
45            if (isObjectBindingPattern(token.parent) || isArrayBindingPattern(token.parent)) {
46                if (isParameter(token.parent.parent)) {
47                    const elements = token.parent.elements;
48                    const diagnostic: [DiagnosticMessage, string] = [
49                        elements.length > 1 ? Diagnostics.Remove_unused_declarations_for_Colon_0 : Diagnostics.Remove_unused_declaration_for_Colon_0,
50                        map(elements, e => e.getText(sourceFile)).join(", ")
51                    ];
52                    return [
53                        createDeleteFix(textChanges.ChangeTracker.with(context, t =>
54                            deleteDestructuringElements(t, sourceFile, token.parent as ObjectBindingPattern | ArrayBindingPattern)), diagnostic)
55                    ];
56                }
57                return [
58                    createDeleteFix(textChanges.ChangeTracker.with(context, t =>
59                        t.delete(sourceFile, token.parent.parent)), Diagnostics.Remove_unused_destructuring_declaration)
60                ];
61            }
62
63            if (canDeleteEntireVariableStatement(sourceFile, token)) {
64                return [
65                    createDeleteFix(textChanges.ChangeTracker.with(context, t =>
66                        deleteEntireVariableStatement(t, sourceFile, token.parent as VariableDeclarationList)), Diagnostics.Remove_variable_statement)
67                ];
68            }
69
70            const result: CodeFixAction[] = [];
71            if (token.kind === SyntaxKind.InferKeyword) {
72                const changes = textChanges.ChangeTracker.with(context, t => changeInferToUnknown(t, sourceFile, token));
73                const name = cast(token.parent, isInferTypeNode).typeParameter.name.text;
74                result.push(createCodeFixAction(fixName, changes, [Diagnostics.Replace_infer_0_with_unknown, name], fixIdInfer, Diagnostics.Replace_all_unused_infer_with_unknown));
75            }
76            else {
77                const deletion = textChanges.ChangeTracker.with(context, t =>
78                    tryDeleteDeclaration(sourceFile, token, t, checker, sourceFiles, program, cancellationToken, /*isFixAll*/ false));
79                if (deletion.length) {
80                    const name = isComputedPropertyName(token.parent) ? token.parent : token;
81                    result.push(createDeleteFix(deletion, [Diagnostics.Remove_unused_declaration_for_Colon_0, name.getText(sourceFile)]));
82                }
83            }
84
85            const prefix = textChanges.ChangeTracker.with(context, t => tryPrefixDeclaration(t, errorCode, sourceFile, token));
86            if (prefix.length) {
87                result.push(createCodeFixAction(fixName, prefix, [Diagnostics.Prefix_0_with_an_underscore, token.getText(sourceFile)], fixIdPrefix, Diagnostics.Prefix_all_unused_declarations_with_where_possible));
88            }
89
90            return result;
91        },
92        fixIds: [fixIdPrefix, fixIdDelete, fixIdDeleteImports, fixIdInfer],
93        getAllCodeActions: context => {
94            const { sourceFile, program, cancellationToken } = context;
95            const checker = program.getTypeChecker();
96            const sourceFiles = program.getSourceFiles();
97            return codeFixAll(context, errorCodes, (changes, diag) => {
98                const token = getTokenAtPosition(sourceFile, diag.start);
99                switch (context.fixId) {
100                    case fixIdPrefix:
101                        tryPrefixDeclaration(changes, diag.code, sourceFile, token);
102                        break;
103                    case fixIdDeleteImports: {
104                        const importDecl = tryGetFullImport(token);
105                        if (importDecl) {
106                            changes.delete(sourceFile, importDecl);
107                        }
108                        else if (isImport(token)) {
109                            tryDeleteDeclaration(sourceFile, token, changes, checker, sourceFiles, program, cancellationToken, /*isFixAll*/ true);
110                        }
111                        break;
112                    }
113                    case fixIdDelete: {
114                        if (token.kind === SyntaxKind.InferKeyword || isImport(token)) {
115                            break; // Can't delete
116                        }
117                        else if (isJSDocTemplateTag(token)) {
118                            changes.delete(sourceFile, token);
119                        }
120                        else if (token.kind === SyntaxKind.LessThanToken) {
121                            deleteTypeParameters(changes, sourceFile, token);
122                        }
123                        else if (isObjectBindingPattern(token.parent)) {
124                            if (token.parent.parent.initializer) {
125                                break;
126                            }
127                            else if (!isParameter(token.parent.parent) || isNotProvidedArguments(token.parent.parent, checker, sourceFiles)) {
128                                changes.delete(sourceFile, token.parent.parent);
129                            }
130                        }
131                        else if (isArrayBindingPattern(token.parent.parent) && token.parent.parent.parent.initializer) {
132                            break;
133                        }
134                        else if (canDeleteEntireVariableStatement(sourceFile, token)) {
135                            deleteEntireVariableStatement(changes, sourceFile, token.parent as VariableDeclarationList);
136                        }
137                        else {
138                            tryDeleteDeclaration(sourceFile, token, changes, checker, sourceFiles, program, cancellationToken, /*isFixAll*/ true);
139                        }
140                        break;
141                    }
142                    case fixIdInfer:
143                        if (token.kind === SyntaxKind.InferKeyword) {
144                            changeInferToUnknown(changes, sourceFile, token);
145                        }
146                        break;
147                    default:
148                        Debug.fail(JSON.stringify(context.fixId));
149                }
150            });
151        },
152    });
153
154    function changeInferToUnknown(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node): void {
155        changes.replaceNode(sourceFile, token.parent, factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword));
156    }
157
158    function createDeleteFix(changes: FileTextChanges[], diag: DiagnosticAndArguments): CodeFixAction {
159        return createCodeFixAction(fixName, changes, diag, fixIdDelete, Diagnostics.Delete_all_unused_declarations);
160    }
161
162    function deleteTypeParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node): void {
163        changes.delete(sourceFile, Debug.checkDefined(cast(token.parent, isDeclarationWithTypeParameterChildren).typeParameters, "The type parameter to delete should exist"));
164    }
165
166    function isImport(token: Node) {
167        return token.kind === SyntaxKind.ImportKeyword
168            || token.kind === SyntaxKind.Identifier && (token.parent.kind === SyntaxKind.ImportSpecifier || token.parent.kind === SyntaxKind.ImportClause);
169    }
170
171    /** Sometimes the diagnostic span is an entire ImportDeclaration, so we should remove the whole thing. */
172    function tryGetFullImport(token: Node): ImportDeclaration | undefined {
173        return token.kind === SyntaxKind.ImportKeyword ? tryCast(token.parent, isImportDeclaration) : undefined;
174    }
175
176    function canDeleteEntireVariableStatement(sourceFile: SourceFile, token: Node): boolean {
177        return isVariableDeclarationList(token.parent) && first(token.parent.getChildren(sourceFile)) === token;
178    }
179
180    function deleteEntireVariableStatement(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: VariableDeclarationList) {
181        changes.delete(sourceFile, node.parent.kind === SyntaxKind.VariableStatement ? node.parent : node);
182    }
183
184    function deleteDestructuringElements(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: ObjectBindingPattern | ArrayBindingPattern) {
185        forEach(node.elements, n => changes.delete(sourceFile, n));
186    }
187
188    function tryPrefixDeclaration(changes: textChanges.ChangeTracker, errorCode: number, sourceFile: SourceFile, token: Node): void {
189        // Don't offer to prefix a property.
190        if (errorCode === Diagnostics.Property_0_is_declared_but_its_value_is_never_read.code) return;
191        if (token.kind === SyntaxKind.InferKeyword) {
192            token = cast(token.parent, isInferTypeNode).typeParameter.name;
193        }
194        if (isIdentifier(token) && canPrefix(token)) {
195            changes.replaceNode(sourceFile, token, factory.createIdentifier(`_${token.text}`));
196            if (isParameter(token.parent)) {
197                getJSDocParameterTags(token.parent).forEach((tag) => {
198                    if (isIdentifier(tag.name)) {
199                        changes.replaceNode(sourceFile, tag.name, factory.createIdentifier(`_${tag.name.text}`));
200                    }
201                });
202            }
203        }
204    }
205
206    function canPrefix(token: Identifier): boolean {
207        switch (token.parent.kind) {
208            case SyntaxKind.Parameter:
209            case SyntaxKind.TypeParameter:
210                return true;
211            case SyntaxKind.VariableDeclaration: {
212                const varDecl = token.parent as VariableDeclaration;
213                switch (varDecl.parent.parent.kind) {
214                    case SyntaxKind.ForOfStatement:
215                    case SyntaxKind.ForInStatement:
216                        return true;
217                }
218            }
219        }
220        return false;
221    }
222
223    function tryDeleteDeclaration(sourceFile: SourceFile, token: Node, changes: textChanges.ChangeTracker, checker: TypeChecker, sourceFiles: readonly SourceFile[], program: Program, cancellationToken: CancellationToken, isFixAll: boolean) {
224        tryDeleteDeclarationWorker(token, changes, sourceFile, checker, sourceFiles, program, cancellationToken, isFixAll);
225        if (isIdentifier(token)) {
226            FindAllReferences.Core.eachSymbolReferenceInFile(token, checker, sourceFile, (ref: Node) => {
227                if (isPropertyAccessExpression(ref.parent) && ref.parent.name === ref) ref = ref.parent;
228                if (!isFixAll && mayDeleteExpression(ref)) {
229                    changes.delete(sourceFile, ref.parent.parent);
230                }
231            });
232        }
233    }
234
235    function tryDeleteDeclarationWorker(token: Node, changes: textChanges.ChangeTracker, sourceFile: SourceFile, checker: TypeChecker, sourceFiles: readonly SourceFile[], program: Program, cancellationToken: CancellationToken, isFixAll: boolean): void {
236        const { parent } = token;
237        if (isParameter(parent)) {
238            tryDeleteParameter(changes, sourceFile, parent, checker, sourceFiles, program, cancellationToken, isFixAll);
239        }
240        else if (!(isFixAll && isIdentifier(token) && FindAllReferences.Core.isSymbolReferencedInFile(token, checker, sourceFile))) {
241            const node = isImportClause(parent) ? token : isComputedPropertyName(parent) ? parent.parent : parent;
242            Debug.assert(node !== sourceFile, "should not delete whole source file");
243            changes.delete(sourceFile, node);
244        }
245    }
246
247    function tryDeleteParameter(
248        changes: textChanges.ChangeTracker,
249        sourceFile: SourceFile,
250        parameter: ParameterDeclaration,
251        checker: TypeChecker,
252        sourceFiles: readonly SourceFile[],
253        program: Program,
254        cancellationToken: CancellationToken,
255        isFixAll = false): void {
256        if (mayDeleteParameter(checker, sourceFile, parameter, sourceFiles, program, cancellationToken, isFixAll)) {
257            if (parameter.modifiers && parameter.modifiers.length > 0 &&
258                (!isIdentifier(parameter.name) || FindAllReferences.Core.isSymbolReferencedInFile(parameter.name, checker, sourceFile))) {
259                for (const modifier of parameter.modifiers) {
260                    if (isModifier(modifier)) {
261                        changes.deleteModifier(sourceFile, modifier);
262                    }
263                }
264            }
265            else if (!parameter.initializer && isNotProvidedArguments(parameter, checker, sourceFiles)) {
266                changes.delete(sourceFile, parameter);
267            }
268        }
269    }
270
271    function isNotProvidedArguments(parameter: ParameterDeclaration, checker: TypeChecker, sourceFiles: readonly SourceFile[]) {
272        const index = parameter.parent.parameters.indexOf(parameter);
273        // Just in case the call didn't provide enough arguments.
274        return !FindAllReferences.Core.someSignatureUsage(parameter.parent, sourceFiles, checker, (_, call) => !call || call.arguments.length > index);
275    }
276
277    function mayDeleteParameter(checker: TypeChecker, sourceFile: SourceFile, parameter: ParameterDeclaration, sourceFiles: readonly SourceFile[], program: Program, cancellationToken: CancellationToken, isFixAll: boolean): boolean {
278        const { parent } = parameter;
279        switch (parent.kind) {
280            case SyntaxKind.MethodDeclaration:
281            case SyntaxKind.Constructor:
282                const index = parent.parameters.indexOf(parameter);
283                const referent = isMethodDeclaration(parent) ? parent.name : parent;
284                const entries = FindAllReferences.Core.getReferencedSymbolsForNode(parent.pos, referent, program, sourceFiles, cancellationToken);
285                if (entries) {
286                    for (const entry of entries) {
287                        for (const reference of entry.references) {
288                            if (reference.kind === FindAllReferences.EntryKind.Node) {
289                                // argument in super(...)
290                                const isSuperCall = isSuperKeyword(reference.node)
291                                    && isCallExpression(reference.node.parent)
292                                    && reference.node.parent.arguments.length > index;
293                                // argument in super.m(...)
294                                const isSuperMethodCall = isPropertyAccessExpression(reference.node.parent)
295                                    && isSuperKeyword(reference.node.parent.expression)
296                                    && isCallExpression(reference.node.parent.parent)
297                                    && reference.node.parent.parent.arguments.length > index;
298                                // parameter in overridden or overriding method
299                                const isOverriddenMethod = (isMethodDeclaration(reference.node.parent) || isMethodSignature(reference.node.parent))
300                                    && reference.node.parent !== parameter.parent
301                                    && reference.node.parent.parameters.length > index;
302                                if (isSuperCall || isSuperMethodCall || isOverriddenMethod) return false;
303                            }
304                        }
305                    }
306                }
307                return true;
308            case SyntaxKind.FunctionDeclaration: {
309                if (parent.name && isCallbackLike(checker, sourceFile, parent.name)) {
310                    return isLastParameter(parent, parameter, isFixAll);
311                }
312                return true;
313            }
314            case SyntaxKind.FunctionExpression:
315            case SyntaxKind.ArrowFunction:
316                // Can't remove a non-last parameter in a callback. Can remove a parameter in code-fix-all if future parameters are also unused.
317                return isLastParameter(parent, parameter, isFixAll);
318
319            case SyntaxKind.SetAccessor:
320                // Setter must have a parameter
321                return false;
322
323            case SyntaxKind.GetAccessor:
324                // Getter cannot have parameters
325                return true;
326
327            default:
328                return Debug.failBadSyntaxKind(parent);
329        }
330    }
331
332    function isCallbackLike(checker: TypeChecker, sourceFile: SourceFile, name: Identifier): boolean {
333        return !!FindAllReferences.Core.eachSymbolReferenceInFile(name, checker, sourceFile, reference =>
334            isIdentifier(reference) && isCallExpression(reference.parent) && reference.parent.arguments.indexOf(reference) >= 0);
335    }
336
337    function isLastParameter(func: FunctionLikeDeclaration, parameter: ParameterDeclaration, isFixAll: boolean): boolean {
338        const parameters = func.parameters;
339        const index = parameters.indexOf(parameter);
340        Debug.assert(index !== -1, "The parameter should already be in the list");
341        return isFixAll ?
342            parameters.slice(index + 1).every(p => isIdentifier(p.name) && !p.symbol.isReferenced) :
343            index === parameters.length - 1;
344    }
345
346    function mayDeleteExpression(node: Node) {
347        return ((isBinaryExpression(node.parent) && node.parent.left === node) ||
348            ((isPostfixUnaryExpression(node.parent) || isPrefixUnaryExpression(node.parent)) && node.parent.operand === node)) && isExpressionStatement(node.parent.parent);
349    }
350}
351