Skip to content

Commit e96b4fd

Browse files
committed
fix(linter): false positive in no-unused-vars
1 parent c049765 commit e96b4fd

4 files changed

Lines changed: 77 additions & 3 deletions

File tree

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: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
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::*};
5-
use oxc_semantic::{AstNode, NodeId, Reference, ScopeId, SymbolFlags, SymbolId};
6+
use oxc_semantic::{AstNode, NodeId, Reference, ScopeFlags, ScopeId, SymbolFlags, SymbolId};
67
use oxc_span::{GetSpan, Span};
78

89
use super::{NoUnusedVars, Symbol, ignored::FoundStatus};
@@ -413,9 +414,31 @@ impl<'a> Symbol<'_, 'a> {
413414
match left {
414415
AssignmentTarget::AssignmentTargetIdentifier(id) => {
415416
if id.name == name {
417+
// Compare *variable scopes* (the nearest function / TS module / class‑static block).
418+
//
419+
// If the variable scope is the same, the the variable is still unused
420+
// ```ts
421+
// let cancel = () => {};
422+
// { // plain block
423+
// cancel = cancel?.(); // `cancel` is unused
424+
// }
425+
// ```
426+
//
427+
// If the variable scope is different, the read can be observed later, so it counts as a real usage:
428+
// ```ts
429+
// let cancel = () => {};
430+
// function foo() { // new var‑scope
431+
// cancel = cancel?.(); // `cancel` is used
432+
// }
433+
// ```
434+
if self.get_parent_variable_scope(self.get_ref_scope(reference))
435+
!= self.get_parent_variable_scope(self.scope_id())
436+
{
437+
return false;
438+
}
416439
is_used_by_others = false;
417440
} else {
418-
return false; // we can short-circuit
441+
return false;
419442
}
420443
}
421444
AssignmentTarget::TSAsExpression(v)
@@ -818,4 +841,18 @@ impl<'a> Symbol<'_, 'a> {
818841
};
819842
}
820843
}
844+
845+
/// Return the **variable scope** for the given `scope_id`.
846+
///
847+
/// A variable scope is the closest ancestor scope (including `scope_id`
848+
/// itself) whose kind can *outlive* the current execution slice:
849+
/// * function‑like scopes
850+
/// * class static blocks
851+
/// * TypeScript namespace/module blocks
852+
fn get_parent_variable_scope(&self, scope_id: ScopeId) -> ScopeId {
853+
self.scoping()
854+
.scope_ancestors(scope_id)
855+
.find_or_last(|scope_id| self.scoping().scope_flags(*scope_id).is_var())
856+
.expect("scope iterator will always contain at least one element")
857+
}
821858
}

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)