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