UNPKG

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