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