summaryrefslogtreecommitdiff
path: root/tools/eslint/lib/rules/no-var.js
blob: dfef7ceea690149bb48cee0cb2c40f456e4d4da1 (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
/**
 * @fileoverview Rule to check for the usage of var.
 * @author Jamund Ferguson
 */

"use strict";

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

const SCOPE_NODE_TYPE = /^(?:Program|BlockStatement|SwitchStatement|ForStatement|ForInStatement|ForOfStatement)$/;

/**
 * Gets the scope node which directly contains a given node.
 *
 * @param {ASTNode} node - A node to get. This is a `VariableDeclaration` or
 *      an `Identifier`.
 * @returns {ASTNode} A scope node. This is one of `Program`, `BlockStatement`,
 *      `SwitchStatement`, `ForStatement`, `ForInStatement`, and
 *      `ForOfStatement`.
 */
function getScopeNode(node) {
    while (node) {
        if (SCOPE_NODE_TYPE.test(node.type)) {
            return node;
        }

        node = node.parent;
    }

    /* istanbul ignore next : unreachable */
    return null;
}

/**
 * Checks whether a given variable is redeclared or not.
 *
 * @param {escope.Variable} variable - A variable to check.
 * @returns {boolean} `true` if the variable is redeclared.
 */
function isRedeclared(variable) {
    return variable.defs.length >= 2;
}

/**
 * Checks whether a given variable is used from outside of the specified scope.
 *
 * @param {ASTNode} scopeNode - A scope node to check.
 * @returns {Function} The predicate function which checks whether a given
 *      variable is used from outside of the specified scope.
 */
function isUsedFromOutsideOf(scopeNode) {

    /**
     * Checks whether a given reference is inside of the specified scope or not.
     *
     * @param {escope.Reference} reference - A reference to check.
     * @returns {boolean} `true` if the reference is inside of the specified
     *      scope.
     */
    function isOutsideOfScope(reference) {
        const scope = scopeNode.range;
        const id = reference.identifier.range;

        return id[0] < scope[0] || id[1] > scope[1];
    }

    return function(variable) {
        return variable.references.some(isOutsideOfScope);
    };
}

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

module.exports = {
    meta: {
        docs: {
            description: "require `let` or `const` instead of `var`",
            category: "ECMAScript 6",
            recommended: false
        },

        schema: [],
        fixable: "code"
    },

    create: function(context) {
        const sourceCode = context.getSourceCode();

        /**
         * Checks whether it can fix a given variable declaration or not.
         * It cannot fix if the following cases:
         *
         * - A variable is declared on a SwitchCase node.
         * - A variable is redeclared.
         * - A variable is used from outside the scope.
         *
         * ## A variable is declared on a SwitchCase node.
         *
         * If this rule modifies 'var' declarations on a SwitchCase node, it
         * would generate the warnings of 'no-case-declarations' rule. And the
         * 'eslint:recommended' preset includes 'no-case-declarations' rule, so
         * this rule doesn't modify those declarations.
         *
         * ## A variable is redeclared.
         *
         * The language spec disallows redeclarations of `let` declarations.
         * Those variables would cause syntax errors.
         *
         * ## A variable is used from outside the scope.
         *
         * The language spec disallows accesses from outside of the scope for
         * `let` declarations. Those variables would cause reference errors.
         *
         * @param {ASTNode} node - A variable declaration node to check.
         * @returns {boolean} `true` if it can fix the node.
         */
        function canFix(node) {
            const variables = context.getDeclaredVariables(node);
            const scopeNode = getScopeNode(node);

            return !(
                node.parent.type === "SwitchCase" ||
                variables.some(isRedeclared) ||
                variables.some(isUsedFromOutsideOf(scopeNode))
            );
        }

        /**
         * Reports a given variable declaration node.
         *
         * @param {ASTNode} node - A variable declaration node to report.
         * @returns {void}
         */
        function report(node) {
            const varToken = sourceCode.getFirstToken(node);

            context.report({
                node: node,
                message: "Unexpected var, use let or const instead.",

                fix: function(fixer) {
                    if (canFix(node)) {
                        return fixer.replaceText(varToken, "let");
                    }
                    return null;
                }
            });
        }

        return {
            VariableDeclaration: function(node) {
                if (node.kind === "var") {
                    report(node);
                }
            }
        };
    }
};