]>
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 | } | |
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 | }; |