]>
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 | ||
34eeec05 | 30 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
31 | module.exports = { |
32 | meta: { | |
33 | type: "problem", | |
34 | ||
35 | docs: { | |
8f9d1d4d | 36 | description: "Disallow unused variables", |
eb39fafa | 37 | recommended: true, |
f2a92ac6 | 38 | url: "https://eslint.org/docs/latest/rules/no-unused-vars" |
eb39fafa DC |
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" | |
8f9d1d4d DC |
70 | }, |
71 | destructuredArrayIgnorePattern: { | |
72 | type: "string" | |
eb39fafa | 73 | } |
6f036462 TL |
74 | }, |
75 | additionalProperties: false | |
eb39fafa DC |
76 | } |
77 | ] | |
78 | } | |
79 | ], | |
80 | ||
81 | messages: { | |
82 | unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}." | |
83 | } | |
84 | }, | |
85 | ||
86 | create(context) { | |
f2a92ac6 | 87 | const sourceCode = context.sourceCode; |
eb39fafa DC |
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 | } | |
8f9d1d4d DC |
120 | |
121 | if (firstOption.destructuredArrayIgnorePattern) { | |
122 | config.destructuredArrayIgnorePattern = new RegExp(firstOption.destructuredArrayIgnorePattern, "u"); | |
123 | } | |
eb39fafa DC |
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) { | |
8f9d1d4d DC |
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 | } | |
eb39fafa DC |
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 | ||
5422a9cc TL |
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 | ||
eb39fafa DC |
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) { | |
5422a9cc TL |
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; | |
eb39fafa DC |
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) { | |
8f9d1d4d | 262 | if (nodes.includes(scope.block)) { |
eb39fafa DC |
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 | ||
609c276f TL |
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 | ||
eb39fafa DC |
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; | |
eb39fafa DC |
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" && | |
609c276f | 368 | isUnusedExpression(parent) && |
eb39fafa DC |
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; | |
eb39fafa DC |
461 | |
462 | return ref.isRead() && ( | |
463 | ||
464 | // self update. e.g. `a += 1`, `a++` | |
5422a9cc TL |
465 | ( |
466 | ( | |
eb39fafa | 467 | parent.type === "AssignmentExpression" && |
5422a9cc TL |
468 | parent.left === id && |
469 | isUnusedExpression(parent) | |
eb39fafa | 470 | ) || |
5422a9cc TL |
471 | ( |
472 | parent.type === "UpdateExpression" && | |
473 | isUnusedExpression(parent) | |
474 | ) | |
475 | ) || | |
476 | ||
477 | // in RHS of an assignment for itself. e.g. `a = a + 1` | |
eb39fafa | 478 | ( |
5422a9cc TL |
479 | rhsNode && |
480 | isInside(id, rhsNode) && | |
481 | !isInsideOfStorableFunction(id, rhsNode) | |
482 | ) | |
eb39fafa DC |
483 | ); |
484 | } | |
485 | ||
486 | /** | |
8f9d1d4d | 487 | * Determine if an identifier is used either in for-in or for-of loops. |
eb39fafa DC |
488 | * @param {Reference} ref The reference to check. |
489 | * @returns {boolean} whether reference is used in the for-in loops | |
490 | * @private | |
491 | */ | |
8f9d1d4d | 492 | function isForInOfRef(ref) { |
eb39fafa DC |
493 | let target = ref.identifier.parent; |
494 | ||
495 | ||
496 | // "for (var ...) { return; }" | |
497 | if (target.type === "VariableDeclarator") { | |
498 | target = target.parent.parent; | |
499 | } | |
500 | ||
8f9d1d4d | 501 | if (target.type !== "ForInStatement" && target.type !== "ForOfStatement") { |
eb39fafa DC |
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 => { | |
8f9d1d4d | 534 | if (isForInOfRef(ref)) { |
eb39fafa DC |
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]; | |
f2a92ac6 | 558 | const params = sourceCode.getDeclaredVariables(def.node); |
eb39fafa DC |
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; | |
8f9d1d4d DC |
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 | } | |
eb39fafa DC |
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) { | |
f2a92ac6 | 676 | const unusedVars = collectUnusedVariables(sourceCode.getScope(programNode), []); |
eb39fafa DC |
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) { | |
5422a9cc TL |
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 | ||
eb39fafa | 693 | context.report({ |
5422a9cc | 694 | node: referenceToReport ? referenceToReport.identifier : unusedVar.identifiers[0], |
eb39fafa DC |
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 | }; |