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