1/** 2 * @fileoverview Rule to flag declared but unused variables 3 * @author Ilya Volodin 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Typedefs 16//------------------------------------------------------------------------------ 17 18/** 19 * Bag of data used for formatting the `unusedVar` lint message. 20 * @typedef {Object} UnusedVarMessageData 21 * @property {string} varName The name of the unused var. 22 * @property {'defined'|'assigned a value'} action Description of the vars state. 23 * @property {string} additional Any additional info to be appended at the end. 24 */ 25 26//------------------------------------------------------------------------------ 27// Rule Definition 28//------------------------------------------------------------------------------ 29 30module.exports = { 31 meta: { 32 type: "problem", 33 34 docs: { 35 description: "disallow unused variables", 36 category: "Variables", 37 recommended: true, 38 url: "https://eslint.org/docs/rules/no-unused-vars" 39 }, 40 41 schema: [ 42 { 43 oneOf: [ 44 { 45 enum: ["all", "local"] 46 }, 47 { 48 type: "object", 49 properties: { 50 vars: { 51 enum: ["all", "local"] 52 }, 53 varsIgnorePattern: { 54 type: "string" 55 }, 56 args: { 57 enum: ["all", "after-used", "none"] 58 }, 59 ignoreRestSiblings: { 60 type: "boolean" 61 }, 62 argsIgnorePattern: { 63 type: "string" 64 }, 65 caughtErrors: { 66 enum: ["all", "none"] 67 }, 68 caughtErrorsIgnorePattern: { 69 type: "string" 70 } 71 } 72 } 73 ] 74 } 75 ], 76 77 messages: { 78 unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}." 79 } 80 }, 81 82 create(context) { 83 const sourceCode = context.getSourceCode(); 84 85 const REST_PROPERTY_TYPE = /^(?:RestElement|(?:Experimental)?RestProperty)$/u; 86 87 const config = { 88 vars: "all", 89 args: "after-used", 90 ignoreRestSiblings: false, 91 caughtErrors: "none" 92 }; 93 94 const firstOption = context.options[0]; 95 96 if (firstOption) { 97 if (typeof firstOption === "string") { 98 config.vars = firstOption; 99 } else { 100 config.vars = firstOption.vars || config.vars; 101 config.args = firstOption.args || config.args; 102 config.ignoreRestSiblings = firstOption.ignoreRestSiblings || config.ignoreRestSiblings; 103 config.caughtErrors = firstOption.caughtErrors || config.caughtErrors; 104 105 if (firstOption.varsIgnorePattern) { 106 config.varsIgnorePattern = new RegExp(firstOption.varsIgnorePattern, "u"); 107 } 108 109 if (firstOption.argsIgnorePattern) { 110 config.argsIgnorePattern = new RegExp(firstOption.argsIgnorePattern, "u"); 111 } 112 113 if (firstOption.caughtErrorsIgnorePattern) { 114 config.caughtErrorsIgnorePattern = new RegExp(firstOption.caughtErrorsIgnorePattern, "u"); 115 } 116 } 117 } 118 119 /** 120 * Generates the message data about the variable being defined and unused, 121 * including the ignore pattern if configured. 122 * @param {Variable} unusedVar eslint-scope variable object. 123 * @returns {UnusedVarMessageData} The message data to be used with this unused variable. 124 */ 125 function getDefinedMessageData(unusedVar) { 126 const defType = unusedVar.defs && unusedVar.defs[0] && unusedVar.defs[0].type; 127 let type; 128 let pattern; 129 130 if (defType === "CatchClause" && config.caughtErrorsIgnorePattern) { 131 type = "args"; 132 pattern = config.caughtErrorsIgnorePattern.toString(); 133 } else if (defType === "Parameter" && config.argsIgnorePattern) { 134 type = "args"; 135 pattern = config.argsIgnorePattern.toString(); 136 } else if (defType !== "Parameter" && config.varsIgnorePattern) { 137 type = "vars"; 138 pattern = config.varsIgnorePattern.toString(); 139 } 140 141 const additional = type ? `. Allowed unused ${type} must match ${pattern}` : ""; 142 143 return { 144 varName: unusedVar.name, 145 action: "defined", 146 additional 147 }; 148 } 149 150 /** 151 * Generate the warning message about the variable being 152 * assigned and unused, including the ignore pattern if configured. 153 * @param {Variable} unusedVar eslint-scope variable object. 154 * @returns {UnusedVarMessageData} The message data to be used with this unused variable. 155 */ 156 function getAssignedMessageData(unusedVar) { 157 const additional = config.varsIgnorePattern ? `. Allowed unused vars must match ${config.varsIgnorePattern.toString()}` : ""; 158 159 return { 160 varName: unusedVar.name, 161 action: "assigned a value", 162 additional 163 }; 164 } 165 166 //-------------------------------------------------------------------------- 167 // Helpers 168 //-------------------------------------------------------------------------- 169 170 const STATEMENT_TYPE = /(?:Statement|Declaration)$/u; 171 172 /** 173 * Determines if a given variable is being exported from a module. 174 * @param {Variable} variable eslint-scope variable object. 175 * @returns {boolean} True if the variable is exported, false if not. 176 * @private 177 */ 178 function isExported(variable) { 179 180 const definition = variable.defs[0]; 181 182 if (definition) { 183 184 let node = definition.node; 185 186 if (node.type === "VariableDeclarator") { 187 node = node.parent; 188 } else if (definition.type === "Parameter") { 189 return false; 190 } 191 192 return node.parent.type.indexOf("Export") === 0; 193 } 194 return false; 195 196 } 197 198 /** 199 * Determines if a variable has a sibling rest property 200 * @param {Variable} variable eslint-scope variable object. 201 * @returns {boolean} True if the variable is exported, false if not. 202 * @private 203 */ 204 function hasRestSpreadSibling(variable) { 205 if (config.ignoreRestSiblings) { 206 return variable.defs.some(def => { 207 const propertyNode = def.name.parent; 208 const patternNode = propertyNode.parent; 209 210 return ( 211 propertyNode.type === "Property" && 212 patternNode.type === "ObjectPattern" && 213 REST_PROPERTY_TYPE.test(patternNode.properties[patternNode.properties.length - 1].type) 214 ); 215 }); 216 } 217 218 return false; 219 } 220 221 /** 222 * Determines if a reference is a read operation. 223 * @param {Reference} ref An eslint-scope Reference 224 * @returns {boolean} whether the given reference represents a read operation 225 * @private 226 */ 227 function isReadRef(ref) { 228 return ref.isRead(); 229 } 230 231 /** 232 * Determine if an identifier is referencing an enclosing function name. 233 * @param {Reference} ref The reference to check. 234 * @param {ASTNode[]} nodes The candidate function nodes. 235 * @returns {boolean} True if it's a self-reference, false if not. 236 * @private 237 */ 238 function isSelfReference(ref, nodes) { 239 let scope = ref.from; 240 241 while (scope) { 242 if (nodes.indexOf(scope.block) >= 0) { 243 return true; 244 } 245 246 scope = scope.upper; 247 } 248 249 return false; 250 } 251 252 /** 253 * Gets a list of function definitions for a specified variable. 254 * @param {Variable} variable eslint-scope variable object. 255 * @returns {ASTNode[]} Function nodes. 256 * @private 257 */ 258 function getFunctionDefinitions(variable) { 259 const functionDefinitions = []; 260 261 variable.defs.forEach(def => { 262 const { type, node } = def; 263 264 // FunctionDeclarations 265 if (type === "FunctionName") { 266 functionDefinitions.push(node); 267 } 268 269 // FunctionExpressions 270 if (type === "Variable" && node.init && 271 (node.init.type === "FunctionExpression" || node.init.type === "ArrowFunctionExpression")) { 272 functionDefinitions.push(node.init); 273 } 274 }); 275 return functionDefinitions; 276 } 277 278 /** 279 * Checks the position of given nodes. 280 * @param {ASTNode} inner A node which is expected as inside. 281 * @param {ASTNode} outer A node which is expected as outside. 282 * @returns {boolean} `true` if the `inner` node exists in the `outer` node. 283 * @private 284 */ 285 function isInside(inner, outer) { 286 return ( 287 inner.range[0] >= outer.range[0] && 288 inner.range[1] <= outer.range[1] 289 ); 290 } 291 292 /** 293 * If a given reference is left-hand side of an assignment, this gets 294 * the right-hand side node of the assignment. 295 * 296 * In the following cases, this returns null. 297 * 298 * - The reference is not the LHS of an assignment expression. 299 * - The reference is inside of a loop. 300 * - The reference is inside of a function scope which is different from 301 * the declaration. 302 * @param {eslint-scope.Reference} ref A reference to check. 303 * @param {ASTNode} prevRhsNode The previous RHS node. 304 * @returns {ASTNode|null} The RHS node or null. 305 * @private 306 */ 307 function getRhsNode(ref, prevRhsNode) { 308 const id = ref.identifier; 309 const parent = id.parent; 310 const grandparent = parent.parent; 311 const refScope = ref.from.variableScope; 312 const varScope = ref.resolved.scope.variableScope; 313 const canBeUsedLater = refScope !== varScope || astUtils.isInLoop(id); 314 315 /* 316 * Inherits the previous node if this reference is in the node. 317 * This is for `a = a + a`-like code. 318 */ 319 if (prevRhsNode && isInside(id, prevRhsNode)) { 320 return prevRhsNode; 321 } 322 323 if (parent.type === "AssignmentExpression" && 324 grandparent.type === "ExpressionStatement" && 325 id === parent.left && 326 !canBeUsedLater 327 ) { 328 return parent.right; 329 } 330 return null; 331 } 332 333 /** 334 * Checks whether a given function node is stored to somewhere or not. 335 * If the function node is stored, the function can be used later. 336 * @param {ASTNode} funcNode A function node to check. 337 * @param {ASTNode} rhsNode The RHS node of the previous assignment. 338 * @returns {boolean} `true` if under the following conditions: 339 * - the funcNode is assigned to a variable. 340 * - the funcNode is bound as an argument of a function call. 341 * - the function is bound to a property and the object satisfies above conditions. 342 * @private 343 */ 344 function isStorableFunction(funcNode, rhsNode) { 345 let node = funcNode; 346 let parent = funcNode.parent; 347 348 while (parent && isInside(parent, rhsNode)) { 349 switch (parent.type) { 350 case "SequenceExpression": 351 if (parent.expressions[parent.expressions.length - 1] !== node) { 352 return false; 353 } 354 break; 355 356 case "CallExpression": 357 case "NewExpression": 358 return parent.callee !== node; 359 360 case "AssignmentExpression": 361 case "TaggedTemplateExpression": 362 case "YieldExpression": 363 return true; 364 365 default: 366 if (STATEMENT_TYPE.test(parent.type)) { 367 368 /* 369 * If it encountered statements, this is a complex pattern. 370 * Since analyzing complex patterns is hard, this returns `true` to avoid false positive. 371 */ 372 return true; 373 } 374 } 375 376 node = parent; 377 parent = parent.parent; 378 } 379 380 return false; 381 } 382 383 /** 384 * Checks whether a given Identifier node exists inside of a function node which can be used later. 385 * 386 * "can be used later" means: 387 * - the function is assigned to a variable. 388 * - the function is bound to a property and the object can be used later. 389 * - the function is bound as an argument of a function call. 390 * 391 * If a reference exists in a function which can be used later, the reference is read when the function is called. 392 * @param {ASTNode} id An Identifier node to check. 393 * @param {ASTNode} rhsNode The RHS node of the previous assignment. 394 * @returns {boolean} `true` if the `id` node exists inside of a function node which can be used later. 395 * @private 396 */ 397 function isInsideOfStorableFunction(id, rhsNode) { 398 const funcNode = astUtils.getUpperFunction(id); 399 400 return ( 401 funcNode && 402 isInside(funcNode, rhsNode) && 403 isStorableFunction(funcNode, rhsNode) 404 ); 405 } 406 407 /** 408 * Checks whether a given reference is a read to update itself or not. 409 * @param {eslint-scope.Reference} ref A reference to check. 410 * @param {ASTNode} rhsNode The RHS node of the previous assignment. 411 * @returns {boolean} The reference is a read to update itself. 412 * @private 413 */ 414 function isReadForItself(ref, rhsNode) { 415 const id = ref.identifier; 416 const parent = id.parent; 417 const grandparent = parent.parent; 418 419 return ref.isRead() && ( 420 421 // self update. e.g. `a += 1`, `a++` 422 (// in RHS of an assignment for itself. e.g. `a = a + 1` 423 (( 424 parent.type === "AssignmentExpression" && 425 grandparent.type === "ExpressionStatement" && 426 parent.left === id 427 ) || 428 ( 429 parent.type === "UpdateExpression" && 430 grandparent.type === "ExpressionStatement" 431 ) || rhsNode && 432 isInside(id, rhsNode) && 433 !isInsideOfStorableFunction(id, rhsNode))) 434 ); 435 } 436 437 /** 438 * Determine if an identifier is used either in for-in loops. 439 * @param {Reference} ref The reference to check. 440 * @returns {boolean} whether reference is used in the for-in loops 441 * @private 442 */ 443 function isForInRef(ref) { 444 let target = ref.identifier.parent; 445 446 447 // "for (var ...) { return; }" 448 if (target.type === "VariableDeclarator") { 449 target = target.parent.parent; 450 } 451 452 if (target.type !== "ForInStatement") { 453 return false; 454 } 455 456 // "for (...) { return; }" 457 if (target.body.type === "BlockStatement") { 458 target = target.body.body[0]; 459 460 // "for (...) return;" 461 } else { 462 target = target.body; 463 } 464 465 // For empty loop body 466 if (!target) { 467 return false; 468 } 469 470 return target.type === "ReturnStatement"; 471 } 472 473 /** 474 * Determines if the variable is used. 475 * @param {Variable} variable The variable to check. 476 * @returns {boolean} True if the variable is used 477 * @private 478 */ 479 function isUsedVariable(variable) { 480 const functionNodes = getFunctionDefinitions(variable), 481 isFunctionDefinition = functionNodes.length > 0; 482 let rhsNode = null; 483 484 return variable.references.some(ref => { 485 if (isForInRef(ref)) { 486 return true; 487 } 488 489 const forItself = isReadForItself(ref, rhsNode); 490 491 rhsNode = getRhsNode(ref, rhsNode); 492 493 return ( 494 isReadRef(ref) && 495 !forItself && 496 !(isFunctionDefinition && isSelfReference(ref, functionNodes)) 497 ); 498 }); 499 } 500 501 /** 502 * Checks whether the given variable is after the last used parameter. 503 * @param {eslint-scope.Variable} variable The variable to check. 504 * @returns {boolean} `true` if the variable is defined after the last 505 * used parameter. 506 */ 507 function isAfterLastUsedArg(variable) { 508 const def = variable.defs[0]; 509 const params = context.getDeclaredVariables(def.node); 510 const posteriorParams = params.slice(params.indexOf(variable) + 1); 511 512 // If any used parameters occur after this parameter, do not report. 513 return !posteriorParams.some(v => v.references.length > 0 || v.eslintUsed); 514 } 515 516 /** 517 * Gets an array of variables without read references. 518 * @param {Scope} scope an eslint-scope Scope object. 519 * @param {Variable[]} unusedVars an array that saving result. 520 * @returns {Variable[]} unused variables of the scope and descendant scopes. 521 * @private 522 */ 523 function collectUnusedVariables(scope, unusedVars) { 524 const variables = scope.variables; 525 const childScopes = scope.childScopes; 526 let i, l; 527 528 if (scope.type !== "global" || config.vars === "all") { 529 for (i = 0, l = variables.length; i < l; ++i) { 530 const variable = variables[i]; 531 532 // skip a variable of class itself name in the class scope 533 if (scope.type === "class" && scope.block.id === variable.identifiers[0]) { 534 continue; 535 } 536 537 // skip function expression names and variables marked with markVariableAsUsed() 538 if (scope.functionExpressionScope || variable.eslintUsed) { 539 continue; 540 } 541 542 // skip implicit "arguments" variable 543 if (scope.type === "function" && variable.name === "arguments" && variable.identifiers.length === 0) { 544 continue; 545 } 546 547 // explicit global variables don't have definitions. 548 const def = variable.defs[0]; 549 550 if (def) { 551 const type = def.type; 552 553 // skip catch variables 554 if (type === "CatchClause") { 555 if (config.caughtErrors === "none") { 556 continue; 557 } 558 559 // skip ignored parameters 560 if (config.caughtErrorsIgnorePattern && config.caughtErrorsIgnorePattern.test(def.name.name)) { 561 continue; 562 } 563 } 564 565 if (type === "Parameter") { 566 567 // skip any setter argument 568 if ((def.node.parent.type === "Property" || def.node.parent.type === "MethodDefinition") && def.node.parent.kind === "set") { 569 continue; 570 } 571 572 // if "args" option is "none", skip any parameter 573 if (config.args === "none") { 574 continue; 575 } 576 577 // skip ignored parameters 578 if (config.argsIgnorePattern && config.argsIgnorePattern.test(def.name.name)) { 579 continue; 580 } 581 582 // if "args" option is "after-used", skip used variables 583 if (config.args === "after-used" && astUtils.isFunction(def.name.parent) && !isAfterLastUsedArg(variable)) { 584 continue; 585 } 586 } else { 587 588 // skip ignored variables 589 if (config.varsIgnorePattern && config.varsIgnorePattern.test(def.name.name)) { 590 continue; 591 } 592 } 593 } 594 595 if (!isUsedVariable(variable) && !isExported(variable) && !hasRestSpreadSibling(variable)) { 596 unusedVars.push(variable); 597 } 598 } 599 } 600 601 for (i = 0, l = childScopes.length; i < l; ++i) { 602 collectUnusedVariables(childScopes[i], unusedVars); 603 } 604 605 return unusedVars; 606 } 607 608 //-------------------------------------------------------------------------- 609 // Public 610 //-------------------------------------------------------------------------- 611 612 return { 613 "Program:exit"(programNode) { 614 const unusedVars = collectUnusedVariables(context.getScope(), []); 615 616 for (let i = 0, l = unusedVars.length; i < l; ++i) { 617 const unusedVar = unusedVars[i]; 618 619 // Report the first declaration. 620 if (unusedVar.defs.length > 0) { 621 context.report({ 622 node: unusedVar.references.length ? unusedVar.references[ 623 unusedVar.references.length - 1 624 ].identifier : unusedVar.identifiers[0], 625 messageId: "unusedVar", 626 data: unusedVar.references.some(ref => ref.isWrite()) 627 ? getAssignedMessageData(unusedVar) 628 : getDefinedMessageData(unusedVar) 629 }); 630 631 // If there are no regular declaration, report the first `/*globals*/` comment directive. 632 } else if (unusedVar.eslintExplicitGlobalComments) { 633 const directiveComment = unusedVar.eslintExplicitGlobalComments[0]; 634 635 context.report({ 636 node: programNode, 637 loc: astUtils.getNameLocationInGlobalDirectiveComment(sourceCode, directiveComment, unusedVar.name), 638 messageId: "unusedVar", 639 data: getDefinedMessageData(unusedVar) 640 }); 641 } 642 } 643 } 644 }; 645 646 } 647}; 648