2 * @fileoverview Rule to flag declared but unused variables
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
23 description: "disallow unused variables",
24 category: "Variables",
26 url: "https://eslint.org/docs/rules/no-unused-vars"
33 enum: ["all", "local"]
39 enum: ["all", "local"]
45 enum: ["all", "after-used", "none"]
56 caughtErrorsIgnorePattern: {
67 const sourceCode = context.getSourceCode();
69 const REST_PROPERTY_TYPE = /^(?:RestElement|(?:Experimental)?RestProperty)$/u;
74 ignoreRestSiblings: false,
78 const firstOption = context.options[0];
81 if (typeof firstOption === "string") {
82 config.vars = firstOption;
84 config.vars = firstOption.vars || config.vars;
85 config.args = firstOption.args || config.args;
86 config.ignoreRestSiblings = firstOption.ignoreRestSiblings || config.ignoreRestSiblings;
87 config.caughtErrors = firstOption.caughtErrors || config.caughtErrors;
89 if (firstOption.varsIgnorePattern) {
90 config.varsIgnorePattern = new RegExp(firstOption.varsIgnorePattern, "u");
93 if (firstOption.argsIgnorePattern) {
94 config.argsIgnorePattern = new RegExp(firstOption.argsIgnorePattern, "u");
97 if (firstOption.caughtErrorsIgnorePattern) {
98 config.caughtErrorsIgnorePattern = new RegExp(firstOption.caughtErrorsIgnorePattern, "u");
104 * Generate the warning message about the variable being
105 * defined and unused, including the ignore pattern if configured.
106 * @param {Variable} unusedVar eslint-scope variable object.
107 * @returns {string} The warning message to be used with this unused variable.
109 function getDefinedMessage(unusedVar) {
110 const defType = unusedVar.defs && unusedVar.defs[0] && unusedVar.defs[0].type;
114 if (defType === "CatchClause" && config.caughtErrorsIgnorePattern) {
116 pattern = config.caughtErrorsIgnorePattern.toString();
117 } else if (defType === "Parameter" && config.argsIgnorePattern) {
119 pattern = config.argsIgnorePattern.toString();
120 } else if (defType !== "Parameter" && config.varsIgnorePattern) {
122 pattern = config.varsIgnorePattern.toString();
125 const additional = type ? ` Allowed unused ${type} must match ${pattern}.` : "";
127 return `'{{name}}' is defined but never used.${additional}`;
131 * Generate the warning message about the variable being
132 * assigned and unused, including the ignore pattern if configured.
133 * @returns {string} The warning message to be used with this unused variable.
135 function getAssignedMessage() {
136 const additional = config.varsIgnorePattern ? ` Allowed unused vars must match ${config.varsIgnorePattern.toString()}.` : "";
138 return `'{{name}}' is assigned a value but never used.${additional}`;
141 //--------------------------------------------------------------------------
143 //--------------------------------------------------------------------------
145 const STATEMENT_TYPE = /(?:Statement|Declaration)$/u;
148 * Determines if a given variable is being exported from a module.
149 * @param {Variable} variable eslint-scope variable object.
150 * @returns {boolean} True if the variable is exported, false if not.
153 function isExported(variable) {
155 const definition = variable.defs[0];
159 let node = definition.node;
161 if (node.type === "VariableDeclarator") {
163 } else if (definition.type === "Parameter") {
167 return node.parent.type.indexOf("Export") === 0;
174 * Determines if a variable has a sibling rest property
175 * @param {Variable} variable eslint-scope variable object.
176 * @returns {boolean} True if the variable is exported, false if not.
179 function hasRestSpreadSibling(variable) {
180 if (config.ignoreRestSiblings) {
181 return variable.defs.some(def => {
182 const propertyNode = def.name.parent;
183 const patternNode = propertyNode.parent;
186 propertyNode.type === "Property" &&
187 patternNode.type === "ObjectPattern" &&
188 REST_PROPERTY_TYPE.test(patternNode.properties[patternNode.properties.length - 1].type)
197 * Determines if a reference is a read operation.
198 * @param {Reference} ref An eslint-scope Reference
199 * @returns {boolean} whether the given reference represents a read operation
202 function isReadRef(ref) {
207 * Determine if an identifier is referencing an enclosing function name.
208 * @param {Reference} ref The reference to check.
209 * @param {ASTNode[]} nodes The candidate function nodes.
210 * @returns {boolean} True if it's a self-reference, false if not.
213 function isSelfReference(ref, nodes) {
214 let scope = ref.from;
217 if (nodes.indexOf(scope.block) >= 0) {
228 * Gets a list of function definitions for a specified variable.
229 * @param {Variable} variable eslint-scope variable object.
230 * @returns {ASTNode[]} Function nodes.
233 function getFunctionDefinitions(variable) {
234 const functionDefinitions = [];
236 variable.defs.forEach(def => {
237 const { type, node } = def;
239 // FunctionDeclarations
240 if (type === "FunctionName") {
241 functionDefinitions.push(node);
244 // FunctionExpressions
245 if (type === "Variable" && node.init &&
246 (node.init.type === "FunctionExpression" || node.init.type === "ArrowFunctionExpression")) {
247 functionDefinitions.push(node.init);
250 return functionDefinitions;
254 * Checks the position of given nodes.
255 * @param {ASTNode} inner A node which is expected as inside.
256 * @param {ASTNode} outer A node which is expected as outside.
257 * @returns {boolean} `true` if the `inner` node exists in the `outer` node.
260 function isInside(inner, outer) {
262 inner.range[0] >= outer.range[0] &&
263 inner.range[1] <= outer.range[1]
268 * If a given reference is left-hand side of an assignment, this gets
269 * the right-hand side node of the assignment.
271 * In the following cases, this returns null.
273 * - The reference is not the LHS of an assignment expression.
274 * - The reference is inside of a loop.
275 * - The reference is inside of a function scope which is different from
277 * @param {eslint-scope.Reference} ref A reference to check.
278 * @param {ASTNode} prevRhsNode The previous RHS node.
279 * @returns {ASTNode|null} The RHS node or null.
282 function getRhsNode(ref, prevRhsNode) {
283 const id = ref.identifier;
284 const parent = id.parent;
285 const granpa = parent.parent;
286 const refScope = ref.from.variableScope;
287 const varScope = ref.resolved.scope.variableScope;
288 const canBeUsedLater = refScope !== varScope || astUtils.isInLoop(id);
291 * Inherits the previous node if this reference is in the node.
292 * This is for `a = a + a`-like code.
294 if (prevRhsNode && isInside(id, prevRhsNode)) {
298 if (parent.type === "AssignmentExpression" &&
299 granpa.type === "ExpressionStatement" &&
300 id === parent.left &&
309 * Checks whether a given function node is stored to somewhere or not.
310 * If the function node is stored, the function can be used later.
311 * @param {ASTNode} funcNode A function node to check.
312 * @param {ASTNode} rhsNode The RHS node of the previous assignment.
313 * @returns {boolean} `true` if under the following conditions:
314 * - the funcNode is assigned to a variable.
315 * - the funcNode is bound as an argument of a function call.
316 * - the function is bound to a property and the object satisfies above conditions.
319 function isStorableFunction(funcNode, rhsNode) {
321 let parent = funcNode.parent;
323 while (parent && isInside(parent, rhsNode)) {
324 switch (parent.type) {
325 case "SequenceExpression":
326 if (parent.expressions[parent.expressions.length - 1] !== node) {
331 case "CallExpression":
332 case "NewExpression":
333 return parent.callee !== node;
335 case "AssignmentExpression":
336 case "TaggedTemplateExpression":
337 case "YieldExpression":
341 if (STATEMENT_TYPE.test(parent.type)) {
344 * If it encountered statements, this is a complex pattern.
345 * Since analyzeing complex patterns is hard, this returns `true` to avoid false positive.
352 parent = parent.parent;
359 * Checks whether a given Identifier node exists inside of a function node which can be used later.
361 * "can be used later" means:
362 * - the function is assigned to a variable.
363 * - the function is bound to a property and the object can be used later.
364 * - the function is bound as an argument of a function call.
366 * If a reference exists in a function which can be used later, the reference is read when the function is called.
367 * @param {ASTNode} id An Identifier node to check.
368 * @param {ASTNode} rhsNode The RHS node of the previous assignment.
369 * @returns {boolean} `true` if the `id` node exists inside of a function node which can be used later.
372 function isInsideOfStorableFunction(id, rhsNode) {
373 const funcNode = astUtils.getUpperFunction(id);
377 isInside(funcNode, rhsNode) &&
378 isStorableFunction(funcNode, rhsNode)
383 * Checks whether a given reference is a read to update itself or not.
384 * @param {eslint-scope.Reference} ref A reference to check.
385 * @param {ASTNode} rhsNode The RHS node of the previous assignment.
386 * @returns {boolean} The reference is a read to update itself.
389 function isReadForItself(ref, rhsNode) {
390 const id = ref.identifier;
391 const parent = id.parent;
392 const granpa = parent.parent;
394 return ref.isRead() && (
396 // self update. e.g. `a += 1`, `a++`
397 (// in RHS of an assignment for itself. e.g. `a = a + 1`
399 parent.type === "AssignmentExpression" &&
400 granpa.type === "ExpressionStatement" &&
404 parent.type === "UpdateExpression" &&
405 granpa.type === "ExpressionStatement"
407 isInside(id, rhsNode) &&
408 !isInsideOfStorableFunction(id, rhsNode)))
413 * Determine if an identifier is used either in for-in loops.
414 * @param {Reference} ref The reference to check.
415 * @returns {boolean} whether reference is used in the for-in loops
418 function isForInRef(ref) {
419 let target = ref.identifier.parent;
422 // "for (var ...) { return; }"
423 if (target.type === "VariableDeclarator") {
424 target = target.parent.parent;
427 if (target.type !== "ForInStatement") {
431 // "for (...) { return; }"
432 if (target.body.type === "BlockStatement") {
433 target = target.body.body[0];
435 // "for (...) return;"
437 target = target.body;
440 // For empty loop body
445 return target.type === "ReturnStatement";
449 * Determines if the variable is used.
450 * @param {Variable} variable The variable to check.
451 * @returns {boolean} True if the variable is used
454 function isUsedVariable(variable) {
455 const functionNodes = getFunctionDefinitions(variable),
456 isFunctionDefinition = functionNodes.length > 0;
459 return variable.references.some(ref => {
460 if (isForInRef(ref)) {
464 const forItself = isReadForItself(ref, rhsNode);
466 rhsNode = getRhsNode(ref, rhsNode);
471 !(isFunctionDefinition && isSelfReference(ref, functionNodes))
477 * Checks whether the given variable is after the last used parameter.
478 * @param {eslint-scope.Variable} variable The variable to check.
479 * @returns {boolean} `true` if the variable is defined after the last
482 function isAfterLastUsedArg(variable) {
483 const def = variable.defs[0];
484 const params = context.getDeclaredVariables(def.node);
485 const posteriorParams = params.slice(params.indexOf(variable) + 1);
487 // If any used parameters occur after this parameter, do not report.
488 return !posteriorParams.some(v => v.references.length > 0 || v.eslintUsed);
492 * Gets an array of variables without read references.
493 * @param {Scope} scope an eslint-scope Scope object.
494 * @param {Variable[]} unusedVars an array that saving result.
495 * @returns {Variable[]} unused variables of the scope and descendant scopes.
498 function collectUnusedVariables(scope, unusedVars) {
499 const variables = scope.variables;
500 const childScopes = scope.childScopes;
503 if (scope.type !== "global" || config.vars === "all") {
504 for (i = 0, l = variables.length; i < l; ++i) {
505 const variable = variables[i];
507 // skip a variable of class itself name in the class scope
508 if (scope.type === "class" && scope.block.id === variable.identifiers[0]) {
512 // skip function expression names and variables marked with markVariableAsUsed()
513 if (scope.functionExpressionScope || variable.eslintUsed) {
517 // skip implicit "arguments" variable
518 if (scope.type === "function" && variable.name === "arguments" && variable.identifiers.length === 0) {
522 // explicit global variables don't have definitions.
523 const def = variable.defs[0];
526 const type = def.type;
528 // skip catch variables
529 if (type === "CatchClause") {
530 if (config.caughtErrors === "none") {
534 // skip ignored parameters
535 if (config.caughtErrorsIgnorePattern && config.caughtErrorsIgnorePattern.test(def.name.name)) {
540 if (type === "Parameter") {
542 // skip any setter argument
543 if ((def.node.parent.type === "Property" || def.node.parent.type === "MethodDefinition") && def.node.parent.kind === "set") {
547 // if "args" option is "none", skip any parameter
548 if (config.args === "none") {
552 // skip ignored parameters
553 if (config.argsIgnorePattern && config.argsIgnorePattern.test(def.name.name)) {
557 // if "args" option is "after-used", skip used variables
558 if (config.args === "after-used" && astUtils.isFunction(def.name.parent) && !isAfterLastUsedArg(variable)) {
563 // skip ignored variables
564 if (config.varsIgnorePattern && config.varsIgnorePattern.test(def.name.name)) {
570 if (!isUsedVariable(variable) && !isExported(variable) && !hasRestSpreadSibling(variable)) {
571 unusedVars.push(variable);
576 for (i = 0, l = childScopes.length; i < l; ++i) {
577 collectUnusedVariables(childScopes[i], unusedVars);
583 //--------------------------------------------------------------------------
585 //--------------------------------------------------------------------------
588 "Program:exit"(programNode) {
589 const unusedVars = collectUnusedVariables(context.getScope(), []);
591 for (let i = 0, l = unusedVars.length; i < l; ++i) {
592 const unusedVar = unusedVars[i];
594 // Report the first declaration.
595 if (unusedVar.defs.length > 0) {
597 node: unusedVar.identifiers[0],
598 message: unusedVar.references.some(ref => ref.isWrite())
599 ? getAssignedMessage()
600 : getDefinedMessage(unusedVar),
604 // If there are no regular declaration, report the first `/*globals*/` comment directive.
605 } else if (unusedVar.eslintExplicitGlobalComments) {
606 const directiveComment = unusedVar.eslintExplicitGlobalComments[0];
610 loc: astUtils.getNameLocationInGlobalDirectiveComment(sourceCode, directiveComment, unusedVar.name),
611 message: getDefinedMessage(unusedVar),