fix(linter/eslint/prefer-const): skip fixers if any declarator is reassigned (#28008)

When a `let` declaration had multiple declarators and an earlier
declarator was reassigned while the last declarator was not (for
example, `let a = 1, b = 2; a++;`), `eslint/prefer-const` passed the
last declarator's `all_const` flag to `fix_let_to_const` and rewrote the
entire declaration to `const`.

Check that every binding identifier across all declarators in the
`VariableDeclaration` can be `const` before applying the `let` to
`const` fix, while continuing to report diagnostics on the individual
declarators that are never reassigned.

Closes #28003

---------

Co-authored-by: Cameron Clark <cameron.clark@hey.com>
This commit is contained in:
Prince JainandCameron Clark authored and GitHub committed 2026-10-21 16:57:16 +00:00
1 parent 71ef6a7ff7
commit f3705a976b
1 file changed
+16 -8
@@ -131,26 +131,22 @@ impl Rule for PreferConst {
let is_for_in_of_init =
matches!(parent.kind(), AstKind::ForInStatement(_) | AstKind::ForOfStatement(_));
// For regular for loops (for (let i = 0, j = 1; ...)), we need special handling
// If ANY variable in the declaration is reassigned, we can't fix the whole declaration
let is_for_statement_init = matches!(parent.kind(), AstKind::ForStatement(_));
if is_for_statement_init && decl.declarations.len() > 1 {
// Check if any declarator is reassigned
let any_reassigned = decl.declarations.iter().any(|declarator| {
let has_init = declarator.init.is_some();
declarator.id.get_binding_identifiers().iter().any(|ident| {
let symbol_id = ident.symbol_id();
!self.should_be_const(symbol_id, has_init, is_for_in_of_init, ctx)
!self.should_be_const(ident.symbol_id(), has_init, is_for_in_of_init, ctx)
})
});
// If any variable is reassigned, we can't convert any of them
if any_reassigned {
return;
}
}
let mut all_declarators_const = true;
for (declarator_index, declarator) in decl.declarations.iter().enumerate() {
let binding_identifiers = declarator.id.get_binding_identifiers();
let has_init = declarator.init.is_some();
@@ -160,6 +156,7 @@ impl Rule for PreferConst {
let symbol_id = ident.symbol_id();
self.should_be_const(symbol_id, has_init, is_for_in_of_init, ctx)
});
all_declarators_const &= all_const;
// For destructuring patterns with "all" mode, check if ALL variables should be const
if matches!(self.0.destructuring, Destructuring::All) && is_destructuring {
@@ -173,7 +170,7 @@ impl Rule for PreferConst {
fixer,
decl,
declarator_index,
true,
all_declarators_const,
is_for_in_of_init,
ctx,
)
@@ -193,7 +190,7 @@ impl Rule for PreferConst {
fixer,
decl,
declarator_index,
all_const,
all_declarators_const,
is_for_in_of_init,
ctx,
)
@@ -1651,6 +1648,17 @@ fn test_oxc() {
("let [x = -1, y] = [1,2]; y = 0;", "let [x = -1, y] = [1,2]; y = 0;", None),
("let {a, b} = c, d;", "let {a, b} = c, d;", None),
("let {a, b, c} = {}, e, f;", "let {a, b, c} = {}, e, f;", None),
("let x = 'x', y = 'y'; x = 1", "let x = 'x', y = 'y'; x = 1", None),
(
"let a = 1, b = 2; a++; console.log(a + b);",
"let a = 1, b = 2; a++; console.log(a + b);",
None,
),
(
"let { a, b } = obj, c = 1; a = 2; console.log(a, b, c);",
"let { a, b } = obj, c = 1; a = 2; console.log(a, b, c);",
Some(serde_json::json!([{ "destructuring": "all" }])),
),
];
Tester::new(PreferConst::NAME, PreferConst::PLUGIN, pass, fail)