mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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()
|
||||
|
||||
Reference in new issue
Block a user