summaryrefslogtreecommitdiff
path: root/tools/eslint/lib/rules/no-unused-vars.js
blob: 2d61a7b988efc7cd2913530ad2ffdb2169a9d8b0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
/**
 * @fileoverview Rule to flag declared but unused variables
 * @author Ilya Volodin
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

var lodash = require("lodash");

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = function(context) {

    var MESSAGE = "'{{name}}' is defined but never used";

    var config = {
        vars: "all",
        args: "after-used"
    };

    var firstOption = context.options[0];

    if (firstOption) {
        if (typeof firstOption === "string") {
            config.vars = firstOption;
        } else {
            config.vars = firstOption.vars || config.vars;
            config.args = firstOption.args || config.args;

            if (firstOption.varsIgnorePattern) {
                config.varsIgnorePattern = new RegExp(firstOption.varsIgnorePattern);
            }

            if (firstOption.argsIgnorePattern) {
                config.argsIgnorePattern = new RegExp(firstOption.argsIgnorePattern);
            }
        }
    }

    //--------------------------------------------------------------------------
    // Helpers
    //--------------------------------------------------------------------------

    /**
     * Determines if a given variable is being exported from a module.
     * @param {Variable} variable - EScope variable object.
     * @returns {boolean} True if the variable is exported, false if not.
     * @private
     */
    function isExported(variable) {

        var definition = variable.defs[0];

        if (definition) {

            var node = definition.node;
            if (node.type === "VariableDeclarator") {
                node = node.parent;
            } else if (definition.type === "Parameter") {
                return false;
            }

            return node.parent.type.indexOf("Export") === 0;
        } else {
            return false;
        }
    }

    /**
     * Determines if a reference is a read operation.
     * @param {Reference} ref - An escope Reference
     * @returns {Boolean} whether the given reference represents a read operation
     * @private
     */
    function isReadRef(ref) {
        return ref.isRead();
    }

    /**
     * Determine if an identifier is referencing an enclosing function name.
     * @param {Reference} ref - The reference to check.
     * @param {ASTNode[]} nodes - The candidate function nodes.
     * @returns {boolean} True if it's a self-reference, false if not.
     * @private
     */
    function isSelfReference(ref, nodes) {
        var scope = ref.from;

        while (scope) {
            if (nodes.indexOf(scope.block) >= 0) {
                return true;
            }

            scope = scope.upper;
        }

        return false;
    }

    /**
     * Determines if the variable is used.
     * @param {Variable} variable - The variable to check.
     * @param {Reference[]} references - The variable references to check.
     * @returns {boolean} True if the variable is used
     */
    function isUsedVariable(variable) {
        var functionNodes = variable.defs.filter(function(def) {
                return def.type === "FunctionName";
            }).map(function(def) {
                return def.node;
            }),
            isFunctionDefinition = functionNodes.length > 0;

        return variable.references.some(function(ref) {
            return isReadRef(ref) && !(isFunctionDefinition && isSelfReference(ref, functionNodes));
        });
    }

    /**
     * Gets an array of variables without read references.
     * @param {Scope} scope - an escope Scope object.
     * @param {Variable[]} unusedVars - an array that saving result.
     * @returns {Variable[]} unused variables of the scope and descendant scopes.
     * @private
     */
    function collectUnusedVariables(scope, unusedVars) {
        var variables = scope.variables;
        var childScopes = scope.childScopes;
        var i, l;

        if (scope.type !== "TDZ" && (scope.type !== "global" || config.vars === "all")) {
            for (i = 0, l = variables.length; i < l; ++i) {
                var variable = variables[i];

                // skip a variable of class itself name in the class scope
                if (scope.type === "class" && scope.block.id === variable.identifiers[0]) {
                    continue;
                }
                // skip function expression names and variables marked with markVariableAsUsed()
                if (scope.functionExpressionScope || variable.eslintUsed) {
                    continue;
                }
                // skip implicit "arguments" variable
                if (scope.type === "function" && variable.name === "arguments" && variable.identifiers.length === 0) {
                    continue;
                }

                // explicit global variables don't have definitions.
                var def = variable.defs[0];
                if (def) {
                    var type = def.type;

                    // skip catch variables
                    if (type === "CatchClause") {
                        continue;
                    }

                    if (type === "Parameter") {
                        // skip any setter argument
                        if (def.node.parent.type === "Property" && def.node.parent.kind === "set") {
                            continue;
                        }

                        // if "args" option is "none", skip any parameter
                        if (config.args === "none") {
                            continue;
                        }

                        // skip ignored parameters
                        if (config.argsIgnorePattern && config.argsIgnorePattern.test(def.name.name)) {
                            continue;
                        }

                        // if "args" option is "after-used", skip all but the last parameter
                        if (config.args === "after-used" && def.index < def.node.params.length - 1) {
                            continue;
                        }
                    } else {
                        // skip ignored variables
                        if (config.varsIgnorePattern && config.varsIgnorePattern.test(def.name.name)) {
                            continue;
                        }
                    }
                }

                if (!isUsedVariable(variable) && !isExported(variable)) {
                    unusedVars.push(variable);
                }
            }
        }

        for (i = 0, l = childScopes.length; i < l; ++i) {
            collectUnusedVariables(childScopes[i], unusedVars);
        }

        return unusedVars;
    }

    /**
     * Gets the index of a given variable name in a given comment.
     * @param {escope.Variable} variable - A variable to get.
     * @param {ASTNode} comment - A comment node which includes the variable name.
     * @returns {number} The index of the variable name's location.
     */
    function getColumnInComment(variable, comment) {
        var namePattern = new RegExp("[\\s,]" + lodash.escapeRegExp(variable.name) + "(?:$|[\\s,:])", "g");

        // To ignore the first text "global".
        namePattern.lastIndex = comment.value.indexOf("global") + 6;

        // Search a given variable name.
        var match = namePattern.exec(comment.value);
        return match ? match.index + 1 : 0;
    }

    /**
     * Creates the correct location of a given variables.
     * The location is at its name string in a `/*global` comment.
     *
     * @param {escope.Variable} variable - A variable to get its location.
     * @returns {{line: number, column: number}} The location object for the variable.
     */
    function getLocation(variable) {
        var comment = variable.eslintExplicitGlobalComment;
        var baseLoc = comment.loc.start;
        var column = getColumnInComment(variable, comment);
        var prefix = comment.value.slice(0, column);
        var lineInComment = (prefix.match(/\n/g) || []).length;

        if (lineInComment > 0) {
            column -= 1 + prefix.lastIndexOf("\n");
        } else {
            // 2 is for `/*`
            column += baseLoc.column + 2;
        }

        return {
            line: baseLoc.line + lineInComment,
            column: column
        };
    }

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    return {
        "Program:exit": function(programNode) {
            var unusedVars = collectUnusedVariables(context.getScope(), []);

            for (var i = 0, l = unusedVars.length; i < l; ++i) {
                var unusedVar = unusedVars[i];

                if (unusedVar.eslintExplicitGlobal) {
                    context.report({
                        node: programNode,
                        loc: getLocation(unusedVar),
                        message: MESSAGE,
                        data: unusedVar
                    });
                } else if (unusedVar.defs.length > 0) {
                    context.report({
                        node: unusedVar.identifiers[0],
                        message: MESSAGE,
                        data: unusedVar
                    });
                }
            }
        }
    };

};

module.exports.schema = [
    {
        "oneOf": [
            {
                "enum": ["all", "local"]
            },
            {
                "type": "object",
                "properties": {
                    "vars": {
                        "enum": ["all", "local"]
                    },
                    "varsIgnorePattern": {
                        "type": "string"
                    },
                    "args": {
                        "enum": ["all", "after-used", "none"]
                    },
                    "argsIgnorePattern": {
                        "type": "string"
                    }
                }
            }
        ]
    }
];