UNPKG

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