1import { 2 AST_NODE_TYPES, 3 TSESLint, 4 TSESTree, 5} from '@typescript-eslint/experimental-utils'; 6import { PatternVisitor } from '@typescript-eslint/scope-manager'; 7import baseRule from 'eslint/lib/rules/no-unused-vars'; 8import * as util from '../util'; 9 10type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>; 11type Options = util.InferOptionsTypeFromRule<typeof baseRule>; 12 13export default util.createRule<Options, MessageIds>({ 14 name: 'no-unused-vars', 15 meta: { 16 type: 'problem', 17 docs: { 18 description: 'Disallow unused variables', 19 category: 'Variables', 20 recommended: 'warn', 21 extendsBaseRule: true, 22 }, 23 schema: baseRule.meta.schema, 24 messages: baseRule.meta.messages ?? { 25 unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.", 26 }, 27 }, 28 defaultOptions: [{}], 29 create(context) { 30 const rules = baseRule.create(context); 31 const filename = context.getFilename(); 32 const MODULE_DECL_CACHE = new Map<TSESTree.TSModuleDeclaration, boolean>(); 33 34 /** 35 * Gets a list of TS module definitions for a specified variable. 36 * @param variable eslint-scope variable object. 37 */ 38 function getModuleNameDeclarations( 39 variable: TSESLint.Scope.Variable, 40 ): TSESTree.TSModuleDeclaration[] { 41 const moduleDeclarations: TSESTree.TSModuleDeclaration[] = []; 42 43 variable.defs.forEach(def => { 44 if (def.type === 'TSModuleName') { 45 moduleDeclarations.push(def.node); 46 } 47 }); 48 49 return moduleDeclarations; 50 } 51 52 /** 53 * Determine if an identifier is referencing an enclosing name. 54 * This only applies to declarations that create their own scope (modules, functions, classes) 55 * @param ref The reference to check. 56 * @param nodes The candidate function nodes. 57 * @returns True if it's a self-reference, false if not. 58 */ 59 function isBlockSelfReference( 60 ref: TSESLint.Scope.Reference, 61 nodes: TSESTree.Node[], 62 ): boolean { 63 let scope: TSESLint.Scope.Scope | null = ref.from; 64 65 while (scope) { 66 if (nodes.indexOf(scope.block) >= 0) { 67 return true; 68 } 69 70 scope = scope.upper; 71 } 72 73 return false; 74 } 75 76 function isExported( 77 variable: TSESLint.Scope.Variable, 78 target: AST_NODE_TYPES, 79 ): boolean { 80 // TS will require that all merged namespaces/interfaces are exported, so we only need to find one 81 return variable.defs.some( 82 def => 83 def.node.type === target && 84 (def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration || 85 def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration), 86 ); 87 } 88 89 return { 90 ...rules, 91 'TSCallSignatureDeclaration, TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, TSFunctionType, TSMethodSignature'( 92 node: 93 | TSESTree.TSCallSignatureDeclaration 94 | TSESTree.TSConstructorType 95 | TSESTree.TSConstructSignatureDeclaration 96 | TSESTree.TSDeclareFunction 97 | TSESTree.TSEmptyBodyFunctionExpression 98 | TSESTree.TSFunctionType 99 | TSESTree.TSMethodSignature, 100 ): void { 101 // function type signature params create variables because they can be referenced within the signature, 102 // but they obviously aren't unused variables for the purposes of this rule. 103 for (const param of node.params) { 104 visitPattern(param, name => { 105 context.markVariableAsUsed(name.name); 106 }); 107 } 108 }, 109 TSEnumDeclaration(): void { 110 // enum members create variables because they can be referenced within the enum, 111 // but they obviously aren't unused variables for the purposes of this rule. 112 const scope = context.getScope(); 113 for (const variable of scope.variables) { 114 context.markVariableAsUsed(variable.name); 115 } 116 }, 117 TSMappedType(node): void { 118 // mapped types create a variable for their type name, but it's not necessary to reference it, 119 // so we shouldn't consider it as unused for the purpose of this rule. 120 context.markVariableAsUsed(node.typeParameter.name.name); 121 }, 122 TSModuleDeclaration(): void { 123 const childScope = context.getScope(); 124 const scope = util.nullThrows( 125 context.getScope().upper, 126 util.NullThrowsReasons.MissingToken(childScope.type, 'upper scope'), 127 ); 128 for (const variable of scope.variables) { 129 const moduleNodes = getModuleNameDeclarations(variable); 130 131 if ( 132 moduleNodes.length === 0 || 133 // ignore unreferenced module definitions, as the base rule will report on them 134 variable.references.length === 0 || 135 // ignore exported nodes 136 isExported(variable, AST_NODE_TYPES.TSModuleDeclaration) 137 ) { 138 continue; 139 } 140 141 // check if the only reference to a module's name is a self-reference in its body 142 // this won't be caught by the base rule because it doesn't understand TS modules 143 const isOnlySelfReferenced = variable.references.every(ref => { 144 return isBlockSelfReference(ref, moduleNodes); 145 }); 146 147 if (isOnlySelfReferenced) { 148 context.report({ 149 node: variable.identifiers[0], 150 messageId: 'unusedVar', 151 data: { 152 varName: variable.name, 153 action: 'defined', 154 additional: '', 155 }, 156 }); 157 } 158 } 159 }, 160 [[ 161 'TSParameterProperty > AssignmentPattern > Identifier.left', 162 'TSParameterProperty > Identifier.parameter', 163 ].join(', ')](node: TSESTree.Identifier): void { 164 // just assume parameter properties are used as property usage tracking is beyond the scope of this rule 165 context.markVariableAsUsed(node.name); 166 }, 167 ':matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression) > Identifier[name="this"].params'( 168 node: TSESTree.Identifier, 169 ): void { 170 // this parameters should always be considered used as they're pseudo-parameters 171 context.markVariableAsUsed(node.name); 172 }, 173 'TSInterfaceDeclaration, TSTypeAliasDeclaration'( 174 node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration, 175 ): void { 176 const variable = context.getScope().set.get(node.id.name); 177 if (!variable) { 178 return; 179 } 180 if ( 181 variable.references.length === 0 || 182 // ignore exported nodes 183 isExported(variable, node.type) 184 ) { 185 return; 186 } 187 188 // check if the type is only self-referenced 189 // this won't be caught by the base rule because it doesn't understand self-referencing types 190 const isOnlySelfReferenced = variable.references.every(ref => { 191 if ( 192 ref.identifier.range[0] >= node.range[0] && 193 ref.identifier.range[1] <= node.range[1] 194 ) { 195 return true; 196 } 197 return false; 198 }); 199 if (isOnlySelfReferenced) { 200 context.report({ 201 node: variable.identifiers[0], 202 messageId: 'unusedVar', 203 data: { 204 varName: variable.name, 205 action: 'defined', 206 additional: '', 207 }, 208 }); 209 } 210 }, 211 212 // declaration file handling 213 [ambientDeclarationSelector(AST_NODE_TYPES.Program, true)]( 214 node: DeclarationSelectorNode, 215 ): void { 216 if (!util.isDefinitionFile(filename)) { 217 return; 218 } 219 markDeclarationChildAsUsed(node); 220 }, 221 222 // global augmentation can be in any file, and they do not need exports 223 'TSModuleDeclaration[declare = true][global = true]'(): void { 224 context.markVariableAsUsed('global'); 225 }, 226 227 // children of a namespace that is a child of a declared namespace are auto-exported 228 [ambientDeclarationSelector( 229 'TSModuleDeclaration[declare = true] > TSModuleBlock TSModuleDeclaration > TSModuleBlock', 230 false, 231 )](node: DeclarationSelectorNode): void { 232 markDeclarationChildAsUsed(node); 233 }, 234 235 // declared namespace handling 236 [ambientDeclarationSelector( 237 'TSModuleDeclaration[declare = true] > TSModuleBlock', 238 false, 239 )](node: DeclarationSelectorNode): void { 240 const moduleDecl = util.nullThrows( 241 node.parent?.parent, 242 util.NullThrowsReasons.MissingParent, 243 ) as TSESTree.TSModuleDeclaration; 244 245 // declared ambient modules with an `export =` statement will only export that one thing 246 // all other statements are not automatically exported in this case 247 if ( 248 moduleDecl.id.type === AST_NODE_TYPES.Literal && 249 checkModuleDeclForExportEquals(moduleDecl) 250 ) { 251 return; 252 } 253 254 markDeclarationChildAsUsed(node); 255 }, 256 }; 257 258 function checkModuleDeclForExportEquals( 259 node: TSESTree.TSModuleDeclaration, 260 ): boolean { 261 const cached = MODULE_DECL_CACHE.get(node); 262 if (cached != null) { 263 return cached; 264 } 265 266 for (const statement of node.body?.body ?? []) { 267 if (statement.type === AST_NODE_TYPES.TSExportAssignment) { 268 MODULE_DECL_CACHE.set(node, true); 269 return true; 270 } 271 } 272 273 MODULE_DECL_CACHE.set(node, false); 274 return false; 275 } 276 277 type DeclarationSelectorNode = 278 | TSESTree.TSInterfaceDeclaration 279 | TSESTree.TSTypeAliasDeclaration 280 | TSESTree.ClassDeclaration 281 | TSESTree.FunctionDeclaration 282 | TSESTree.TSDeclareFunction 283 | TSESTree.TSEnumDeclaration 284 | TSESTree.TSModuleDeclaration 285 | TSESTree.VariableDeclaration; 286 function ambientDeclarationSelector( 287 parent: string, 288 childDeclare: boolean, 289 ): string { 290 return [ 291 // Types are ambiently exported 292 `${parent} > :matches(${[ 293 AST_NODE_TYPES.TSInterfaceDeclaration, 294 AST_NODE_TYPES.TSTypeAliasDeclaration, 295 ].join(', ')})`, 296 // Value things are ambiently exported if they are "declare"d 297 `${parent} > :matches(${[ 298 AST_NODE_TYPES.ClassDeclaration, 299 AST_NODE_TYPES.TSDeclareFunction, 300 AST_NODE_TYPES.TSEnumDeclaration, 301 AST_NODE_TYPES.TSModuleDeclaration, 302 AST_NODE_TYPES.VariableDeclaration, 303 ].join(', ')})${childDeclare ? '[declare = true]' : ''}`, 304 ].join(', '); 305 } 306 function markDeclarationChildAsUsed(node: DeclarationSelectorNode): void { 307 const identifiers: TSESTree.Identifier[] = []; 308 switch (node.type) { 309 case AST_NODE_TYPES.TSInterfaceDeclaration: 310 case AST_NODE_TYPES.TSTypeAliasDeclaration: 311 case AST_NODE_TYPES.ClassDeclaration: 312 case AST_NODE_TYPES.FunctionDeclaration: 313 case AST_NODE_TYPES.TSDeclareFunction: 314 case AST_NODE_TYPES.TSEnumDeclaration: 315 case AST_NODE_TYPES.TSModuleDeclaration: 316 if (node.id?.type === AST_NODE_TYPES.Identifier) { 317 identifiers.push(node.id); 318 } 319 break; 320 321 case AST_NODE_TYPES.VariableDeclaration: 322 for (const declaration of node.declarations) { 323 visitPattern(declaration, pattern => { 324 identifiers.push(pattern); 325 }); 326 } 327 break; 328 } 329 330 let scope = context.getScope(); 331 const shouldUseUpperScope = [ 332 AST_NODE_TYPES.TSModuleDeclaration, 333 AST_NODE_TYPES.TSDeclareFunction, 334 ].includes(node.type); 335 336 if (scope.variableScope !== scope) { 337 scope = scope.variableScope; 338 } else if (shouldUseUpperScope && scope.upper) { 339 scope = scope.upper; 340 } 341 342 for (const id of identifiers) { 343 const superVar = scope.set.get(id.name); 344 if (superVar) { 345 superVar.eslintUsed = true; 346 } 347 } 348 } 349 350 function visitPattern( 351 node: TSESTree.Node, 352 cb: (node: TSESTree.Identifier) => void, 353 ): void { 354 const visitor = new PatternVisitor({}, node, cb); 355 visitor.visit(node); 356 } 357 }, 358}); 359 360/* 361 362###### TODO ###### 363 364Edge cases that aren't currently handled due to laziness and them being super edgy edge cases 365 366 367--- function params referenced in typeof type refs in the function declaration --- 368--- NOTE - TS gets these cases wrong 369 370function _foo( 371 arg: number // arg should be unused 372): typeof arg { 373 return 1 as any; 374} 375 376function _bar( 377 arg: number, // arg should be unused 378 _arg2: typeof arg, 379) {} 380 381 382--- function names referenced in typeof type refs in the function declaration --- 383--- NOTE - TS gets these cases right 384 385function foo( // foo should be unused 386): typeof foo { 387 return 1 as any; 388} 389 390function bar( // bar should be unused 391 _arg: typeof bar 392) {} 393 394*/ 395 396/* 397 398###### TODO ###### 399 400We currently extend base `no-unused-vars` implementation because it's easier and lighter-weight. 401 402Because of this, there are a few false-negatives which won't get caught. 403We could fix these if we fork the base rule; but that's a lot of code (~650 lines) to add in. 404I didn't want to do that just yet without some real-world issues, considering these are pretty rare edge-cases. 405 406These cases are mishandled because the base rule assumes that each variable has one def, but type-value shadowing 407creates a variable with two defs 408 409--- type-only or value-only references to type/value shadowed variables --- 410--- NOTE - TS gets these cases wrong 411 412type T = 1; 413const T = 2; // this T should be unused 414 415type U = T; // this U should be unused 416const U = 3; 417 418const _V = U; 419 420 421--- partially exported type/value shadowed variables --- 422--- NOTE - TS gets these cases wrong 423 424export interface Foo {} 425const Foo = 1; // this Foo should be unused 426 427interface Bar {} // this Bar should be unused 428export const Bar = 1; 429 430*/ 431