|
10 | 10 | /// - Conditional branches (if/else, try/catch, loops) — collects all |
11 | 11 | /// possible types when the variable is assigned differently in each |
12 | 12 | /// branch. |
| 13 | +/// - Foreach value variables: when iterating over a variable annotated |
| 14 | +/// with a generic iterable type (e.g. `@var list<User>`, `@param |
| 15 | +/// list<User>`, `User[]`), the foreach value variable is resolved to |
| 16 | +/// the element type. |
13 | 17 | /// |
14 | 18 | /// Type narrowing (instanceof, assert, custom type guards) is delegated |
15 | 19 | /// to the [`super::type_narrowing`] module. Closure/arrow-function scope |
@@ -471,19 +475,33 @@ impl Backend { |
471 | 475 | } |
472 | 476 | } |
473 | 477 | }, |
474 | | - Statement::Foreach(foreach) => match &foreach.body { |
475 | | - ForeachBody::Statement(inner) => { |
476 | | - Self::check_statement_for_assignments(inner, ctx, results, true); |
477 | | - } |
478 | | - ForeachBody::ColonDelimited(body) => { |
479 | | - Self::walk_statements_for_assignments( |
480 | | - body.statements.iter(), |
481 | | - ctx, |
482 | | - results, |
483 | | - true, |
484 | | - ); |
| 478 | + Statement::Foreach(foreach) => { |
| 479 | + // ── Foreach value type from generic iterables ──── |
| 480 | + // When the variable we're resolving is the foreach |
| 481 | + // *value* variable, try to infer its type from the |
| 482 | + // iterated expression's generic type annotation. |
| 483 | + // |
| 484 | + // Example: |
| 485 | + // /** @var list<User> $users */ |
| 486 | + // foreach ($users as $user) { $user-> } |
| 487 | + // |
| 488 | + // Here `$user` is resolved to `User`. |
| 489 | + Self::try_resolve_foreach_value_type(foreach, ctx, results, conditional); |
| 490 | + |
| 491 | + match &foreach.body { |
| 492 | + ForeachBody::Statement(inner) => { |
| 493 | + Self::check_statement_for_assignments(inner, ctx, results, true); |
| 494 | + } |
| 495 | + ForeachBody::ColonDelimited(body) => { |
| 496 | + Self::walk_statements_for_assignments( |
| 497 | + body.statements.iter(), |
| 498 | + ctx, |
| 499 | + results, |
| 500 | + true, |
| 501 | + ); |
| 502 | + } |
485 | 503 | } |
486 | | - }, |
| 504 | + } |
487 | 505 | Statement::While(while_stmt) => match &while_stmt.body { |
488 | 506 | WhileBody::Statement(inner) => { |
489 | 507 | // ── instanceof narrowing for while-body ── |
@@ -575,6 +593,85 @@ impl Backend { |
575 | 593 | } |
576 | 594 |
|
577 | 595 | /// Helper: treat a single statement as an iterator of one and recurse. |
| 596 | + /// Try to resolve the foreach value variable's type from a generic |
| 597 | + /// iterable annotation on the iterated expression. |
| 598 | + /// |
| 599 | + /// When the variable being resolved (`ctx.var_name`) matches the |
| 600 | + /// foreach value variable and the iterated expression is a simple |
| 601 | + /// `$variable` whose type is annotated as a generic iterable (via |
| 602 | + /// `@var list<User> $var` or `@param list<User> $var`), this method |
| 603 | + /// extracts the element type and pushes the resolved `ClassInfo` into |
| 604 | + /// `results`. |
| 605 | + fn try_resolve_foreach_value_type<'b>( |
| 606 | + foreach: &'b Foreach<'b>, |
| 607 | + ctx: &VarResolutionCtx<'_>, |
| 608 | + results: &mut Vec<ClassInfo>, |
| 609 | + conditional: bool, |
| 610 | + ) { |
| 611 | + // Check if the foreach value variable is the one we're resolving. |
| 612 | + let value_expr = foreach.target.value(); |
| 613 | + let value_var_name = match value_expr { |
| 614 | + Expression::Variable(Variable::Direct(dv)) => dv.name.to_string(), |
| 615 | + _ => return, |
| 616 | + }; |
| 617 | + if value_var_name != ctx.var_name { |
| 618 | + return; |
| 619 | + } |
| 620 | + |
| 621 | + // Extract the iterated expression's source text. |
| 622 | + let expr_span = foreach.expression.span(); |
| 623 | + let expr_start = expr_span.start.offset as usize; |
| 624 | + let expr_end = expr_span.end.offset as usize; |
| 625 | + let expr_text = match ctx.content.get(expr_start..expr_end) { |
| 626 | + Some(t) => t.trim(), |
| 627 | + None => return, |
| 628 | + }; |
| 629 | + |
| 630 | + // Currently we handle simple `$variable` expressions. |
| 631 | + if !expr_text.starts_with('$') || expr_text.contains("->") || expr_text.contains("::") { |
| 632 | + return; |
| 633 | + } |
| 634 | + |
| 635 | + // Search backward from the foreach for @var or @param annotations |
| 636 | + // on the iterated variable that include a generic type. |
| 637 | + let foreach_offset = foreach.foreach.span().start.offset as usize; |
| 638 | + let raw_type = match docblock::find_iterable_raw_type_in_source( |
| 639 | + ctx.content, |
| 640 | + foreach_offset, |
| 641 | + expr_text, |
| 642 | + ) { |
| 643 | + Some(t) => t, |
| 644 | + None => return, |
| 645 | + }; |
| 646 | + |
| 647 | + // Extract the generic element type (e.g. `list<User>` → `User`). |
| 648 | + let element_type = match docblock::types::extract_generic_value_type(&raw_type) { |
| 649 | + Some(t) => t, |
| 650 | + None => return, |
| 651 | + }; |
| 652 | + |
| 653 | + // Resolve the element type to ClassInfo. |
| 654 | + let resolved = Self::type_hint_to_classes( |
| 655 | + &element_type, |
| 656 | + &ctx.current_class.name, |
| 657 | + ctx.all_classes, |
| 658 | + ctx.class_loader, |
| 659 | + ); |
| 660 | + |
| 661 | + if resolved.is_empty() { |
| 662 | + return; |
| 663 | + } |
| 664 | + |
| 665 | + if !conditional { |
| 666 | + results.clear(); |
| 667 | + } |
| 668 | + for cls in resolved { |
| 669 | + if !results.iter().any(|c| c.name == cls.name) { |
| 670 | + results.push(cls); |
| 671 | + } |
| 672 | + } |
| 673 | + } |
| 674 | + |
578 | 675 | pub(super) fn check_statement_for_assignments<'b>( |
579 | 676 | stmt: &'b Statement<'b>, |
580 | 677 | ctx: &VarResolutionCtx<'_>, |
|
0 commit comments