fix(linter/eslint/no-unused-vars): recognize consumed update expressions (#26782)

`no-unused-vars` incorrectly reports variables whose updated values are consumed, such as `let a = 0; new Foo(a++);`.

Recognize constructor arguments, computed member access, switch discriminants, and conditional/logical tests as uses of update values. Keep warnings for discarded updates and self-assignments, with regression coverage for both cases.
This commit is contained in:
camc314 committed 2026-09-22 10:31:15 +00:00
1 parent f2d621eea4
commit 5bdb9b8736
2 files changed
+114 -3

No files matched your search

@@ -1998,3 +1998,69 @@ fn test_root_exports_do_not_export_namespace_bindings() {
.intentionally_allow_no_fix_tests()
.test();
}
#[test]
fn consumed_update_values_are_usages() {
let pass = vec![
"let a = 0; new Foo(a++);",
"const a = 0; new Foo(a++);",
"const a = 0; new Foo((0, a++));",
"const a = class {}; new (a ??= Fallback)();",
"let a = class {}; new (a ??= Fallback)();",
"let a = class {}; (new (a ||= Fallback)(), 0);",
"let a = 0; switch (value) { case a++: break; }",
"let a = 0; switch (value) { case (a = a + 1): break; }",
"let a = 0; (a++).toString();",
"let a = 0; ((a++).toString, 0);",
"let a = 0; (a = a + 1).toString();",
"class C { #value; next = this; static f() { let a = new C(); ((a = a.next).#value, 0); } } C.f();",
"let a = 0; (a++ && foo(), 0);",
"let a = 0; (a++ || foo(), 0);",
"let a = 0; (a++ ?? foo(), 0);",
"let a = 0; (object[a++], 0);",
"let a = 0; (object[a = a + 1], 0);",
"let a = 0; (a++ ? foo() : bar(), 0);",
"let a = 0; object[a++];",
"let a = 0; switch (a++) {}",
"let a = 0; a++ ? foo() : bar();",
"let a = 0; a++ && foo();",
"let a = 0; a++ || foo();",
"let a = 0; a++ ?? foo();",
"let a = 0; new Foo(a = a + 1);",
"let a = 0; object[a = a + 1];",
];
let fail = vec![
"let a = 0; a++;",
"let a = 0; (a++, 0);",
"let a = 0; (a++, 0) && foo();",
"let a = 0; flag && a++;",
"let a = 0; (a++, 0).toString();",
"let a = 0; ((a++, 0).toString, 0);",
"let a = 0; a = (a++).toString();",
"let a = 0; (flag && a++, 0);",
"let a = 0; ((a++, 0) && foo(), 0);",
"let a = 0; (object[(a++, 0)], 0);",
"let a = 0; flag || a++;",
"let a = 0; flag ?? a++;",
"let a = 0; flag ? a++ : foo();",
"let a = 0; flag ? foo() : a++;",
"let a = 0; new Foo((a++, 0));",
"const a = 0; new Foo((a++, 0));",
"const a = 0; new Foo(((a++, 0), 1));",
"const a = class {}; new (a ??= Fallback, Other)();",
"let a = class {}; new (a ??= Fallback, Other)();",
"let a = 0; switch (value) { case (a++, 0): break; }",
"let a = 0; switch (value) { case 0: a++; break; }",
"let a = 0; object[(a++, 0)];",
"let a = 0; switch ((a++, 0)) {}",
// Consuming an intermediate value does not make an outer self-assignment used.
"let a = 0; a = a || 1;",
"let a = 0; a = a ? 1 : 2;",
"let a = 0; a = a++ || 1;",
"let a = 0; a = object[a++];",
];
Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail)
.intentionally_allow_no_fix_tests()
.test();
}
@@ -477,6 +477,19 @@ impl<'a> Symbol<'_, 'a> {
{
return false;
}
AstKind::NewExpression(new_expr)
if new_expr.callee.span().contains_inclusive(ref_span)
|| new_expr
.arguments_span()
.is_some_and(|span| span.contains_inclusive(ref_span)) =>
{
return false;
}
AstKind::ComputedMemberExpression(_)
| AstKind::StaticMemberExpression(_)
| AstKind::PrivateFieldExpression(_) => {
is_used_by_others = true;
}
// When symbol is being assigned a new value, we flag the reference
// as only affecting itself until proven otherwise.
AstKind::UpdateExpression(UpdateExpression { argument, .. })
@@ -563,6 +576,30 @@ impl<'a> Symbol<'_, 'a> {
{
return false;
}
AstKind::ConditionalExpression(expr)
if expr.test.span().contains_inclusive(ref_span) =>
{
is_used_by_others = true;
}
AstKind::LogicalExpression(expr)
if expr.left.span().contains_inclusive(ref_span) =>
{
is_used_by_others = true;
}
AstKind::SwitchStatement(stmt)
if stmt.discriminant.span().contains_inclusive(ref_span) =>
{
return false;
}
AstKind::SwitchCase(case)
if case
.test
.as_ref()
.is_some_and(|test| test.span().contains_inclusive(ref_span)) =>
{
return false;
}
// expression is over, save cycles by breaking
// todo: do we need to check if variable is used as iterator in
@@ -753,13 +790,21 @@ impl<'a> Symbol<'_, 'a> {
{
return false;
}
// x && (a = x)
// The left operand controls whether the right operand is evaluated,
// even if the logical expression's result is discarded.
(AstKind::LogicalExpression(expr), _)
if expr.left.span().contains_inclusive(ref_span())
&& expr.right.get_inner_expression().is_assignment() =>
if expr.left.span().contains_inclusive(ref_span()) =>
{
return false;
}
// Reading a property consumes its object and key, even if the
// member expression's result is discarded.
(
AstKind::ComputedMemberExpression(_)
| AstKind::StaticMemberExpression(_)
| AstKind::PrivateFieldExpression(_),
_,
) => return false,
// x instanceof Foo && (a = x)
(AstKind::BinaryExpression(expr), _)
if expr.operator.is_relational()