UNPKG

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