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)$/u;
|
13 | const 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 | */
|
20 | function 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 | */
|
41 | function 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 | */
|
51 | function 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 | */
|
64 | function 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 | */
|
77 | function 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 | */
|
95 | function 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 |
|
132 | module.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 | };
|