1 | /**
|
2 | * @fileoverview Rule to flag use of variables before they are defined
|
3 | * @author Ilya Volodin
|
4 | * @copyright 2013 Ilya Volodin. All rights reserved.
|
5 | */
|
6 |
|
7 | ;
|
8 |
|
9 | //------------------------------------------------------------------------------
|
10 | // Helpers
|
11 | //------------------------------------------------------------------------------
|
12 |
|
13 | var SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/;
|
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 | var functions = true;
|
23 | var classes = 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 | }
|
31 |
|
32 | return {functions: functions, classes: classes};
|
33 | }
|
34 |
|
35 | /**
|
36 | * @returns {boolean} `false`.
|
37 | */
|
38 | function alwaysFalse() {
|
39 | return false;
|
40 | }
|
41 |
|
42 | /**
|
43 | * Checks whether or not a given variable is a function declaration.
|
44 | *
|
45 | * @param {escope.Variable} variable - A variable to check.
|
46 | * @returns {boolean} `true` if the variable is a function declaration.
|
47 | */
|
48 | function isFunction(variable) {
|
49 | return variable.defs[0].type === "FunctionName";
|
50 | }
|
51 |
|
52 | /**
|
53 | * Checks whether or not a given variable is a class declaration in an upper function scope.
|
54 | *
|
55 | * @param {escope.Variable} variable - A variable to check.
|
56 | * @param {escope.Reference} reference - A reference to check.
|
57 | * @returns {boolean} `true` if the variable is a class declaration.
|
58 | */
|
59 | function isOuterClass(variable, reference) {
|
60 | return (
|
61 | variable.defs[0].type === "ClassName" &&
|
62 | variable.scope.variableScope !== reference.from.variableScope
|
63 | );
|
64 | }
|
65 |
|
66 | /**
|
67 | * Checks whether or not a given variable is a function declaration or a class declaration in an upper function scope.
|
68 | *
|
69 | * @param {escope.Variable} variable - A variable to check.
|
70 | * @param {escope.Reference} reference - A reference to check.
|
71 | * @returns {boolean} `true` if the variable is a function declaration or a class declaration.
|
72 | */
|
73 | function isFunctionOrOuterClass(variable, reference) {
|
74 | return isFunction(variable, reference) || isOuterClass(variable, reference);
|
75 | }
|
76 |
|
77 | /**
|
78 | * Checks whether or not a given location is inside of the range of a given node.
|
79 | *
|
80 | * @param {ASTNode} node - An node to check.
|
81 | * @param {number} location - A location to check.
|
82 | * @returns {boolean} `true` if the location is inside of the range of the node.
|
83 | */
|
84 | function isInRange(node, location) {
|
85 | return node && node.range[0] <= location && location <= node.range[1];
|
86 | }
|
87 |
|
88 | /**
|
89 | * Checks whether or not a given reference is inside of the initializers of a given variable.
|
90 | *
|
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 | var node = variable.identifiers[0].parent;
|
101 | var 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 | break;
|
109 | } else if (node.type === "AssignmentPattern") {
|
110 | if (isInRange(node.right, location)) {
|
111 | return true;
|
112 | }
|
113 | } else if (SENTINEL_TYPE.test(node.type)) {
|
114 | break;
|
115 | }
|
116 |
|
117 | node = node.parent;
|
118 | }
|
119 |
|
120 | return false;
|
121 | }
|
122 |
|
123 | //------------------------------------------------------------------------------
|
124 | // Rule Definition
|
125 | //------------------------------------------------------------------------------
|
126 |
|
127 | module.exports = function(context) {
|
128 | var options = parseOptions(context.options[0]);
|
129 |
|
130 | // Defines a function which checks whether or not a reference is allowed according to the option.
|
131 | var isAllowed;
|
132 | if (options.functions && options.classes) {
|
133 | isAllowed = alwaysFalse;
|
134 | } else if (options.functions) {
|
135 | isAllowed = isOuterClass;
|
136 | } else if (options.classes) {
|
137 | isAllowed = isFunction;
|
138 | } else {
|
139 | isAllowed = isFunctionOrOuterClass;
|
140 | }
|
141 |
|
142 | /**
|
143 | * Finds and validates all variables in a given scope.
|
144 | * @param {Scope} scope The scope object.
|
145 | * @returns {void}
|
146 | * @private
|
147 | */
|
148 | function findVariablesInScope(scope) {
|
149 | scope.references.forEach(function(reference) {
|
150 | var variable = reference.resolved;
|
151 |
|
152 | // Skips when the reference is:
|
153 | // - initialization's.
|
154 | // - referring to an undefined variable.
|
155 | // - referring to a global environment variable (there're no identifiers).
|
156 | // - located preceded by the variable (except in initializers).
|
157 | // - allowed by options.
|
158 | if (reference.init ||
|
159 | !variable ||
|
160 | variable.identifiers.length === 0 ||
|
161 | (variable.identifiers[0].range[1] < reference.identifier.range[1] && !isInInitializer(variable, reference)) ||
|
162 | isAllowed(variable, reference)
|
163 | ) {
|
164 | return;
|
165 | }
|
166 |
|
167 | // Reports.
|
168 | context.report({
|
169 | node: reference.identifier,
|
170 | message: "'{{name}}' was used before it was defined",
|
171 | data: reference.identifier
|
172 | });
|
173 | });
|
174 | }
|
175 |
|
176 | /**
|
177 | * Validates variables inside of a node's scope.
|
178 | * @param {ASTNode} node The node to check.
|
179 | * @returns {void}
|
180 | * @private
|
181 | */
|
182 | function findVariables() {
|
183 | var scope = context.getScope();
|
184 | findVariablesInScope(scope);
|
185 | }
|
186 |
|
187 | var ruleDefinition = {
|
188 | "Program:exit": function(node) {
|
189 | var scope = context.getScope(),
|
190 | ecmaFeatures = context.parserOptions.ecmaFeatures || {};
|
191 |
|
192 | findVariablesInScope(scope);
|
193 |
|
194 | // both Node.js and Modules have an extra scope
|
195 | if (ecmaFeatures.globalReturn || node.sourceType === "module") {
|
196 | findVariablesInScope(scope.childScopes[0]);
|
197 | }
|
198 | }
|
199 | };
|
200 |
|
201 | if (context.parserOptions.ecmaVersion >= 6) {
|
202 | ruleDefinition["BlockStatement:exit"] =
|
203 | ruleDefinition["SwitchStatement:exit"] = findVariables;
|
204 |
|
205 | ruleDefinition["ArrowFunctionExpression:exit"] = function(node) {
|
206 | if (node.body.type !== "BlockStatement") {
|
207 | findVariables(node);
|
208 | }
|
209 | };
|
210 | } else {
|
211 | ruleDefinition["FunctionExpression:exit"] =
|
212 | ruleDefinition["FunctionDeclaration:exit"] =
|
213 | ruleDefinition["ArrowFunctionExpression:exit"] = findVariables;
|
214 | }
|
215 |
|
216 | return ruleDefinition;
|
217 | };
|
218 |
|
219 | module.exports.schema = [
|
220 | {
|
221 | "oneOf": [
|
222 | {
|
223 | "enum": ["nofunc"]
|
224 | },
|
225 | {
|
226 | "type": "object",
|
227 | "properties": {
|
228 | "functions": {"type": "boolean"},
|
229 | "classes": {"type": "boolean"}
|
230 | },
|
231 | "additionalProperties": false
|
232 | }
|
233 | ]
|
234 | }
|
235 | ];
|