Recursively scan an expression tree for closures/arrow functions and walk their bodies with properly seeded scopes. When a closure/arrow function is found: 1. Build a scope for the closure body (fresh for closures, cloned from outer for arrow functions). 2. Seed the scope with parameter types (using callable inference from the enclosing call's signature when parameters are untyped). 3. Walk the b
(
expr: &'b Expression<'b>,
outer_scope: &ScopeState,
ctx: &ForwardWalkCtx<'_>,
inferred_params: Option<&[PhpType]>,
)
| 431 | /// passes the inferred types so untyped parameters get the correct |
| 432 | /// types. |
| 433 | fn walk_closures_in_expr<'b>( |
| 434 | expr: &'b Expression<'b>, |
| 435 | outer_scope: &ScopeState, |
| 436 | ctx: &ForwardWalkCtx<'_>, |
| 437 | inferred_params: Option<&[PhpType]>, |
| 438 | ) { |
| 439 | match expr { |
| 440 | Expression::Closure(closure) => { |
| 441 | // Build a fresh scope for the closure. |
| 442 | let mut closure_scope = ScopeState::new(); |
| 443 | |
| 444 | // PHP closures implicitly capture `$this` from the |
| 445 | // enclosing class method. Seed it from the outer scope |
| 446 | // so that `$this->prop` inside the closure resolves |
| 447 | // without calling `resolve_variable_types`. |
| 448 | let this_types = outer_scope.get("$this"); |
| 449 | if !this_types.is_empty() { |
| 450 | closure_scope.set("$this", this_types.to_vec()); |
| 451 | } |
| 452 | |
| 453 | // Seed with `use(...)` variables from the outer scope. |
| 454 | if let Some(ref use_clause) = closure.use_clause { |
| 455 | for use_var in use_clause.variables.iter() { |
| 456 | let var_name = use_var.variable.name.to_string(); |
| 457 | let from_outer = outer_scope.get(&var_name); |
| 458 | if !from_outer.is_empty() { |
| 459 | closure_scope.set(&var_name, from_outer.to_vec()); |
| 460 | } |
| 461 | } |
| 462 | } |
| 463 | |
| 464 | // Seed with parameter types, using callable inference when |
| 465 | // available. Filter out any inferred params whose base |
| 466 | // type is unresolvable (e.g. PHPStan pseudo-types like |
| 467 | // `collection-of<T>`) so they don't poison the scope — |
| 468 | // the param simply won't be seeded, which is better than |
| 469 | // skipping the entire closure body. |
| 470 | let inferred = inferred_params.unwrap_or(&[]); |
| 471 | let filtered_inferred = filter_resolvable_inferred_params(inferred, ctx); |
| 472 | seed_closure_params( |
| 473 | &mut closure_scope, |
| 474 | &closure.parameter_list, |
| 475 | closure.span().start.offset, |
| 476 | &filtered_inferred, |
| 477 | ctx, |
| 478 | ); |
| 479 | |
| 480 | // Record the scope at the body start. |
| 481 | let body_span = closure.body.span(); |
| 482 | record_scope_snapshot(body_span.start.offset, &closure_scope); |
| 483 | |
| 484 | // Walk the closure body. |
| 485 | walk_body_for_diagnostics(closure.body.statements.iter(), &mut closure_scope, ctx); |
| 486 | |
| 487 | // Record at body end (closure scope). |
| 488 | record_scope_snapshot(body_span.end.offset, &closure_scope); |
| 489 | |
| 490 | // Restore the outer scope immediately after the closure body |
no test coverage detected