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