fix(linter/eslint/no-unused-vars): count conditional calls in sequences as uses (#27755)

Calls such as `(x ? a(y) : 0, y)` and `(x && a(y), y)` consume `a` even though their results are discarded. Track that consumption through surrounding expressions, resetting at function boundaries so discarded self-referencing closures remain unused, matching ESLint.

Addresses report 1 (`eslint/no-unused-vars`) in #27746.
This commit is contained in:
camc314 committed 2026-10-15 21:07:17 +00:00
1 parent 7f22fa0d8c
commit 2c671979b5
2 files changed
+61 -7

No files matched your search

@@ -246,6 +246,24 @@ fn test_vars_self_use_js() {
.test();
}
#[test]
fn discarded_instance_initializers() {
let pass = vec![
"let f = () => 0; (class { static field = f() }, 0);",
"let f = () => 0; (class { [f()] = 0 }, 0);",
"let f = () => 0; (class { static accessor field = f() }, 0);",
"let f = () => 0; (class { accessor [f()] = 0 }, 0);",
];
let fail = vec![
"let C; (C = class { field = new C() }, 0);",
"let C; (C = class { accessor field = new C() }, 0);",
];
Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail)
.intentionally_allow_no_fix_tests()
.test();
}
#[test]
fn test_vars_discarded_reads() {
let pass = vec![
@@ -307,6 +325,18 @@ fn test_vars_discarded_reads() {
<button onClick={() => (onDismiss?.(), remove())}>x</button>
);
};",
"let a = (v) => v; module.exports = (x, y) => (x ? a(y) : 0, y);",
"let a = (v) => v; module.exports = (x, y) => (x ? 0 : a(y), y);",
"let a = (v) => v; module.exports = (x, y) => (x && a(y), y);",
"let a = (v) => v; module.exports = (x, y) => (x || a(y), y);",
"let a = (v) => v; module.exports = (x, y) => (x ?? a(y), y);",
"module.exports = (x, y, a) => (x ? a(y) : 0, y);",
"module.exports = (x, y, a) => (x && a(y), y);",
"module.exports = (x, y, a) => (x ? new a(y) : 0, y);",
"module.exports = (x, y, a) => (x ? a?.(y) : 0, y);",
"module.exports = (x, y, a) => (x ? (0, a)(y) : 0, y);",
"module.exports = (x, y, a) => (x ? a((y, 0)) : 0, y);",
"module.exports = (x, y, a) => (x ? (a(y), 0) : 0, y);",
];
let fail = vec![
@@ -748,11 +748,33 @@ impl<'a> Symbol<'_, 'a> {
return false;
}
let mut is_call_result = false;
for (parent, grandparent) in
self.iter_relevant_parent_and_grandparent_kinds(reference.node_id())
{
let ref_span = || self.get_ref_span(reference);
if match parent {
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_) => true,
AstKind::PropertyDefinition(property) => {
!property.r#static
&& property
.value
.as_ref()
.is_some_and(|value| value.span().contains_inclusive(ref_span()))
}
AstKind::AccessorProperty(property) => {
!property.r#static
&& property
.value
.as_ref()
.is_some_and(|value| value.span().contains_inclusive(ref_span()))
}
_ => false,
} {
is_call_result = false;
}
match (parent, grandparent) {
// (foo.bar = new Foo(a), f(b))
// `a` should not be considered discarded
@@ -764,6 +786,7 @@ impl<'a> Symbol<'_, 'a> {
AstKind::CallExpression(_) | AstKind::NewExpression(_),
) => {
if id.span == ref_span() {
is_call_result = true;
continue;
}
break;
@@ -814,13 +837,14 @@ impl<'a> Symbol<'_, 'a> {
return false;
}
(parent, AstKind::SequenceExpression(seq)) => {
if matches!(
parent,
AstKind::CallExpression(_)
| AstKind::AwaitExpression(_)
| AstKind::YieldExpression(_)
| AstKind::ChainExpression(_)
) {
if is_call_result
|| matches!(
parent,
AstKind::AwaitExpression(_)
| AstKind::YieldExpression(_)
| AstKind::ChainExpression(_)
)
{
continue;
}