• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to flag use of variables before they are defined
3 * @author Ilya Volodin
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Helpers
10//------------------------------------------------------------------------------
11
12const SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/u;
13const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/u;
14
15/**
16 * Parses a given value as options.
17 * @param {any} options A value to parse.
18 * @returns {Object} The parsed options.
19 */
20function parseOptions(options) {
21    let functions = true;
22    let classes = true;
23    let variables = true;
24
25    if (typeof options === "string") {
26        functions = (options !== "nofunc");
27    } else if (typeof options === "object" && options !== null) {
28        functions = options.functions !== false;
29        classes = options.classes !== false;
30        variables = options.variables !== false;
31    }
32
33    return { functions, classes, variables };
34}
35
36/**
37 * Checks whether or not a given variable is a function declaration.
38 * @param {eslint-scope.Variable} variable A variable to check.
39 * @returns {boolean} `true` if the variable is a function declaration.
40 */
41function isFunction(variable) {
42    return variable.defs[0].type === "FunctionName";
43}
44
45/**
46 * Checks whether or not a given variable is a class declaration in an upper function scope.
47 * @param {eslint-scope.Variable} variable A variable to check.
48 * @param {eslint-scope.Reference} reference A reference to check.
49 * @returns {boolean} `true` if the variable is a class declaration.
50 */
51function isOuterClass(variable, reference) {
52    return (
53        variable.defs[0].type === "ClassName" &&
54        variable.scope.variableScope !== reference.from.variableScope
55    );
56}
57
58/**
59 * Checks whether or not a given variable is a variable declaration in an upper function scope.
60 * @param {eslint-scope.Variable} variable A variable to check.
61 * @param {eslint-scope.Reference} reference A reference to check.
62 * @returns {boolean} `true` if the variable is a variable declaration.
63 */
64function isOuterVariable(variable, reference) {
65    return (
66        variable.defs[0].type === "Variable" &&
67        variable.scope.variableScope !== reference.from.variableScope
68    );
69}
70
71/**
72 * Checks whether or not a given location is inside of the range of a given node.
73 * @param {ASTNode} node An node to check.
74 * @param {number} location A location to check.
75 * @returns {boolean} `true` if the location is inside of the range of the node.
76 */
77function isInRange(node, location) {
78    return node && node.range[0] <= location && location <= node.range[1];
79}
80
81/**
82 * Checks whether or not a given reference is inside of the initializers of a given variable.
83 *
84 * This returns `true` in the following cases:
85 *
86 *     var a = a
87 *     var [a = a] = list
88 *     var {a = a} = obj
89 *     for (var a in a) {}
90 *     for (var a of a) {}
91 * @param {Variable} variable A variable to check.
92 * @param {Reference} reference A reference to check.
93 * @returns {boolean} `true` if the reference is inside of the initializers.
94 */
95function isInInitializer(variable, reference) {
96    if (variable.scope !== reference.from) {
97        return false;
98    }
99
100    let node = variable.identifiers[0].parent;
101    const location = reference.identifier.range[1];
102
103    while (node) {
104        if (node.type === "VariableDeclarator") {
105            if (isInRange(node.init, location)) {
106                return true;
107            }
108            if (FOR_IN_OF_TYPE.test(node.parent.parent.type) &&
109                isInRange(node.parent.parent.right, location)
110            ) {
111                return true;
112            }
113            break;
114        } else if (node.type === "AssignmentPattern") {
115            if (isInRange(node.right, location)) {
116                return true;
117            }
118        } else if (SENTINEL_TYPE.test(node.type)) {
119            break;
120        }
121
122        node = node.parent;
123    }
124
125    return false;
126}
127
128//------------------------------------------------------------------------------
129// Rule Definition
130//------------------------------------------------------------------------------
131
132module.exports = {
133    meta: {
134        type: "problem",
135
136        docs: {
137            description: "disallow the use of variables before they are defined",
138            category: "Variables",
139            recommended: false,
140            url: "https://eslint.org/docs/rules/no-use-before-define"
141        },
142
143        schema: [
144            {
145                oneOf: [
146                    {
147                        enum: ["nofunc"]
148                    },
149                    {
150                        type: "object",
151                        properties: {
152                            functions: { type: "boolean" },
153                            classes: { type: "boolean" },
154                            variables: { type: "boolean" }
155                        },
156                        additionalProperties: false
157                    }
158                ]
159            }
160        ],
161
162        messages: {
163            usedBeforeDefined: "'{{name}}' was used before it was defined."
164        }
165    },
166
167    create(context) {
168        const options = parseOptions(context.options[0]);
169
170        /**
171         * Determines whether a given use-before-define case should be reported according to the options.
172         * @param {eslint-scope.Variable} variable The variable that gets used before being defined
173         * @param {eslint-scope.Reference} reference The reference to the variable
174         * @returns {boolean} `true` if the usage should be reported
175         */
176        function isForbidden(variable, reference) {
177            if (isFunction(variable)) {
178                return options.functions;
179            }
180            if (isOuterClass(variable, reference)) {
181                return options.classes;
182            }
183            if (isOuterVariable(variable, reference)) {
184                return options.variables;
185            }
186            return true;
187        }
188
189        /**
190         * Finds and validates all variables in a given scope.
191         * @param {Scope} scope The scope object.
192         * @returns {void}
193         * @private
194         */
195        function findVariablesInScope(scope) {
196            scope.references.forEach(reference => {
197                const variable = reference.resolved;
198
199                /*
200                 * Skips when the reference is:
201                 * - initialization's.
202                 * - referring to an undefined variable.
203                 * - referring to a global environment variable (there're no identifiers).
204                 * - located preceded by the variable (except in initializers).
205                 * - allowed by options.
206                 */
207                if (reference.init ||
208                    !variable ||
209                    variable.identifiers.length === 0 ||
210                    (variable.identifiers[0].range[1] < reference.identifier.range[1] && !isInInitializer(variable, reference)) ||
211                    !isForbidden(variable, reference)
212                ) {
213                    return;
214                }
215
216                // Reports.
217                context.report({
218                    node: reference.identifier,
219                    messageId: "usedBeforeDefined",
220                    data: reference.identifier
221                });
222            });
223
224            scope.childScopes.forEach(findVariablesInScope);
225        }
226
227        return {
228            Program() {
229                findVariablesInScope(context.getScope());
230            }
231        };
232    }
233};
234