Skip to content

Commit 8956870

Browse files
committed
fix(linter): false positive in no-unused-vars (#11002)
close: #10994 fixes a false positive when a variable is reassigned in a different scope
1 parent 567368e commit 8956870

File tree

4 files changed

+76
-2
lines changed

4 files changed

+76
-2
lines changed

crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ impl NoUnusedVars {
281281
}
282282
let report = match symbol.references().rev().find(|r| r.is_write()) {
283283
Some(last_write) => {
284-
// ahg
285284
let span = ctx.nodes().get_node(last_write.node_id()).kind().span();
286285
diagnostic::assign(symbol, span, &self.vars_ignore_pattern)
287286
}

crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ fn test_vars_self_use() {
171171
}
172172
foo();
173173
",
174+
"
175+
let cancel = () => {}
176+
export function close() { cancel = cancel?.() }
177+
",
174178
];
175179
let fail = vec![
176180
"
@@ -183,6 +187,14 @@ fn test_vars_self_use() {
183187
return foo
184188
}
185189
",
190+
"
191+
let cancel = () => {};
192+
cancel = cancel?.();
193+
",
194+
"
195+
let cancel = () => {};
196+
{ cancel = cancel?.(); }
197+
",
186198
];
187199

188200
Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail)

crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! This module contains logic for checking if any [`Reference`]s to a
22
//! [`Symbol`] are considered a usage.
33
4+
use itertools::Itertools;
45
use oxc_ast::{AstKind, ast::*};
56
use oxc_semantic::{AstNode, NodeId, Reference, ScopeId, SymbolFlags, SymbolId};
67
use oxc_span::{GetSpan, Span};
@@ -427,9 +428,31 @@ impl<'a> Symbol<'_, 'a> {
427428
match left {
428429
AssignmentTarget::AssignmentTargetIdentifier(id) => {
429430
if id.name == name {
431+
// Compare *variable scopes* (the nearest function / TS module / class‑static block).
432+
//
433+
// If the variable scope is the same, the the variable is still unused
434+
// ```ts
435+
// let cancel = () => {};
436+
// { // plain block
437+
// cancel = cancel?.(); // `cancel` is unused
438+
// }
439+
// ```
440+
//
441+
// If the variable scope is different, the read can be observed later, so it counts as a real usage:
442+
// ```ts
443+
// let cancel = () => {};
444+
// function foo() { // new var‑scope
445+
// cancel = cancel?.(); // `cancel` is used
446+
// }
447+
// ```
448+
if self.get_parent_variable_scope(self.get_ref_scope(reference))
449+
!= self.get_parent_variable_scope(self.scope_id())
450+
{
451+
return false;
452+
}
430453
is_used_by_others = false;
431454
} else {
432-
return false; // we can short-circuit
455+
return false;
433456
}
434457
}
435458
AssignmentTarget::TSAsExpression(v)
@@ -832,4 +855,18 @@ impl<'a> Symbol<'_, 'a> {
832855
};
833856
}
834857
}
858+
859+
/// Return the **variable scope** for the given `scope_id`.
860+
///
861+
/// A variable scope is the closest ancestor scope (including `scope_id`
862+
/// itself) whose kind can *outlive* the current execution slice:
863+
/// * function‑like scopes
864+
/// * class static blocks
865+
/// * TypeScript namespace/module blocks
866+
fn get_parent_variable_scope(&self, scope_id: ScopeId) -> ScopeId {
867+
self.scoping()
868+
.scope_ancestors(scope_id)
869+
.find_or_last(|scope_id| self.scoping().scope_flags(*scope_id).is_var())
870+
.expect("scope iterator will always contain at least one element")
871+
}
835872
}

crates/oxc_linter/src/snapshots/[email protected]

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,29 @@ source: crates/oxc_linter/src/tester.rs
2020
3return foo
2121
╰────
2222
help: Consider removing this declaration.
23+
24+
eslint(no-unused-vars): Variable 'cancel' is assigned a value but never used. Unused variables should start with a '_'.
25+
╭─[no_unused_vars.tsx:2:13]
26+
1
27+
2let cancel = () => {};
28+
· ───┬──
29+
· ╰── 'cancel' is declared here
30+
3cancel = cancel?.();
31+
· ───┬──
32+
· ╰── it was last assigned here
33+
4
34+
╰────
35+
help: Did you mean to use this variable?
36+
37+
eslint(no-unused-vars): Variable 'cancel' is assigned a value but never used. Unused variables should start with a '_'.
38+
╭─[no_unused_vars.tsx:2:13]
39+
1
40+
2let cancel = () => {};
41+
· ───┬──
42+
· ╰── 'cancel' is declared here
43+
3 │ { cancel = cancel?.(); }
44+
· ───┬──
45+
· ╰── it was last assigned here
46+
4
47+
╰────
48+
help: Did you mean to use this variable?

0 commit comments

Comments
 (0)