]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview A rule to control the use of single variable declarations. | |
3 | * @author Ian Christian Myers | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
456be15e TL |
8 | //------------------------------------------------------------------------------ |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
14 | //------------------------------------------------------------------------------ | |
15 | // Helpers | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | /** | |
19 | * Determines whether the given node is in a statement list. | |
20 | * @param {ASTNode} node node to check | |
21 | * @returns {boolean} `true` if the given node is in a statement list | |
22 | */ | |
23 | function isInStatementList(node) { | |
24 | return astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type); | |
25 | } | |
26 | ||
eb39fafa DC |
27 | //------------------------------------------------------------------------------ |
28 | // Rule Definition | |
29 | //------------------------------------------------------------------------------ | |
30 | ||
31 | module.exports = { | |
32 | meta: { | |
33 | type: "suggestion", | |
34 | ||
35 | docs: { | |
36 | description: "enforce variables to be declared either together or separately in functions", | |
eb39fafa DC |
37 | recommended: false, |
38 | url: "https://eslint.org/docs/rules/one-var" | |
39 | }, | |
40 | ||
41 | fixable: "code", | |
42 | ||
43 | schema: [ | |
44 | { | |
45 | oneOf: [ | |
46 | { | |
47 | enum: ["always", "never", "consecutive"] | |
48 | }, | |
49 | { | |
50 | type: "object", | |
51 | properties: { | |
52 | separateRequires: { | |
53 | type: "boolean" | |
54 | }, | |
55 | var: { | |
56 | enum: ["always", "never", "consecutive"] | |
57 | }, | |
58 | let: { | |
59 | enum: ["always", "never", "consecutive"] | |
60 | }, | |
61 | const: { | |
62 | enum: ["always", "never", "consecutive"] | |
63 | } | |
64 | }, | |
65 | additionalProperties: false | |
66 | }, | |
67 | { | |
68 | type: "object", | |
69 | properties: { | |
70 | initialized: { | |
71 | enum: ["always", "never", "consecutive"] | |
72 | }, | |
73 | uninitialized: { | |
74 | enum: ["always", "never", "consecutive"] | |
75 | } | |
76 | }, | |
77 | additionalProperties: false | |
78 | } | |
79 | ] | |
80 | } | |
81 | ], | |
82 | ||
83 | messages: { | |
84 | combineUninitialized: "Combine this with the previous '{{type}}' statement with uninitialized variables.", | |
85 | combineInitialized: "Combine this with the previous '{{type}}' statement with initialized variables.", | |
86 | splitUninitialized: "Split uninitialized '{{type}}' declarations into multiple statements.", | |
87 | splitInitialized: "Split initialized '{{type}}' declarations into multiple statements.", | |
88 | splitRequires: "Split requires to be separated into a single block.", | |
89 | combine: "Combine this with the previous '{{type}}' statement.", | |
90 | split: "Split '{{type}}' declarations into multiple statements." | |
91 | } | |
92 | }, | |
93 | ||
94 | create(context) { | |
95 | const MODE_ALWAYS = "always"; | |
96 | const MODE_NEVER = "never"; | |
97 | const MODE_CONSECUTIVE = "consecutive"; | |
98 | const mode = context.options[0] || MODE_ALWAYS; | |
99 | ||
100 | const options = {}; | |
101 | ||
102 | if (typeof mode === "string") { // simple options configuration with just a string | |
103 | options.var = { uninitialized: mode, initialized: mode }; | |
104 | options.let = { uninitialized: mode, initialized: mode }; | |
105 | options.const = { uninitialized: mode, initialized: mode }; | |
106 | } else if (typeof mode === "object") { // options configuration is an object | |
107 | options.separateRequires = !!mode.separateRequires; | |
108 | options.var = { uninitialized: mode.var, initialized: mode.var }; | |
109 | options.let = { uninitialized: mode.let, initialized: mode.let }; | |
110 | options.const = { uninitialized: mode.const, initialized: mode.const }; | |
111 | if (Object.prototype.hasOwnProperty.call(mode, "uninitialized")) { | |
112 | options.var.uninitialized = mode.uninitialized; | |
113 | options.let.uninitialized = mode.uninitialized; | |
114 | options.const.uninitialized = mode.uninitialized; | |
115 | } | |
116 | if (Object.prototype.hasOwnProperty.call(mode, "initialized")) { | |
117 | options.var.initialized = mode.initialized; | |
118 | options.let.initialized = mode.initialized; | |
119 | options.const.initialized = mode.initialized; | |
120 | } | |
121 | } | |
122 | ||
123 | const sourceCode = context.getSourceCode(); | |
124 | ||
125 | //-------------------------------------------------------------------------- | |
126 | // Helpers | |
127 | //-------------------------------------------------------------------------- | |
128 | ||
129 | const functionStack = []; | |
130 | const blockStack = []; | |
131 | ||
132 | /** | |
133 | * Increments the blockStack counter. | |
134 | * @returns {void} | |
135 | * @private | |
136 | */ | |
137 | function startBlock() { | |
138 | blockStack.push({ | |
139 | let: { initialized: false, uninitialized: false }, | |
140 | const: { initialized: false, uninitialized: false } | |
141 | }); | |
142 | } | |
143 | ||
144 | /** | |
145 | * Increments the functionStack counter. | |
146 | * @returns {void} | |
147 | * @private | |
148 | */ | |
149 | function startFunction() { | |
150 | functionStack.push({ initialized: false, uninitialized: false }); | |
151 | startBlock(); | |
152 | } | |
153 | ||
154 | /** | |
155 | * Decrements the blockStack counter. | |
156 | * @returns {void} | |
157 | * @private | |
158 | */ | |
159 | function endBlock() { | |
160 | blockStack.pop(); | |
161 | } | |
162 | ||
163 | /** | |
164 | * Decrements the functionStack counter. | |
165 | * @returns {void} | |
166 | * @private | |
167 | */ | |
168 | function endFunction() { | |
169 | functionStack.pop(); | |
170 | endBlock(); | |
171 | } | |
172 | ||
173 | /** | |
174 | * Check if a variable declaration is a require. | |
175 | * @param {ASTNode} decl variable declaration Node | |
176 | * @returns {bool} if decl is a require, return true; else return false. | |
177 | * @private | |
178 | */ | |
179 | function isRequire(decl) { | |
180 | return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require"; | |
181 | } | |
182 | ||
183 | /** | |
184 | * Records whether initialized/uninitialized/required variables are defined in current scope. | |
185 | * @param {string} statementType node.kind, one of: "var", "let", or "const" | |
186 | * @param {ASTNode[]} declarations List of declarations | |
187 | * @param {Object} currentScope The scope being investigated | |
188 | * @returns {void} | |
189 | * @private | |
190 | */ | |
191 | function recordTypes(statementType, declarations, currentScope) { | |
192 | for (let i = 0; i < declarations.length; i++) { | |
193 | if (declarations[i].init === null) { | |
194 | if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) { | |
195 | currentScope.uninitialized = true; | |
196 | } | |
197 | } else { | |
198 | if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) { | |
199 | if (options.separateRequires && isRequire(declarations[i])) { | |
200 | currentScope.required = true; | |
201 | } else { | |
202 | currentScope.initialized = true; | |
203 | } | |
204 | } | |
205 | } | |
206 | } | |
207 | } | |
208 | ||
209 | /** | |
210 | * Determines the current scope (function or block) | |
609c276f | 211 | * @param {string} statementType node.kind, one of: "var", "let", or "const" |
eb39fafa DC |
212 | * @returns {Object} The scope associated with statementType |
213 | */ | |
214 | function getCurrentScope(statementType) { | |
215 | let currentScope; | |
216 | ||
217 | if (statementType === "var") { | |
218 | currentScope = functionStack[functionStack.length - 1]; | |
219 | } else if (statementType === "let") { | |
220 | currentScope = blockStack[blockStack.length - 1].let; | |
221 | } else if (statementType === "const") { | |
222 | currentScope = blockStack[blockStack.length - 1].const; | |
223 | } | |
224 | return currentScope; | |
225 | } | |
226 | ||
227 | /** | |
228 | * Counts the number of initialized and uninitialized declarations in a list of declarations | |
229 | * @param {ASTNode[]} declarations List of declarations | |
230 | * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations | |
231 | * @private | |
232 | */ | |
233 | function countDeclarations(declarations) { | |
234 | const counts = { uninitialized: 0, initialized: 0 }; | |
235 | ||
236 | for (let i = 0; i < declarations.length; i++) { | |
237 | if (declarations[i].init === null) { | |
238 | counts.uninitialized++; | |
239 | } else { | |
240 | counts.initialized++; | |
241 | } | |
242 | } | |
243 | return counts; | |
244 | } | |
245 | ||
246 | /** | |
247 | * Determines if there is more than one var statement in the current scope. | |
248 | * @param {string} statementType node.kind, one of: "var", "let", or "const" | |
249 | * @param {ASTNode[]} declarations List of declarations | |
250 | * @returns {boolean} Returns true if it is the first var declaration, false if not. | |
251 | * @private | |
252 | */ | |
253 | function hasOnlyOneStatement(statementType, declarations) { | |
254 | ||
255 | const declarationCounts = countDeclarations(declarations); | |
256 | const currentOptions = options[statementType] || {}; | |
257 | const currentScope = getCurrentScope(statementType); | |
258 | const hasRequires = declarations.some(isRequire); | |
259 | ||
260 | if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) { | |
261 | if (currentScope.uninitialized || currentScope.initialized) { | |
262 | if (!hasRequires) { | |
263 | return false; | |
264 | } | |
265 | } | |
266 | } | |
267 | ||
268 | if (declarationCounts.uninitialized > 0) { | |
269 | if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) { | |
270 | return false; | |
271 | } | |
272 | } | |
273 | if (declarationCounts.initialized > 0) { | |
274 | if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) { | |
275 | if (!hasRequires) { | |
276 | return false; | |
277 | } | |
278 | } | |
279 | } | |
280 | if (currentScope.required && hasRequires) { | |
281 | return false; | |
282 | } | |
283 | recordTypes(statementType, declarations, currentScope); | |
284 | return true; | |
285 | } | |
286 | ||
287 | /** | |
288 | * Fixer to join VariableDeclaration's into a single declaration | |
456be15e TL |
289 | * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join |
290 | * @returns {Function} The fixer function | |
eb39fafa DC |
291 | */ |
292 | function joinDeclarations(declarations) { | |
293 | const declaration = declarations[0]; | |
294 | const body = Array.isArray(declaration.parent.parent.body) ? declaration.parent.parent.body : []; | |
295 | const currentIndex = body.findIndex(node => node.range[0] === declaration.parent.range[0]); | |
296 | const previousNode = body[currentIndex - 1]; | |
297 | ||
298 | return fixer => { | |
299 | const type = sourceCode.getTokenBefore(declaration); | |
300 | const prevSemi = sourceCode.getTokenBefore(type); | |
301 | const res = []; | |
302 | ||
303 | if (previousNode && previousNode.kind === sourceCode.getText(type)) { | |
304 | if (prevSemi.value === ";") { | |
305 | res.push(fixer.replaceText(prevSemi, ",")); | |
306 | } else { | |
307 | res.push(fixer.insertTextAfter(prevSemi, ",")); | |
308 | } | |
309 | res.push(fixer.replaceText(type, "")); | |
310 | } | |
311 | ||
312 | return res; | |
313 | }; | |
314 | } | |
315 | ||
316 | /** | |
317 | * Fixer to split a VariableDeclaration into individual declarations | |
456be15e TL |
318 | * @param {VariableDeclaration} declaration The `VariableDeclaration` to split |
319 | * @returns {Function|null} The fixer function | |
eb39fafa DC |
320 | */ |
321 | function splitDeclarations(declaration) { | |
456be15e TL |
322 | const { parent } = declaration; |
323 | ||
324 | // don't autofix code such as: if (foo) var x, y; | |
325 | if (!isInStatementList(parent.type === "ExportNamedDeclaration" ? parent : declaration)) { | |
326 | return null; | |
327 | } | |
328 | ||
eb39fafa DC |
329 | return fixer => declaration.declarations.map(declarator => { |
330 | const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator); | |
331 | ||
332 | if (tokenAfterDeclarator === null) { | |
333 | return null; | |
334 | } | |
335 | ||
336 | const afterComma = sourceCode.getTokenAfter(tokenAfterDeclarator, { includeComments: true }); | |
337 | ||
338 | if (tokenAfterDeclarator.value !== ",") { | |
339 | return null; | |
340 | } | |
341 | ||
456be15e TL |
342 | const exportPlacement = declaration.parent.type === "ExportNamedDeclaration" ? "export " : ""; |
343 | ||
eb39fafa DC |
344 | /* |
345 | * `var x,y` | |
346 | * tokenAfterDeclarator ^^ afterComma | |
347 | */ | |
348 | if (afterComma.range[0] === tokenAfterDeclarator.range[1]) { | |
456be15e | 349 | return fixer.replaceText(tokenAfterDeclarator, `; ${exportPlacement}${declaration.kind} `); |
eb39fafa DC |
350 | } |
351 | ||
352 | /* | |
353 | * `var x, | |
354 | * tokenAfterDeclarator ^ | |
355 | * y` | |
356 | * ^ afterComma | |
357 | */ | |
358 | if ( | |
359 | afterComma.loc.start.line > tokenAfterDeclarator.loc.end.line || | |
360 | afterComma.type === "Line" || | |
361 | afterComma.type === "Block" | |
362 | ) { | |
363 | let lastComment = afterComma; | |
364 | ||
365 | while (lastComment.type === "Line" || lastComment.type === "Block") { | |
366 | lastComment = sourceCode.getTokenAfter(lastComment, { includeComments: true }); | |
367 | } | |
368 | ||
369 | return fixer.replaceTextRange( | |
370 | [tokenAfterDeclarator.range[0], lastComment.range[0]], | |
456be15e | 371 | `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${exportPlacement}${declaration.kind} ` |
eb39fafa DC |
372 | ); |
373 | } | |
374 | ||
456be15e | 375 | return fixer.replaceText(tokenAfterDeclarator, `; ${exportPlacement}${declaration.kind}`); |
eb39fafa DC |
376 | }).filter(x => x); |
377 | } | |
378 | ||
379 | /** | |
380 | * Checks a given VariableDeclaration node for errors. | |
381 | * @param {ASTNode} node The VariableDeclaration node to check | |
382 | * @returns {void} | |
383 | * @private | |
384 | */ | |
385 | function checkVariableDeclaration(node) { | |
386 | const parent = node.parent; | |
387 | const type = node.kind; | |
388 | ||
389 | if (!options[type]) { | |
390 | return; | |
391 | } | |
392 | ||
393 | const declarations = node.declarations; | |
394 | const declarationCounts = countDeclarations(declarations); | |
395 | const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire); | |
396 | ||
397 | if (options[type].initialized === MODE_ALWAYS) { | |
398 | if (options.separateRequires && mixedRequires) { | |
399 | context.report({ | |
400 | node, | |
401 | messageId: "splitRequires" | |
402 | }); | |
403 | } | |
404 | } | |
405 | ||
406 | // consecutive | |
407 | const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0; | |
408 | ||
409 | if (nodeIndex > 0) { | |
410 | const previousNode = parent.body[nodeIndex - 1]; | |
411 | const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration"; | |
412 | const declarationsWithPrevious = declarations.concat(previousNode.declarations || []); | |
413 | ||
414 | if ( | |
415 | isPreviousNodeDeclaration && | |
416 | previousNode.kind === type && | |
417 | !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire)) | |
418 | ) { | |
419 | const previousDeclCounts = countDeclarations(previousNode.declarations); | |
420 | ||
421 | if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) { | |
422 | context.report({ | |
423 | node, | |
424 | messageId: "combine", | |
425 | data: { | |
426 | type | |
427 | }, | |
428 | fix: joinDeclarations(declarations) | |
429 | }); | |
430 | } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) { | |
431 | context.report({ | |
432 | node, | |
433 | messageId: "combineInitialized", | |
434 | data: { | |
435 | type | |
436 | }, | |
437 | fix: joinDeclarations(declarations) | |
438 | }); | |
439 | } else if (options[type].uninitialized === MODE_CONSECUTIVE && | |
440 | declarationCounts.uninitialized > 0 && | |
441 | previousDeclCounts.uninitialized > 0) { | |
442 | context.report({ | |
443 | node, | |
444 | messageId: "combineUninitialized", | |
445 | data: { | |
446 | type | |
447 | }, | |
448 | fix: joinDeclarations(declarations) | |
449 | }); | |
450 | } | |
451 | } | |
452 | } | |
453 | ||
454 | // always | |
455 | if (!hasOnlyOneStatement(type, declarations)) { | |
456 | if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) { | |
457 | context.report({ | |
458 | node, | |
459 | messageId: "combine", | |
460 | data: { | |
461 | type | |
462 | }, | |
463 | fix: joinDeclarations(declarations) | |
464 | }); | |
465 | } else { | |
466 | if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) { | |
467 | context.report({ | |
468 | node, | |
469 | messageId: "combineInitialized", | |
470 | data: { | |
471 | type | |
472 | }, | |
473 | fix: joinDeclarations(declarations) | |
474 | }); | |
475 | } | |
476 | if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) { | |
477 | if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) { | |
478 | return; | |
479 | } | |
480 | context.report({ | |
481 | node, | |
482 | messageId: "combineUninitialized", | |
483 | data: { | |
484 | type | |
485 | }, | |
486 | fix: joinDeclarations(declarations) | |
487 | }); | |
488 | } | |
489 | } | |
490 | } | |
491 | ||
492 | // never | |
493 | if (parent.type !== "ForStatement" || parent.init !== node) { | |
494 | const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized; | |
495 | ||
496 | if (totalDeclarations > 1) { | |
497 | if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) { | |
498 | ||
499 | // both initialized and uninitialized | |
500 | context.report({ | |
501 | node, | |
502 | messageId: "split", | |
503 | data: { | |
504 | type | |
505 | }, | |
506 | fix: splitDeclarations(node) | |
507 | }); | |
508 | } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) { | |
509 | ||
510 | // initialized | |
511 | context.report({ | |
512 | node, | |
513 | messageId: "splitInitialized", | |
514 | data: { | |
515 | type | |
516 | }, | |
517 | fix: splitDeclarations(node) | |
518 | }); | |
519 | } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) { | |
520 | ||
521 | // uninitialized | |
522 | context.report({ | |
523 | node, | |
524 | messageId: "splitUninitialized", | |
525 | data: { | |
526 | type | |
527 | }, | |
528 | fix: splitDeclarations(node) | |
529 | }); | |
530 | } | |
531 | } | |
532 | } | |
533 | } | |
534 | ||
535 | //-------------------------------------------------------------------------- | |
536 | // Public API | |
537 | //-------------------------------------------------------------------------- | |
538 | ||
539 | return { | |
540 | Program: startFunction, | |
541 | FunctionDeclaration: startFunction, | |
542 | FunctionExpression: startFunction, | |
543 | ArrowFunctionExpression: startFunction, | |
609c276f TL |
544 | StaticBlock: startFunction, // StaticBlock creates a new scope for `var` variables |
545 | ||
eb39fafa DC |
546 | BlockStatement: startBlock, |
547 | ForStatement: startBlock, | |
548 | ForInStatement: startBlock, | |
549 | ForOfStatement: startBlock, | |
550 | SwitchStatement: startBlock, | |
551 | VariableDeclaration: checkVariableDeclaration, | |
552 | "ForStatement:exit": endBlock, | |
553 | "ForOfStatement:exit": endBlock, | |
554 | "ForInStatement:exit": endBlock, | |
555 | "SwitchStatement:exit": endBlock, | |
556 | "BlockStatement:exit": endBlock, | |
609c276f | 557 | |
eb39fafa DC |
558 | "Program:exit": endFunction, |
559 | "FunctionDeclaration:exit": endFunction, | |
560 | "FunctionExpression:exit": endFunction, | |
609c276f TL |
561 | "ArrowFunctionExpression:exit": endFunction, |
562 | "StaticBlock:exit": endFunction | |
eb39fafa DC |
563 | }; |
564 | ||
565 | } | |
566 | }; |