]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/rules/no-unused-vars.js
import and build new upstream release 7.2.0
[pve-eslint.git] / eslint / lib / rules / no-unused-vars.js
CommitLineData
eb39fafa
DC
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({
d3726936
TL
622 node: unusedVar.references.length ? unusedVar.references[
623 unusedVar.references.length - 1
624 ].identifier : unusedVar.identifiers[0],
eb39fafa
DC
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};