|
1 | 1 | //! This module contains logic for checking if any [`Reference`]s to a |
2 | 2 | //! [`Symbol`] are considered a usage. |
3 | 3 |
|
| 4 | +use itertools::Itertools; |
4 | 5 | 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}; |
6 | 7 | use oxc_span::{GetSpan, Span}; |
7 | 8 |
|
8 | 9 | use super::{NoUnusedVars, Symbol, ignored::FoundStatus}; |
@@ -413,9 +414,31 @@ impl<'a> Symbol<'_, 'a> { |
413 | 414 | match left { |
414 | 415 | AssignmentTarget::AssignmentTargetIdentifier(id) => { |
415 | 416 | 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 | + } |
416 | 439 | is_used_by_others = false; |
417 | 440 | } else { |
418 | | - return false; // we can short-circuit |
| 441 | + return false; |
419 | 442 | } |
420 | 443 | } |
421 | 444 | AssignmentTarget::TSAsExpression(v) |
@@ -818,4 +841,18 @@ impl<'a> Symbol<'_, 'a> { |
818 | 841 | }; |
819 | 842 | } |
820 | 843 | } |
| 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 | + } |
821 | 858 | } |
0 commit comments