]>
Commit | Line | Data |
---|---|---|
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 | ||
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 | } | |
6f036462 TL |
71 | }, |
72 | additionalProperties: false | |
eb39fafa DC |
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 | * Determines if a variable has a sibling rest property | |
201 | * @param {Variable} variable eslint-scope variable object. | |
202 | * @returns {boolean} True if the variable is exported, false if not. | |
203 | * @private | |
204 | */ | |
205 | function hasRestSpreadSibling(variable) { | |
206 | if (config.ignoreRestSiblings) { | |
207 | return variable.defs.some(def => { | |
208 | const propertyNode = def.name.parent; | |
209 | const patternNode = propertyNode.parent; | |
210 | ||
211 | return ( | |
212 | propertyNode.type === "Property" && | |
213 | patternNode.type === "ObjectPattern" && | |
214 | REST_PROPERTY_TYPE.test(patternNode.properties[patternNode.properties.length - 1].type) | |
215 | ); | |
216 | }); | |
217 | } | |
218 | ||
219 | return false; | |
220 | } | |
221 | ||
222 | /** | |
223 | * Determines if a reference is a read operation. | |
224 | * @param {Reference} ref An eslint-scope Reference | |
225 | * @returns {boolean} whether the given reference represents a read operation | |
226 | * @private | |
227 | */ | |
228 | function isReadRef(ref) { | |
229 | return ref.isRead(); | |
230 | } | |
231 | ||
232 | /** | |
233 | * Determine if an identifier is referencing an enclosing function name. | |
234 | * @param {Reference} ref The reference to check. | |
235 | * @param {ASTNode[]} nodes The candidate function nodes. | |
236 | * @returns {boolean} True if it's a self-reference, false if not. | |
237 | * @private | |
238 | */ | |
239 | function isSelfReference(ref, nodes) { | |
240 | let scope = ref.from; | |
241 | ||
242 | while (scope) { | |
243 | if (nodes.indexOf(scope.block) >= 0) { | |
244 | return true; | |
245 | } | |
246 | ||
247 | scope = scope.upper; | |
248 | } | |
249 | ||
250 | return false; | |
251 | } | |
252 | ||
253 | /** | |
254 | * Gets a list of function definitions for a specified variable. | |
255 | * @param {Variable} variable eslint-scope variable object. | |
256 | * @returns {ASTNode[]} Function nodes. | |
257 | * @private | |
258 | */ | |
259 | function getFunctionDefinitions(variable) { | |
260 | const functionDefinitions = []; | |
261 | ||
262 | variable.defs.forEach(def => { | |
263 | const { type, node } = def; | |
264 | ||
265 | // FunctionDeclarations | |
266 | if (type === "FunctionName") { | |
267 | functionDefinitions.push(node); | |
268 | } | |
269 | ||
270 | // FunctionExpressions | |
271 | if (type === "Variable" && node.init && | |
272 | (node.init.type === "FunctionExpression" || node.init.type === "ArrowFunctionExpression")) { | |
273 | functionDefinitions.push(node.init); | |
274 | } | |
275 | }); | |
276 | return functionDefinitions; | |
277 | } | |
278 | ||
279 | /** | |
280 | * Checks the position of given nodes. | |
281 | * @param {ASTNode} inner A node which is expected as inside. | |
282 | * @param {ASTNode} outer A node which is expected as outside. | |
283 | * @returns {boolean} `true` if the `inner` node exists in the `outer` node. | |
284 | * @private | |
285 | */ | |
286 | function isInside(inner, outer) { | |
287 | return ( | |
288 | inner.range[0] >= outer.range[0] && | |
289 | inner.range[1] <= outer.range[1] | |
290 | ); | |
291 | } | |
292 | ||
293 | /** | |
294 | * If a given reference is left-hand side of an assignment, this gets | |
295 | * the right-hand side node of the assignment. | |
296 | * | |
297 | * In the following cases, this returns null. | |
298 | * | |
299 | * - The reference is not the LHS of an assignment expression. | |
300 | * - The reference is inside of a loop. | |
301 | * - The reference is inside of a function scope which is different from | |
302 | * the declaration. | |
303 | * @param {eslint-scope.Reference} ref A reference to check. | |
304 | * @param {ASTNode} prevRhsNode The previous RHS node. | |
305 | * @returns {ASTNode|null} The RHS node or null. | |
306 | * @private | |
307 | */ | |
308 | function getRhsNode(ref, prevRhsNode) { | |
309 | const id = ref.identifier; | |
310 | const parent = id.parent; | |
311 | const grandparent = parent.parent; | |
312 | const refScope = ref.from.variableScope; | |
313 | const varScope = ref.resolved.scope.variableScope; | |
314 | const canBeUsedLater = refScope !== varScope || astUtils.isInLoop(id); | |
315 | ||
316 | /* | |
317 | * Inherits the previous node if this reference is in the node. | |
318 | * This is for `a = a + a`-like code. | |
319 | */ | |
320 | if (prevRhsNode && isInside(id, prevRhsNode)) { | |
321 | return prevRhsNode; | |
322 | } | |
323 | ||
324 | if (parent.type === "AssignmentExpression" && | |
325 | grandparent.type === "ExpressionStatement" && | |
326 | id === parent.left && | |
327 | !canBeUsedLater | |
328 | ) { | |
329 | return parent.right; | |
330 | } | |
331 | return null; | |
332 | } | |
333 | ||
334 | /** | |
335 | * Checks whether a given function node is stored to somewhere or not. | |
336 | * If the function node is stored, the function can be used later. | |
337 | * @param {ASTNode} funcNode A function node to check. | |
338 | * @param {ASTNode} rhsNode The RHS node of the previous assignment. | |
339 | * @returns {boolean} `true` if under the following conditions: | |
340 | * - the funcNode is assigned to a variable. | |
341 | * - the funcNode is bound as an argument of a function call. | |
342 | * - the function is bound to a property and the object satisfies above conditions. | |
343 | * @private | |
344 | */ | |
345 | function isStorableFunction(funcNode, rhsNode) { | |
346 | let node = funcNode; | |
347 | let parent = funcNode.parent; | |
348 | ||
349 | while (parent && isInside(parent, rhsNode)) { | |
350 | switch (parent.type) { | |
351 | case "SequenceExpression": | |
352 | if (parent.expressions[parent.expressions.length - 1] !== node) { | |
353 | return false; | |
354 | } | |
355 | break; | |
356 | ||
357 | case "CallExpression": | |
358 | case "NewExpression": | |
359 | return parent.callee !== node; | |
360 | ||
361 | case "AssignmentExpression": | |
362 | case "TaggedTemplateExpression": | |
363 | case "YieldExpression": | |
364 | return true; | |
365 | ||
366 | default: | |
367 | if (STATEMENT_TYPE.test(parent.type)) { | |
368 | ||
369 | /* | |
370 | * If it encountered statements, this is a complex pattern. | |
371 | * Since analyzing complex patterns is hard, this returns `true` to avoid false positive. | |
372 | */ | |
373 | return true; | |
374 | } | |
375 | } | |
376 | ||
377 | node = parent; | |
378 | parent = parent.parent; | |
379 | } | |
380 | ||
381 | return false; | |
382 | } | |
383 | ||
384 | /** | |
385 | * Checks whether a given Identifier node exists inside of a function node which can be used later. | |
386 | * | |
387 | * "can be used later" means: | |
388 | * - the function is assigned to a variable. | |
389 | * - the function is bound to a property and the object can be used later. | |
390 | * - the function is bound as an argument of a function call. | |
391 | * | |
392 | * If a reference exists in a function which can be used later, the reference is read when the function is called. | |
393 | * @param {ASTNode} id An Identifier node to check. | |
394 | * @param {ASTNode} rhsNode The RHS node of the previous assignment. | |
395 | * @returns {boolean} `true` if the `id` node exists inside of a function node which can be used later. | |
396 | * @private | |
397 | */ | |
398 | function isInsideOfStorableFunction(id, rhsNode) { | |
399 | const funcNode = astUtils.getUpperFunction(id); | |
400 | ||
401 | return ( | |
402 | funcNode && | |
403 | isInside(funcNode, rhsNode) && | |
404 | isStorableFunction(funcNode, rhsNode) | |
405 | ); | |
406 | } | |
407 | ||
408 | /** | |
409 | * Checks whether a given reference is a read to update itself or not. | |
410 | * @param {eslint-scope.Reference} ref A reference to check. | |
411 | * @param {ASTNode} rhsNode The RHS node of the previous assignment. | |
412 | * @returns {boolean} The reference is a read to update itself. | |
413 | * @private | |
414 | */ | |
415 | function isReadForItself(ref, rhsNode) { | |
416 | const id = ref.identifier; | |
417 | const parent = id.parent; | |
418 | const grandparent = parent.parent; | |
419 | ||
420 | return ref.isRead() && ( | |
421 | ||
422 | // self update. e.g. `a += 1`, `a++` | |
423 | (// in RHS of an assignment for itself. e.g. `a = a + 1` | |
424 | (( | |
425 | parent.type === "AssignmentExpression" && | |
426 | grandparent.type === "ExpressionStatement" && | |
427 | parent.left === id | |
428 | ) || | |
429 | ( | |
430 | parent.type === "UpdateExpression" && | |
431 | grandparent.type === "ExpressionStatement" | |
432 | ) || rhsNode && | |
433 | isInside(id, rhsNode) && | |
434 | !isInsideOfStorableFunction(id, rhsNode))) | |
435 | ); | |
436 | } | |
437 | ||
438 | /** | |
439 | * Determine if an identifier is used either in for-in loops. | |
440 | * @param {Reference} ref The reference to check. | |
441 | * @returns {boolean} whether reference is used in the for-in loops | |
442 | * @private | |
443 | */ | |
444 | function isForInRef(ref) { | |
445 | let target = ref.identifier.parent; | |
446 | ||
447 | ||
448 | // "for (var ...) { return; }" | |
449 | if (target.type === "VariableDeclarator") { | |
450 | target = target.parent.parent; | |
451 | } | |
452 | ||
453 | if (target.type !== "ForInStatement") { | |
454 | return false; | |
455 | } | |
456 | ||
457 | // "for (...) { return; }" | |
458 | if (target.body.type === "BlockStatement") { | |
459 | target = target.body.body[0]; | |
460 | ||
461 | // "for (...) return;" | |
462 | } else { | |
463 | target = target.body; | |
464 | } | |
465 | ||
466 | // For empty loop body | |
467 | if (!target) { | |
468 | return false; | |
469 | } | |
470 | ||
471 | return target.type === "ReturnStatement"; | |
472 | } | |
473 | ||
474 | /** | |
475 | * Determines if the variable is used. | |
476 | * @param {Variable} variable The variable to check. | |
477 | * @returns {boolean} True if the variable is used | |
478 | * @private | |
479 | */ | |
480 | function isUsedVariable(variable) { | |
481 | const functionNodes = getFunctionDefinitions(variable), | |
482 | isFunctionDefinition = functionNodes.length > 0; | |
483 | let rhsNode = null; | |
484 | ||
485 | return variable.references.some(ref => { | |
486 | if (isForInRef(ref)) { | |
487 | return true; | |
488 | } | |
489 | ||
490 | const forItself = isReadForItself(ref, rhsNode); | |
491 | ||
492 | rhsNode = getRhsNode(ref, rhsNode); | |
493 | ||
494 | return ( | |
495 | isReadRef(ref) && | |
496 | !forItself && | |
497 | !(isFunctionDefinition && isSelfReference(ref, functionNodes)) | |
498 | ); | |
499 | }); | |
500 | } | |
501 | ||
502 | /** | |
503 | * Checks whether the given variable is after the last used parameter. | |
504 | * @param {eslint-scope.Variable} variable The variable to check. | |
505 | * @returns {boolean} `true` if the variable is defined after the last | |
506 | * used parameter. | |
507 | */ | |
508 | function isAfterLastUsedArg(variable) { | |
509 | const def = variable.defs[0]; | |
510 | const params = context.getDeclaredVariables(def.node); | |
511 | const posteriorParams = params.slice(params.indexOf(variable) + 1); | |
512 | ||
513 | // If any used parameters occur after this parameter, do not report. | |
514 | return !posteriorParams.some(v => v.references.length > 0 || v.eslintUsed); | |
515 | } | |
516 | ||
517 | /** | |
518 | * Gets an array of variables without read references. | |
519 | * @param {Scope} scope an eslint-scope Scope object. | |
520 | * @param {Variable[]} unusedVars an array that saving result. | |
521 | * @returns {Variable[]} unused variables of the scope and descendant scopes. | |
522 | * @private | |
523 | */ | |
524 | function collectUnusedVariables(scope, unusedVars) { | |
525 | const variables = scope.variables; | |
526 | const childScopes = scope.childScopes; | |
527 | let i, l; | |
528 | ||
529 | if (scope.type !== "global" || config.vars === "all") { | |
530 | for (i = 0, l = variables.length; i < l; ++i) { | |
531 | const variable = variables[i]; | |
532 | ||
533 | // skip a variable of class itself name in the class scope | |
534 | if (scope.type === "class" && scope.block.id === variable.identifiers[0]) { | |
535 | continue; | |
536 | } | |
537 | ||
538 | // skip function expression names and variables marked with markVariableAsUsed() | |
539 | if (scope.functionExpressionScope || variable.eslintUsed) { | |
540 | continue; | |
541 | } | |
542 | ||
543 | // skip implicit "arguments" variable | |
544 | if (scope.type === "function" && variable.name === "arguments" && variable.identifiers.length === 0) { | |
545 | continue; | |
546 | } | |
547 | ||
548 | // explicit global variables don't have definitions. | |
549 | const def = variable.defs[0]; | |
550 | ||
551 | if (def) { | |
552 | const type = def.type; | |
553 | ||
554 | // skip catch variables | |
555 | if (type === "CatchClause") { | |
556 | if (config.caughtErrors === "none") { | |
557 | continue; | |
558 | } | |
559 | ||
560 | // skip ignored parameters | |
561 | if (config.caughtErrorsIgnorePattern && config.caughtErrorsIgnorePattern.test(def.name.name)) { | |
562 | continue; | |
563 | } | |
564 | } | |
565 | ||
566 | if (type === "Parameter") { | |
567 | ||
568 | // skip any setter argument | |
569 | if ((def.node.parent.type === "Property" || def.node.parent.type === "MethodDefinition") && def.node.parent.kind === "set") { | |
570 | continue; | |
571 | } | |
572 | ||
573 | // if "args" option is "none", skip any parameter | |
574 | if (config.args === "none") { | |
575 | continue; | |
576 | } | |
577 | ||
578 | // skip ignored parameters | |
579 | if (config.argsIgnorePattern && config.argsIgnorePattern.test(def.name.name)) { | |
580 | continue; | |
581 | } | |
582 | ||
583 | // if "args" option is "after-used", skip used variables | |
584 | if (config.args === "after-used" && astUtils.isFunction(def.name.parent) && !isAfterLastUsedArg(variable)) { | |
585 | continue; | |
586 | } | |
587 | } else { | |
588 | ||
589 | // skip ignored variables | |
590 | if (config.varsIgnorePattern && config.varsIgnorePattern.test(def.name.name)) { | |
591 | continue; | |
592 | } | |
593 | } | |
594 | } | |
595 | ||
596 | if (!isUsedVariable(variable) && !isExported(variable) && !hasRestSpreadSibling(variable)) { | |
597 | unusedVars.push(variable); | |
598 | } | |
599 | } | |
600 | } | |
601 | ||
602 | for (i = 0, l = childScopes.length; i < l; ++i) { | |
603 | collectUnusedVariables(childScopes[i], unusedVars); | |
604 | } | |
605 | ||
606 | return unusedVars; | |
607 | } | |
608 | ||
609 | //-------------------------------------------------------------------------- | |
610 | // Public | |
611 | //-------------------------------------------------------------------------- | |
612 | ||
613 | return { | |
614 | "Program:exit"(programNode) { | |
615 | const unusedVars = collectUnusedVariables(context.getScope(), []); | |
616 | ||
617 | for (let i = 0, l = unusedVars.length; i < l; ++i) { | |
618 | const unusedVar = unusedVars[i]; | |
619 | ||
620 | // Report the first declaration. | |
621 | if (unusedVar.defs.length > 0) { | |
622 | context.report({ | |
d3726936 TL |
623 | node: unusedVar.references.length ? unusedVar.references[ |
624 | unusedVar.references.length - 1 | |
625 | ].identifier : unusedVar.identifiers[0], | |
eb39fafa DC |
626 | messageId: "unusedVar", |
627 | data: unusedVar.references.some(ref => ref.isWrite()) | |
628 | ? getAssignedMessageData(unusedVar) | |
629 | : getDefinedMessageData(unusedVar) | |
630 | }); | |
631 | ||
632 | // If there are no regular declaration, report the first `/*globals*/` comment directive. | |
633 | } else if (unusedVar.eslintExplicitGlobalComments) { | |
634 | const directiveComment = unusedVar.eslintExplicitGlobalComments[0]; | |
635 | ||
636 | context.report({ | |
637 | node: programNode, | |
638 | loc: astUtils.getNameLocationInGlobalDirectiveComment(sourceCode, directiveComment, unusedVar.name), | |
639 | messageId: "unusedVar", | |
640 | data: getDefinedMessageData(unusedVar) | |
641 | }); | |
642 | } | |
643 | } | |
644 | } | |
645 | }; | |
646 | ||
647 | } | |
648 | }; |