UNPKG

14.1 kBJavaScriptView Raw
1/**
2 * @fileoverview A rule to control the use of single variable declarations.
3 * @author Ian Christian Myers
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Rule Definition
10//------------------------------------------------------------------------------
11
12module.exports = {
13 meta: {
14 docs: {
15 description: "enforce variables to be declared either together or separately in functions",
16 category: "Stylistic Issues",
17 recommended: false
18 },
19
20 schema: [
21 {
22 oneOf: [
23 {
24 enum: ["always", "never"]
25 },
26 {
27 type: "object",
28 properties: {
29 var: {
30 enum: ["always", "never"]
31 },
32 let: {
33 enum: ["always", "never"]
34 },
35 const: {
36 enum: ["always", "never"]
37 }
38 },
39 additionalProperties: false
40 },
41 {
42 type: "object",
43 properties: {
44 initialized: {
45 enum: ["always", "never"]
46 },
47 uninitialized: {
48 enum: ["always", "never"]
49 }
50 },
51 additionalProperties: false
52 }
53 ]
54 }
55 ]
56 },
57
58 create(context) {
59
60 const MODE_ALWAYS = "always",
61 MODE_NEVER = "never";
62
63 const mode = context.options[0] || MODE_ALWAYS;
64
65 const options = {
66 };
67
68 if (typeof mode === "string") { // simple options configuration with just a string
69 options.var = { uninitialized: mode, initialized: mode};
70 options.let = { uninitialized: mode, initialized: mode};
71 options.const = { uninitialized: mode, initialized: mode};
72 } else if (typeof mode === "object") { // options configuration is an object
73 if (mode.hasOwnProperty("var") && typeof mode.var === "string") {
74 options.var = { uninitialized: mode.var, initialized: mode.var};
75 }
76 if (mode.hasOwnProperty("let") && typeof mode.let === "string") {
77 options.let = { uninitialized: mode.let, initialized: mode.let};
78 }
79 if (mode.hasOwnProperty("const") && typeof mode.const === "string") {
80 options.const = { uninitialized: mode.const, initialized: mode.const};
81 }
82 if (mode.hasOwnProperty("uninitialized")) {
83 if (!options.var) {
84 options.var = {};
85 }
86 if (!options.let) {
87 options.let = {};
88 }
89 if (!options.const) {
90 options.const = {};
91 }
92 options.var.uninitialized = mode.uninitialized;
93 options.let.uninitialized = mode.uninitialized;
94 options.const.uninitialized = mode.uninitialized;
95 }
96 if (mode.hasOwnProperty("initialized")) {
97 if (!options.var) {
98 options.var = {};
99 }
100 if (!options.let) {
101 options.let = {};
102 }
103 if (!options.const) {
104 options.const = {};
105 }
106 options.var.initialized = mode.initialized;
107 options.let.initialized = mode.initialized;
108 options.const.initialized = mode.initialized;
109 }
110 }
111
112 //--------------------------------------------------------------------------
113 // Helpers
114 //--------------------------------------------------------------------------
115
116 const functionStack = [];
117 const blockStack = [];
118
119 /**
120 * Increments the blockStack counter.
121 * @returns {void}
122 * @private
123 */
124 function startBlock() {
125 blockStack.push({
126 let: {initialized: false, uninitialized: false},
127 const: {initialized: false, uninitialized: false}
128 });
129 }
130
131 /**
132 * Increments the functionStack counter.
133 * @returns {void}
134 * @private
135 */
136 function startFunction() {
137 functionStack.push({initialized: false, uninitialized: false});
138 startBlock();
139 }
140
141 /**
142 * Decrements the blockStack counter.
143 * @returns {void}
144 * @private
145 */
146 function endBlock() {
147 blockStack.pop();
148 }
149
150 /**
151 * Decrements the functionStack counter.
152 * @returns {void}
153 * @private
154 */
155 function endFunction() {
156 functionStack.pop();
157 endBlock();
158 }
159
160 /**
161 * Records whether initialized or uninitialized variables are defined in current scope.
162 * @param {string} statementType node.kind, one of: "var", "let", or "const"
163 * @param {ASTNode[]} declarations List of declarations
164 * @param {Object} currentScope The scope being investigated
165 * @returns {void}
166 * @private
167 */
168 function recordTypes(statementType, declarations, currentScope) {
169 for (let i = 0; i < declarations.length; i++) {
170 if (declarations[i].init === null) {
171 if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) {
172 currentScope.uninitialized = true;
173 }
174 } else {
175 if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) {
176 currentScope.initialized = true;
177 }
178 }
179 }
180 }
181
182 /**
183 * Determines the current scope (function or block)
184 * @param {string} statementType node.kind, one of: "var", "let", or "const"
185 * @returns {Object} The scope associated with statementType
186 */
187 function getCurrentScope(statementType) {
188 let currentScope;
189
190 if (statementType === "var") {
191 currentScope = functionStack[functionStack.length - 1];
192 } else if (statementType === "let") {
193 currentScope = blockStack[blockStack.length - 1].let;
194 } else if (statementType === "const") {
195 currentScope = blockStack[blockStack.length - 1].const;
196 }
197 return currentScope;
198 }
199
200 /**
201 * Counts the number of initialized and uninitialized declarations in a list of declarations
202 * @param {ASTNode[]} declarations List of declarations
203 * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations
204 * @private
205 */
206 function countDeclarations(declarations) {
207 const counts = { uninitialized: 0, initialized: 0 };
208
209 for (let i = 0; i < declarations.length; i++) {
210 if (declarations[i].init === null) {
211 counts.uninitialized++;
212 } else {
213 counts.initialized++;
214 }
215 }
216 return counts;
217 }
218
219 /**
220 * Determines if there is more than one var statement in the current scope.
221 * @param {string} statementType node.kind, one of: "var", "let", or "const"
222 * @param {ASTNode[]} declarations List of declarations
223 * @returns {boolean} Returns true if it is the first var declaration, false if not.
224 * @private
225 */
226 function hasOnlyOneStatement(statementType, declarations) {
227
228 const declarationCounts = countDeclarations(declarations);
229 const currentOptions = options[statementType] || {};
230 const currentScope = getCurrentScope(statementType);
231
232 if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) {
233 if (currentScope.uninitialized || currentScope.initialized) {
234 return false;
235 }
236 }
237
238 if (declarationCounts.uninitialized > 0) {
239 if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) {
240 return false;
241 }
242 }
243 if (declarationCounts.initialized > 0) {
244 if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) {
245 return false;
246 }
247 }
248 recordTypes(statementType, declarations, currentScope);
249 return true;
250 }
251
252
253 //--------------------------------------------------------------------------
254 // Public API
255 //--------------------------------------------------------------------------
256
257 return {
258 Program: startFunction,
259 FunctionDeclaration: startFunction,
260 FunctionExpression: startFunction,
261 ArrowFunctionExpression: startFunction,
262 BlockStatement: startBlock,
263 ForStatement: startBlock,
264 ForInStatement: startBlock,
265 ForOfStatement: startBlock,
266 SwitchStatement: startBlock,
267
268 VariableDeclaration(node) {
269 const parent = node.parent;
270 const type = node.kind;
271
272 if (!options[type]) {
273 return;
274 }
275
276 const declarations = node.declarations;
277 const declarationCounts = countDeclarations(declarations);
278
279 // always
280 if (!hasOnlyOneStatement(type, declarations)) {
281 if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) {
282 context.report({
283 node,
284 message: "Combine this with the previous '{{type}}' statement.",
285 data: {
286 type
287 }
288 });
289 } else {
290 if (options[type].initialized === MODE_ALWAYS) {
291 context.report({
292 node,
293 message: "Combine this with the previous '{{type}}' statement with initialized variables.",
294 data: {
295 type
296 }
297 });
298 }
299 if (options[type].uninitialized === MODE_ALWAYS) {
300 if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) {
301 return;
302 }
303 context.report({
304 node,
305 message: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
306 data: {
307 type
308 }
309 });
310 }
311 }
312 }
313
314 // never
315 if (parent.type !== "ForStatement" || parent.init !== node) {
316 const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized;
317
318 if (totalDeclarations > 1) {
319
320 if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) {
321
322 // both initialized and uninitialized
323 context.report({
324 node,
325 message: "Split '{{type}}' declarations into multiple statements.",
326 data: {
327 type
328 }
329 });
330 } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) {
331
332 // initialized
333 context.report({
334 node,
335 message: "Split initialized '{{type}}' declarations into multiple statements.",
336 data: {
337 type
338 }
339 });
340 } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) {
341
342 // uninitialized
343 context.report({
344 node,
345 message: "Split uninitialized '{{type}}' declarations into multiple statements.",
346 data: {
347 type
348 }
349 });
350 }
351 }
352 }
353 },
354
355 "ForStatement:exit": endBlock,
356 "ForOfStatement:exit": endBlock,
357 "ForInStatement:exit": endBlock,
358 "SwitchStatement:exit": endBlock,
359 "BlockStatement:exit": endBlock,
360 "Program:exit": endFunction,
361 "FunctionDeclaration:exit": endFunction,
362 "FunctionExpression:exit": endFunction,
363 "ArrowFunctionExpression:exit": endFunction
364 };
365
366 }
367};