1 | /**
|
2 | * @fileoverview Rule to flag use of variables before they are defined
|
3 | * @author Ilya Volodin
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Helpers
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/;
|
13 | const 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 | */
|
21 | function 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 | */
|
43 | function 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 | */
|
54 | function 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 | */
|
67 | function 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 | */
|
81 | function 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 | */
|
100 | function 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 |
|
137 | module.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 | };
|