1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | "use strict";
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | module.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") {
|
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") {
|
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 |
|
119 |
|
120 |
|
121 | const functionStack = [];
|
122 | const blockStack = [];
|
123 |
|
124 | |
125 |
|
126 |
|
127 |
|
128 |
|
129 | function startBlock() {
|
130 | blockStack.push({
|
131 | let: { initialized: false, uninitialized: false },
|
132 | const: { initialized: false, uninitialized: false }
|
133 | });
|
134 | }
|
135 |
|
136 | |
137 |
|
138 |
|
139 |
|
140 |
|
141 | function startFunction() {
|
142 | functionStack.push({ initialized: false, uninitialized: false });
|
143 | startBlock();
|
144 | }
|
145 |
|
146 | |
147 |
|
148 |
|
149 |
|
150 |
|
151 | function endBlock() {
|
152 | blockStack.pop();
|
153 | }
|
154 |
|
155 | |
156 |
|
157 |
|
158 |
|
159 |
|
160 | function endFunction() {
|
161 | functionStack.pop();
|
162 | endBlock();
|
163 | }
|
164 |
|
165 | |
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 | function isRequire(decl) {
|
172 | return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require";
|
173 | }
|
174 |
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
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 |
|
203 |
|
204 |
|
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 |
|
221 |
|
222 |
|
223 |
|
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 |
|
240 |
|
241 |
|
242 |
|
243 |
|
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 |
|
277 |
|
278 |
|
279 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 | };
|