UNPKG

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