2 * @fileoverview A rule to suggest using of const declaration for variables that are never reassigned after declared.
3 * @author Toru Nagashima
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const FixTracker
= require("./utils/fix-tracker");
13 const astUtils
= require("./utils/ast-utils");
15 //------------------------------------------------------------------------------
17 //------------------------------------------------------------------------------
19 const PATTERN_TYPE
= /^(?:.+?Pattern|RestElement|SpreadProperty|ExperimentalRestProperty|Property)$/u;
20 const DECLARATION_HOST_TYPE
= /^(?:Program|BlockStatement|StaticBlock|SwitchCase)$/u;
21 const DESTRUCTURING_HOST_TYPE
= /^(?:VariableDeclarator|AssignmentExpression)$/u;
24 * Checks whether a given node is located at `ForStatement.init` or not.
25 * @param {ASTNode} node A node to check.
26 * @returns {boolean} `true` if the node is located at `ForStatement.init`.
28 function isInitOfForStatement(node
) {
29 return node
.parent
.type
=== "ForStatement" && node
.parent
.init
=== node
;
33 * Checks whether a given Identifier node becomes a VariableDeclaration or not.
34 * @param {ASTNode} identifier An Identifier node to check.
35 * @returns {boolean} `true` if the node can become a VariableDeclaration.
37 function canBecomeVariableDeclaration(identifier
) {
38 let node
= identifier
.parent
;
40 while (PATTERN_TYPE
.test(node
.type
)) {
45 node
.type
=== "VariableDeclarator" ||
47 node
.type
=== "AssignmentExpression" &&
48 node
.parent
.type
=== "ExpressionStatement" &&
49 DECLARATION_HOST_TYPE
.test(node
.parent
.parent
.type
)
55 * Checks if an property or element is from outer scope or function parameters
56 * in destructing pattern.
57 * @param {string} name A variable name to be checked.
58 * @param {eslint-scope.Scope} initScope A scope to start find.
59 * @returns {boolean} Indicates if the variable is from outer scope or function parameters.
61 function isOuterVariableInDestructing(name
, initScope
) {
63 if (initScope
.through
.some(ref
=> ref
.resolved
&& ref
.resolved
.name
=== name
)) {
67 const variable
= astUtils
.getVariableByName(initScope
, name
);
69 if (variable
!== null) {
70 return variable
.defs
.some(def
=> def
.type
=== "Parameter");
77 * Gets the VariableDeclarator/AssignmentExpression node that a given reference
79 * This is used to detect a mix of reassigned and never reassigned in a
81 * @param {eslint-scope.Reference} reference A reference to get.
82 * @returns {ASTNode|null} A VariableDeclarator/AssignmentExpression node or
85 function getDestructuringHost(reference
) {
86 if (!reference
.isWrite()) {
89 let node
= reference
.identifier
.parent
;
91 while (PATTERN_TYPE
.test(node
.type
)) {
95 if (!DESTRUCTURING_HOST_TYPE
.test(node
.type
)) {
102 * Determines if a destructuring assignment node contains
103 * any MemberExpression nodes. This is used to determine if a
104 * variable that is only written once using destructuring can be
105 * safely converted into a const declaration.
106 * @param {ASTNode} node The ObjectPattern or ArrayPattern node to check.
107 * @returns {boolean} True if the destructuring pattern contains
108 * a MemberExpression, false if not.
110 function hasMemberExpressionAssignment(node
) {
112 case "ObjectPattern":
113 return node
.properties
.some(prop
=> {
117 * Spread elements have an argument property while
118 * others have a value property. Because different
119 * parsers use different node types for spread elements,
120 * we just check if there is an argument property.
122 return hasMemberExpressionAssignment(prop
.argument
|| prop
.value
);
129 return node
.elements
.some(element
=> {
131 return hasMemberExpressionAssignment(element
);
137 case "AssignmentPattern":
138 return hasMemberExpressionAssignment(node
.left
);
140 case "MemberExpression":
150 * Gets an identifier node of a given variable.
152 * If the initialization exists or one or more reading references exist before
153 * the first assignment, the identifier node is the node of the declaration.
154 * Otherwise, the identifier node is the node of the first assignment.
156 * If the variable should not change to const, this function returns null.
157 * - If the variable is reassigned.
158 * - If the variable is never initialized nor assigned.
159 * - If the variable is initialized in a different scope from the declaration.
160 * - If the unique assignment of the variable cannot change to a declaration.
161 * e.g. `if (a) b = 1` / `return (b = 1)`
162 * - If the variable is declared in the global scope and `eslintUsed` is `true`.
163 * `/*exported foo` directive comment makes such variables. This rule does not
164 * warn such variables because this rule cannot distinguish whether the
165 * exported variables are reassigned or not.
166 * @param {eslint-scope.Variable} variable A variable to get.
167 * @param {boolean} ignoreReadBeforeAssign
168 * The value of `ignoreReadBeforeAssign` option.
169 * @returns {ASTNode|null}
170 * An Identifier node if the variable should change to const.
173 function getIdentifierIfShouldBeConst(variable
, ignoreReadBeforeAssign
) {
174 if (variable
.eslintUsed
&& variable
.scope
.type
=== "global") {
178 // Finds the unique WriteReference.
180 let isReadBeforeInit
= false;
181 const references
= variable
.references
;
183 for (let i
= 0; i
< references
.length
; ++i
) {
184 const reference
= references
[i
];
186 if (reference
.isWrite()) {
187 const isReassigned
= (
189 writer
.identifier
!== reference
.identifier
196 const destructuringHost
= getDestructuringHost(reference
);
198 if (destructuringHost
!== null && destructuringHost
.left
!== void 0) {
199 const leftNode
= destructuringHost
.left
;
200 let hasOuterVariables
= false,
201 hasNonIdentifiers
= false;
203 if (leftNode
.type
=== "ObjectPattern") {
204 const properties
= leftNode
.properties
;
206 hasOuterVariables
= properties
207 .filter(prop
=> prop
.value
)
208 .map(prop
=> prop
.value
.name
)
209 .some(name
=> isOuterVariableInDestructing(name
, variable
.scope
));
211 hasNonIdentifiers
= hasMemberExpressionAssignment(leftNode
);
213 } else if (leftNode
.type
=== "ArrayPattern") {
214 const elements
= leftNode
.elements
;
216 hasOuterVariables
= elements
217 .map(element
=> element
&& element
.name
)
218 .some(name
=> isOuterVariableInDestructing(name
, variable
.scope
));
220 hasNonIdentifiers
= hasMemberExpressionAssignment(leftNode
);
223 if (hasOuterVariables
|| hasNonIdentifiers
) {
231 } else if (reference
.isRead() && writer
=== null) {
232 if (ignoreReadBeforeAssign
) {
235 isReadBeforeInit
= true;
240 * If the assignment is from a different scope, ignore it.
241 * If the assignment cannot change to a declaration, ignore it.
243 const shouldBeConst
= (
245 writer
.from === variable
.scope
&&
246 canBecomeVariableDeclaration(writer
.identifier
)
249 if (!shouldBeConst
) {
253 if (isReadBeforeInit
) {
254 return variable
.defs
[0].name
;
257 return writer
.identifier
;
261 * Groups by the VariableDeclarator/AssignmentExpression node that each
262 * reference of given variables belongs to.
263 * This is used to detect a mix of reassigned and never reassigned in a
265 * @param {eslint-scope.Variable[]} variables Variables to group by destructuring.
266 * @param {boolean} ignoreReadBeforeAssign
267 * The value of `ignoreReadBeforeAssign` option.
268 * @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes.
270 function groupByDestructuring(variables
, ignoreReadBeforeAssign
) {
271 const identifierMap
= new Map();
273 for (let i
= 0; i
< variables
.length
; ++i
) {
274 const variable
= variables
[i
];
275 const references
= variable
.references
;
276 const identifier
= getIdentifierIfShouldBeConst(variable
, ignoreReadBeforeAssign
);
279 for (let j
= 0; j
< references
.length
; ++j
) {
280 const reference
= references
[j
];
281 const id
= reference
.identifier
;
284 * Avoid counting a reference twice or more for default values of
292 // Add the identifier node into the destructuring group.
293 const group
= getDestructuringHost(reference
);
296 if (identifierMap
.has(group
)) {
297 identifierMap
.get(group
).push(identifier
);
299 identifierMap
.set(group
, [identifier
]);
305 return identifierMap
;
309 * Finds the nearest parent of node with a given type.
310 * @param {ASTNode} node The node to search from.
311 * @param {string} type The type field of the parent node.
312 * @param {Function} shouldStop A predicate that returns true if the traversal should stop, and false otherwise.
313 * @returns {ASTNode} The closest ancestor with the specified type; null if no such ancestor exists.
315 function findUp(node
, type
, shouldStop
) {
316 if (!node
|| shouldStop(node
)) {
319 if (node
.type
=== type
) {
322 return findUp(node
.parent
, type
, shouldStop
);
325 //------------------------------------------------------------------------------
327 //------------------------------------------------------------------------------
329 /** @type {import('../shared/types').Rule} */
335 description
: "Require `const` declarations for variables that are never reassigned after declared",
337 url
: "https://eslint.org/docs/latest/rules/prefer-const"
346 destructuring
: { enum: ["any", "all"], default: "any" },
347 ignoreReadBeforeAssign
: { type
: "boolean", default: false }
349 additionalProperties
: false
353 useConst
: "'{{name}}' is never reassigned. Use 'const' instead."
358 const options
= context
.options
[0] || {};
359 const sourceCode
= context
.sourceCode
;
360 const shouldMatchAnyDestructuredVariable
= options
.destructuring
!== "all";
361 const ignoreReadBeforeAssign
= options
.ignoreReadBeforeAssign
=== true;
362 const variables
= [];
364 let checkedId
= null;
365 let checkedName
= "";
369 * Reports given identifier nodes if all of the nodes should be declared
372 * The argument 'nodes' is an array of Identifier nodes.
373 * This node is the result of 'getIdentifierIfShouldBeConst()', so it's
374 * nullable. In simple declaration or assignment cases, the length of
375 * the array is 1. In destructuring cases, the length of the array can
377 * @param {(eslint-scope.Reference|null)[]} nodes
378 * References which are grouped by destructuring to report.
381 function checkGroup(nodes
) {
382 const nodesToReport
= nodes
.filter(Boolean
);
384 if (nodes
.length
&& (shouldMatchAnyDestructuredVariable
|| nodesToReport
.length
=== nodes
.length
)) {
385 const varDeclParent
= findUp(nodes
[0], "VariableDeclaration", parentNode
=> parentNode
.type
.endsWith("Statement"));
386 const isVarDecParentNull
= varDeclParent
=== null;
388 if (!isVarDecParentNull
&& varDeclParent
.declarations
.length
> 0) {
389 const firstDeclaration
= varDeclParent
.declarations
[0];
391 if (firstDeclaration
.init
) {
392 const firstDecParent
= firstDeclaration
.init
.parent
;
395 * First we check the declaration type and then depending on
396 * if the type is a "VariableDeclarator" or its an "ObjectPattern"
397 * we compare the name and id from the first identifier, if the names are different
398 * we assign the new name, id and reset the count of reportCount and nodeCount in
399 * order to check each block for the number of reported errors and base our fix
400 * based on comparing nodes.length and nodesToReport.length.
403 if (firstDecParent
.type
=== "VariableDeclarator") {
405 if (firstDecParent
.id
.name
!== checkedName
) {
406 checkedName
= firstDecParent
.id
.name
;
410 if (firstDecParent
.id
.type
=== "ObjectPattern") {
411 if (firstDecParent
.init
.name
!== checkedName
) {
412 checkedName
= firstDecParent
.init
.name
;
417 if (firstDecParent
.id
!== checkedId
) {
418 checkedId
= firstDecParent
.id
;
425 let shouldFix
= varDeclParent
&&
427 // Don't do a fix unless all variables in the declarations are initialized (or it's in a for-in or for-of loop)
428 (varDeclParent
.parent
.type
=== "ForInStatement" || varDeclParent
.parent
.type
=== "ForOfStatement" ||
429 varDeclParent
.declarations
.every(declaration
=> declaration
.init
)) &&
432 * If options.destructuring is "all", then this warning will not occur unless
433 * every assignment in the destructuring should be const. In that case, it's safe
436 nodesToReport
.length
=== nodes
.length
;
438 if (!isVarDecParentNull
&& varDeclParent
.declarations
&& varDeclParent
.declarations
.length
!== 1) {
440 if (varDeclParent
&& varDeclParent
.declarations
&& varDeclParent
.declarations
.length
>= 1) {
443 * Add nodesToReport.length to a count, then comparing the count to the length
444 * of the declarations in the current block.
447 reportCount
+= nodesToReport
.length
;
449 let totalDeclarationsCount
= 0;
451 varDeclParent
.declarations
.forEach(declaration
=> {
452 if (declaration
.id
.type
=== "ObjectPattern") {
453 totalDeclarationsCount
+= declaration
.id
.properties
.length
;
454 } else if (declaration
.id
.type
=== "ArrayPattern") {
455 totalDeclarationsCount
+= declaration
.id
.elements
.length
;
457 totalDeclarationsCount
+= 1;
461 shouldFix
= shouldFix
&& (reportCount
=== totalDeclarationsCount
);
465 nodesToReport
.forEach(node
=> {
468 messageId
: "useConst",
472 const letKeywordToken
= sourceCode
.getFirstToken(varDeclParent
, t
=> t
.value
=== varDeclParent
.kind
);
475 * Extend the replacement range to the whole declaration,
476 * in order to prevent other fixes in the same pass
477 * https://github.com/eslint/eslint/issues/13899
479 return new FixTracker(fixer
, sourceCode
)
480 .retainRange(varDeclParent
.range
)
481 .replaceTextRange(letKeywordToken
.range
, "const");
491 groupByDestructuring(variables
, ignoreReadBeforeAssign
).forEach(checkGroup
);
494 VariableDeclaration(node
) {
495 if (node
.kind
=== "let" && !isInitOfForStatement(node
)) {
496 variables
.push(...sourceCode
.getDeclaredVariables(node
));