UNPKG

13.1 kBJavaScriptView Raw
1/**
2 * @fileoverview A rule to suggest using of const declaration for variables that are never reassigned after declared.
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const lodash = require("lodash");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const PATTERN_TYPE = /^(?:.+?Pattern|RestElement|Property)$/;
19const DECLARATION_HOST_TYPE = /^(?:Program|BlockStatement|SwitchCase)$/;
20const DESTRUCTURING_HOST_TYPE = /^(?:VariableDeclarator|AssignmentExpression)$/;
21
22/**
23 * Adds multiple items to the tail of an array.
24 *
25 * @param {any[]} array - A destination to add.
26 * @param {any[]} values - Items to be added.
27 * @returns {void}
28 */
29const pushAll = Function.apply.bind(Array.prototype.push);
30
31/**
32 * Checks whether a given node is located at `ForStatement.init` or not.
33 *
34 * @param {ASTNode} node - A node to check.
35 * @returns {boolean} `true` if the node is located at `ForStatement.init`.
36 */
37function isInitOfForStatement(node) {
38 return node.parent.type === "ForStatement" && node.parent.init === node;
39}
40
41/**
42 * Checks whether a given Identifier node becomes a VariableDeclaration or not.
43 *
44 * @param {ASTNode} identifier - An Identifier node to check.
45 * @returns {boolean} `true` if the node can become a VariableDeclaration.
46 */
47function canBecomeVariableDeclaration(identifier) {
48 let node = identifier.parent;
49
50 while (PATTERN_TYPE.test(node.type)) {
51 node = node.parent;
52 }
53
54 return (
55 node.type === "VariableDeclarator" ||
56 (
57 node.type === "AssignmentExpression" &&
58 node.parent.type === "ExpressionStatement" &&
59 DECLARATION_HOST_TYPE.test(node.parent.parent.type)
60 )
61 );
62}
63
64/**
65 * Gets an identifier node of a given variable.
66 *
67 * If the initialization exists or one or more reading references exist before
68 * the first assignment, the identifier node is the node of the declaration.
69 * Otherwise, the identifier node is the node of the first assignment.
70 *
71 * If the variable should not change to const, this function returns null.
72 * - If the variable is reassigned.
73 * - If the variable is never initialized nor assigned.
74 * - If the variable is initialized in a different scope from the declaration.
75 * - If the unique assignment of the variable cannot change to a declaration.
76 * e.g. `if (a) b = 1` / `return (b = 1)`
77 * - If the variable is declared in the global scope and `eslintUsed` is `true`.
78 * `/*exported foo` directive comment makes such variables. This rule does not
79 * warn such variables because this rule cannot distinguish whether the
80 * exported variables are reassigned or not.
81 *
82 * @param {escope.Variable} variable - A variable to get.
83 * @param {boolean} ignoreReadBeforeAssign -
84 * The value of `ignoreReadBeforeAssign` option.
85 * @returns {ASTNode|null}
86 * An Identifier node if the variable should change to const.
87 * Otherwise, null.
88 */
89function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) {
90 if (variable.eslintUsed && variable.scope.type === "global") {
91 return null;
92 }
93
94 // Finds the unique WriteReference.
95 let writer = null;
96 let isReadBeforeInit = false;
97 const references = variable.references;
98
99 for (let i = 0; i < references.length; ++i) {
100 const reference = references[i];
101
102 if (reference.isWrite()) {
103 const isReassigned = (
104 writer !== null &&
105 writer.identifier !== reference.identifier
106 );
107
108 if (isReassigned) {
109 return null;
110 }
111 writer = reference;
112
113 } else if (reference.isRead() && writer === null) {
114 if (ignoreReadBeforeAssign) {
115 return null;
116 }
117 isReadBeforeInit = true;
118 }
119 }
120
121 // If the assignment is from a different scope, ignore it.
122 // If the assignment cannot change to a declaration, ignore it.
123 const shouldBeConst = (
124 writer !== null &&
125 writer.from === variable.scope &&
126 canBecomeVariableDeclaration(writer.identifier)
127 );
128
129 if (!shouldBeConst) {
130 return null;
131 }
132 if (isReadBeforeInit) {
133 return variable.defs[0].name;
134 }
135 return writer.identifier;
136}
137
138/**
139 * Gets the VariableDeclarator/AssignmentExpression node that a given reference
140 * belongs to.
141 * This is used to detect a mix of reassigned and never reassigned in a
142 * destructuring.
143 *
144 * @param {escope.Reference} reference - A reference to get.
145 * @returns {ASTNode|null} A VariableDeclarator/AssignmentExpression node or
146 * null.
147 */
148function getDestructuringHost(reference) {
149 if (!reference.isWrite()) {
150 return null;
151 }
152 let node = reference.identifier.parent;
153
154 while (PATTERN_TYPE.test(node.type)) {
155 node = node.parent;
156 }
157
158 if (!DESTRUCTURING_HOST_TYPE.test(node.type)) {
159 return null;
160 }
161 return node;
162}
163
164/**
165 * Groups by the VariableDeclarator/AssignmentExpression node that each
166 * reference of given variables belongs to.
167 * This is used to detect a mix of reassigned and never reassigned in a
168 * destructuring.
169 *
170 * @param {escope.Variable[]} variables - Variables to group by destructuring.
171 * @param {boolean} ignoreReadBeforeAssign -
172 * The value of `ignoreReadBeforeAssign` option.
173 * @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes.
174 */
175function groupByDestructuring(variables, ignoreReadBeforeAssign) {
176 const identifierMap = new Map();
177
178 for (let i = 0; i < variables.length; ++i) {
179 const variable = variables[i];
180 const references = variable.references;
181 const identifier = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign);
182 let prevId = null;
183
184 for (let j = 0; j < references.length; ++j) {
185 const reference = references[j];
186 const id = reference.identifier;
187
188 // Avoid counting a reference twice or more for default values of
189 // destructuring.
190 if (id === prevId) {
191 continue;
192 }
193 prevId = id;
194
195 // Add the identifier node into the destructuring group.
196 const group = getDestructuringHost(reference);
197
198 if (group) {
199 if (identifierMap.has(group)) {
200 identifierMap.get(group).push(identifier);
201 } else {
202 identifierMap.set(group, [identifier]);
203 }
204 }
205 }
206 }
207
208 return identifierMap;
209}
210
211/**
212 * Finds the nearest parent of node with a given type.
213 *
214 * @param {ASTNode} node – The node to search from.
215 * @param {string} type – The type field of the parent node.
216 * @param {Function} shouldStop – a predicate that returns true if the traversal should stop, and false otherwise.
217 * @returns {ASTNode} The closest ancestor with the specified type; null if no such ancestor exists.
218 */
219function findUp(node, type, shouldStop) {
220 if (!node || shouldStop(node)) {
221 return null;
222 }
223 if (node.type === type) {
224 return node;
225 }
226 return findUp(node.parent, type, shouldStop);
227}
228
229//------------------------------------------------------------------------------
230// Rule Definition
231//------------------------------------------------------------------------------
232
233module.exports = {
234 meta: {
235 docs: {
236 description: "require `const` declarations for variables that are never reassigned after declared",
237 category: "ECMAScript 6",
238 recommended: false
239 },
240
241 fixable: "code",
242
243 schema: [
244 {
245 type: "object",
246 properties: {
247 destructuring: {enum: ["any", "all"]},
248 ignoreReadBeforeAssign: {type: "boolean"}
249 },
250 additionalProperties: false
251 }
252 ]
253 },
254
255 create(context) {
256 const options = context.options[0] || {};
257 const checkingMixedDestructuring = options.destructuring !== "all";
258 const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true;
259 let variables = null;
260
261 /**
262 * Reports a given Identifier node.
263 *
264 * @param {ASTNode} node - An Identifier node to report.
265 * @returns {void}
266 */
267 function report(node) {
268 const reportArgs = {
269 node,
270 message: "'{{name}}' is never reassigned. Use 'const' instead.",
271 data: node
272 },
273 varDeclParent = findUp(node, "VariableDeclaration", function(parentNode) {
274 return lodash.endsWith(parentNode.type, "Statement");
275 }),
276 isNormalVarDecl = (node.parent.parent.parent.type === "ForInStatement" ||
277 node.parent.parent.parent.type === "ForOfStatement" ||
278 node.parent.init),
279
280 isDestructuringVarDecl =
281
282 // {let {a} = obj} should be written as {const {a} = obj}
283 (node.parent.parent.type === "ObjectPattern" &&
284
285 // If options.destucturing is "all", then this warning will not occur unless
286 // every assignment in the destructuring should be const. In that case, it's safe
287 // to apply the fix. Otherwise, it's safe to apply the fix if there's only one
288 // assignment occurring. If there is more than one assignment and options.destructuring
289 // is not "all", then it's not clear how the developer would want to resolve the issue,
290 // so we should not attempt to do it programmatically.
291 (options.destructuring === "all" || node.parent.parent.properties.length === 1)) ||
292
293 // {let [a] = [1]} should be written as {const [a] = [1]}
294 (node.parent.type === "ArrayPattern" &&
295
296 // See note above about fixing multiple warnings at once.
297 (options.destructuring === "all" || node.parent.elements.length === 1));
298
299 if (varDeclParent &&
300 (isNormalVarDecl || isDestructuringVarDecl) &&
301
302 // If there are multiple variable declarations, like {let a = 1, b = 2}, then
303 // do not attempt to fix if one of the declarations should be `const`. It's
304 // too hard to know how the developer would want to automatically resolve the issue.
305 varDeclParent.declarations.length === 1) {
306
307 reportArgs.fix = function(fixer) {
308 return fixer.replaceTextRange(
309 [varDeclParent.start, varDeclParent.start + "let".length],
310 "const"
311 );
312 };
313 }
314
315 context.report(reportArgs);
316 }
317
318 /**
319 * Reports a given variable if the variable should be declared as const.
320 *
321 * @param {escope.Variable} variable - A variable to report.
322 * @returns {void}
323 */
324 function checkVariable(variable) {
325 const node = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign);
326
327 if (node) {
328 report(node);
329 }
330 }
331
332 /**
333 * Reports given identifier nodes if all of the nodes should be declared
334 * as const.
335 *
336 * The argument 'nodes' is an array of Identifier nodes.
337 * This node is the result of 'getIdentifierIfShouldBeConst()', so it's
338 * nullable. In simple declaration or assignment cases, the length of
339 * the array is 1. In destructuring cases, the length of the array can
340 * be 2 or more.
341 *
342 * @param {(escope.Reference|null)[]} nodes -
343 * References which are grouped by destructuring to report.
344 * @returns {void}
345 */
346 function checkGroup(nodes) {
347 if (nodes.every(Boolean)) {
348 nodes.forEach(report);
349 }
350 }
351
352 return {
353 Program() {
354 variables = [];
355 },
356
357 "Program:exit"() {
358 if (checkingMixedDestructuring) {
359 variables.forEach(checkVariable);
360 } else {
361 groupByDestructuring(variables, ignoreReadBeforeAssign)
362 .forEach(checkGroup);
363 }
364
365 variables = null;
366 },
367
368 VariableDeclaration(node) {
369 if (node.kind === "let" && !isInitOfForStatement(node)) {
370 pushAll(variables, context.getDeclaredVariables(node));
371 }
372 }
373 };
374 }
375};