Collect all scopes reachable from `root_scope` for `var_name` through closure `use` captures and implicit arrow-function captures. Returns a set containing `root_scope` plus every nested closure/arrow scope that captures the variable without shadowing it with a new parameter of the same name.
(
symbol_map: &SymbolMap,
var_name: &str,
root_scope: u32,
)
| 354 | /// closure/arrow scope that captures the variable without shadowing |
| 355 | /// it with a new parameter of the same name. |
| 356 | fn collect_capture_scopes( |
| 357 | symbol_map: &SymbolMap, |
| 358 | var_name: &str, |
| 359 | root_scope: u32, |
| 360 | ) -> HashSet<u32> { |
| 361 | let mut reachable = HashSet::new(); |
| 362 | reachable.insert(root_scope); |
| 363 | |
| 364 | // Explicit closure captures: `function () use ($var) { … }` |
| 365 | // These have VarDefKind::ClosureCapture with scope_start |
| 366 | // pointing to the closure body. |
| 367 | for def in &symbol_map.var_defs { |
| 368 | if def.name != var_name || def.kind != VarDefKind::ClosureCapture { |
| 369 | continue; |
| 370 | } |
| 371 | // The `use ($var)` token sits physically in the outer scope. |
| 372 | // Check if the outer scope is already reachable. |
| 373 | let outer_scope = symbol_map.find_enclosing_scope(def.offset); |
| 374 | if reachable.contains(&outer_scope) { |
| 375 | reachable.insert(def.scope_start); |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | // Implicit arrow-function captures: `fn () => $var` |
| 380 | // Arrow functions have a scope entry but no ClosureCapture def. |
| 381 | // A variable is implicitly captured if: |
| 382 | // 1. The arrow scope is directly nested in a reachable scope. |
| 383 | // 2. There is no parameter with the same name in the arrow scope. |
| 384 | for &(scope_start, _scope_end) in &symbol_map.scopes { |
| 385 | if reachable.contains(&scope_start) { |
| 386 | continue; // Already reachable, skip. |
| 387 | } |
| 388 | // Find the parent scope of this scope. |
| 389 | let parent = symbol_map.find_enclosing_scope(scope_start.saturating_sub(1)); |
| 390 | if !reachable.contains(&parent) { |
| 391 | continue; |
| 392 | } |
| 393 | // Check if this is an arrow function scope (no ClosureCapture |
| 394 | // or Parameter def that would indicate a closure with `use`). |
| 395 | // Arrow scopes don't have braces; their scope_start is the |
| 396 | // arrow function expression's start offset. |
| 397 | // |
| 398 | // Skip if there's a parameter with the same name (shadowed). |
| 399 | let has_shadowing_param = symbol_map.var_defs.iter().any(|d| { |
| 400 | d.name == var_name |
| 401 | && d.scope_start == scope_start |
| 402 | && d.kind == VarDefKind::Parameter |
| 403 | }); |
| 404 | if has_shadowing_param { |
| 405 | continue; |
| 406 | } |
| 407 | // Check if this scope actually uses the variable (has a |
| 408 | // Variable span in it). Only include it if the variable |
| 409 | // appears there to avoid false positives with unrelated |
| 410 | // nested functions. |
| 411 | // |
| 412 | // But we also need to check: is this scope a closure body |
| 413 | // (not an arrow function)? Closures create new variable |
nothing calls this directly
no test coverage detected