UNPKG

17.7 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 url: "https://eslint.org/docs/rules/one-var"
19 },
20
21 schema: [
22 {
23 oneOf: [
24 {
25 enum: ["always", "never", "consecutive"]
26 },
27 {
28 type: "object",
29 properties: {
30 separateRequires: {
31 type: "boolean"
32 },
33 var: {
34 enum: ["always", "never", "consecutive"]
35 },
36 let: {
37 enum: ["always", "never", "consecutive"]
38 },
39 const: {
40 enum: ["always", "never", "consecutive"]
41 }
42 },
43 additionalProperties: false
44 },
45 {
46 type: "object",
47 properties: {
48 initialized: {
49 enum: ["always", "never", "consecutive"]
50 },
51 uninitialized: {
52 enum: ["always", "never", "consecutive"]
53 }
54 },
55 additionalProperties: false
56 }
57 ]
58 }
59 ]
60 },
61
62 create(context) {
63 const MODE_ALWAYS = "always";
64 const MODE_NEVER = "never";
65 const MODE_CONSECUTIVE = "consecutive";
66 const mode = context.options[0] || MODE_ALWAYS;
67
68 const options = {};
69
70 if (typeof mode === "string") { // simple options configuration with just a string
71 options.var = { uninitialized: mode, initialized: mode };
72 options.let = { uninitialized: mode, initialized: mode };
73 options.const = { uninitialized: mode, initialized: mode };
74 } else if (typeof mode === "object") { // options configuration is an object
75 if (mode.hasOwnProperty("separateRequires")) {
76 options.separateRequires = !!mode.separateRequires;
77 }
78 if (mode.hasOwnProperty("var")) {
79 options.var = { uninitialized: mode.var, initialized: mode.var };
80 }
81 if (mode.hasOwnProperty("let")) {
82 options.let = { uninitialized: mode.let, initialized: mode.let };
83 }
84 if (mode.hasOwnProperty("const")) {
85 options.const = { uninitialized: mode.const, initialized: mode.const };
86 }
87 if (mode.hasOwnProperty("uninitialized")) {
88 if (!options.var) {
89 options.var = {};
90 }
91 if (!options.let) {
92 options.let = {};
93 }
94 if (!options.const) {
95 options.const = {};
96 }
97 options.var.uninitialized = mode.uninitialized;
98 options.let.uninitialized = mode.uninitialized;
99 options.const.uninitialized = mode.uninitialized;
100 }
101 if (mode.hasOwnProperty("initialized")) {
102 if (!options.var) {
103 options.var = {};
104 }
105 if (!options.let) {
106 options.let = {};
107 }
108 if (!options.const) {
109 options.const = {};
110 }
111 options.var.initialized = mode.initialized;
112 options.let.initialized = mode.initialized;
113 options.const.initialized = mode.initialized;
114 }
115 }
116
117 //--------------------------------------------------------------------------
118 // Helpers
119 //--------------------------------------------------------------------------
120
121 const functionStack = [];
122 const blockStack = [];
123
124 /**
125 * Increments the blockStack counter.
126 * @returns {void}
127 * @private
128 */
129 function startBlock() {
130 blockStack.push({
131 let: { initialized: false, uninitialized: false },
132 const: { initialized: false, uninitialized: false }
133 });
134 }
135
136 /**
137 * Increments the functionStack counter.
138 * @returns {void}
139 * @private
140 */
141 function startFunction() {
142 functionStack.push({ initialized: false, uninitialized: false });
143 startBlock();
144 }
145
146 /**
147 * Decrements the blockStack counter.
148 * @returns {void}
149 * @private
150 */
151 function endBlock() {
152 blockStack.pop();
153 }
154
155 /**
156 * Decrements the functionStack counter.
157 * @returns {void}
158 * @private
159 */
160 function endFunction() {
161 functionStack.pop();
162 endBlock();
163 }
164
165 /**
166 * Check if a variable declaration is a require.
167 * @param {ASTNode} decl variable declaration Node
168 * @returns {bool} if decl is a require, return true; else return false.
169 * @private
170 */
171 function isRequire(decl) {
172 return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require";
173 }
174
175 /**
176 * Records whether initialized/uninitialized/required variables are defined in current scope.
177 * @param {string} statementType node.kind, one of: "var", "let", or "const"
178 * @param {ASTNode[]} declarations List of declarations
179 * @param {Object} currentScope The scope being investigated
180 * @returns {void}
181 * @private
182 */
183 function recordTypes(statementType, declarations, currentScope) {
184 for (let i = 0; i < declarations.length; i++) {
185 if (declarations[i].init === null) {
186 if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) {
187 currentScope.uninitialized = true;
188 }
189 } else {
190 if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) {
191 if (options.separateRequires && isRequire(declarations[i])) {
192 currentScope.required = true;
193 } else {
194 currentScope.initialized = true;
195 }
196 }
197 }
198 }
199 }
200
201 /**
202 * Determines the current scope (function or block)
203 * @param {string} statementType node.kind, one of: "var", "let", or "const"
204 * @returns {Object} The scope associated with statementType
205 */
206 function getCurrentScope(statementType) {
207 let currentScope;
208
209 if (statementType === "var") {
210 currentScope = functionStack[functionStack.length - 1];
211 } else if (statementType === "let") {
212 currentScope = blockStack[blockStack.length - 1].let;
213 } else if (statementType === "const") {
214 currentScope = blockStack[blockStack.length - 1].const;
215 }
216 return currentScope;
217 }
218
219 /**
220 * Counts the number of initialized and uninitialized declarations in a list of declarations
221 * @param {ASTNode[]} declarations List of declarations
222 * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations
223 * @private
224 */
225 function countDeclarations(declarations) {
226 const counts = { uninitialized: 0, initialized: 0 };
227
228 for (let i = 0; i < declarations.length; i++) {
229 if (declarations[i].init === null) {
230 counts.uninitialized++;
231 } else {
232 counts.initialized++;
233 }
234 }
235 return counts;
236 }
237
238 /**
239 * Determines if there is more than one var statement in the current scope.
240 * @param {string} statementType node.kind, one of: "var", "let", or "const"
241 * @param {ASTNode[]} declarations List of declarations
242 * @returns {boolean} Returns true if it is the first var declaration, false if not.
243 * @private
244 */
245 function hasOnlyOneStatement(statementType, declarations) {
246
247 const declarationCounts = countDeclarations(declarations);
248 const currentOptions = options[statementType] || {};
249 const currentScope = getCurrentScope(statementType);
250 const hasRequires = declarations.some(isRequire);
251
252 if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) {
253 if (currentScope.uninitialized || currentScope.initialized) {
254 return false;
255 }
256 }
257
258 if (declarationCounts.uninitialized > 0) {
259 if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) {
260 return false;
261 }
262 }
263 if (declarationCounts.initialized > 0) {
264 if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) {
265 return false;
266 }
267 }
268 if (currentScope.required && hasRequires) {
269 return false;
270 }
271 recordTypes(statementType, declarations, currentScope);
272 return true;
273 }
274
275 /**
276 * Checks a given VariableDeclaration node for errors.
277 * @param {ASTNode} node The VariableDeclaration node to check
278 * @returns {void}
279 * @private
280 */
281 function checkVariableDeclaration(node) {
282 const parent = node.parent;
283 const type = node.kind;
284
285 if (!options[type]) {
286 return;
287 }
288
289 const declarations = node.declarations;
290 const declarationCounts = countDeclarations(declarations);
291 const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire);
292
293 if (options[type].initialized === MODE_ALWAYS) {
294 if (options.separateRequires && mixedRequires) {
295 context.report({
296 node,
297 message: "Split requires to be separated into a single block."
298 });
299 }
300 }
301
302 // consecutive
303 const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0;
304
305 if (nodeIndex > 0) {
306 const previousNode = parent.body[nodeIndex - 1];
307 const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration";
308
309 if (isPreviousNodeDeclaration && previousNode.kind === type) {
310 const previousDeclCounts = countDeclarations(previousNode.declarations);
311
312 if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) {
313 context.report({
314 node,
315 message: "Combine this with the previous '{{type}}' statement.",
316 data: {
317 type
318 }
319 });
320 } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) {
321 context.report({
322 node,
323 message: "Combine this with the previous '{{type}}' statement with initialized variables.",
324 data: {
325 type
326 }
327 });
328 } else if (options[type].uninitialized === MODE_CONSECUTIVE &&
329 declarationCounts.uninitialized > 0 &&
330 previousDeclCounts.uninitialized > 0) {
331 context.report({
332 node,
333 message: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
334 data: {
335 type
336 }
337 });
338 }
339 }
340 }
341
342 // always
343 if (!hasOnlyOneStatement(type, declarations)) {
344 if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) {
345 context.report({
346 node,
347 message: "Combine this with the previous '{{type}}' statement.",
348 data: {
349 type
350 }
351 });
352 } else {
353 if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) {
354 context.report({
355 node,
356 message: "Combine this with the previous '{{type}}' statement with initialized variables.",
357 data: {
358 type
359 }
360 });
361 }
362 if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) {
363 if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) {
364 return;
365 }
366 context.report({
367 node,
368 message: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
369 data: {
370 type
371 }
372 });
373 }
374 }
375 }
376
377 // never
378 if (parent.type !== "ForStatement" || parent.init !== node) {
379 const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized;
380
381 if (totalDeclarations > 1) {
382 if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) {
383
384 // both initialized and uninitialized
385 context.report({
386 node,
387 message: "Split '{{type}}' declarations into multiple statements.",
388 data: {
389 type
390 }
391 });
392 } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) {
393
394 // initialized
395 context.report({
396 node,
397 message: "Split initialized '{{type}}' declarations into multiple statements.",
398 data: {
399 type
400 }
401 });
402 } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) {
403
404 // uninitialized
405 context.report({
406 node,
407 message: "Split uninitialized '{{type}}' declarations into multiple statements.",
408 data: {
409 type
410 }
411 });
412 }
413 }
414 }
415 }
416
417 //--------------------------------------------------------------------------
418 // Public API
419 //--------------------------------------------------------------------------
420
421 return {
422 Program: startFunction,
423 FunctionDeclaration: startFunction,
424 FunctionExpression: startFunction,
425 ArrowFunctionExpression: startFunction,
426 BlockStatement: startBlock,
427 ForStatement: startBlock,
428 ForInStatement: startBlock,
429 ForOfStatement: startBlock,
430 SwitchStatement: startBlock,
431 VariableDeclaration: checkVariableDeclaration,
432 "ForStatement:exit": endBlock,
433 "ForOfStatement:exit": endBlock,
434 "ForInStatement:exit": endBlock,
435 "SwitchStatement:exit": endBlock,
436 "BlockStatement:exit": endBlock,
437 "Program:exit": endFunction,
438 "FunctionDeclaration:exit": endFunction,
439 "FunctionExpression:exit": endFunction,
440 "ArrowFunctionExpression:exit": endFunction
441 };
442
443 }
444};