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