Bind a foreach value variable from the iterable's element type. Resolution strategy: 1. Try `PhpType::extract_value_type` — works for types that already carry generic parameters (e.g. `list `, `array `, `Collection `). 2. Class-based fallback — when the type is a bare class name (e.g. `OrderProductCollection`), resolve it to `ClassInfo`, merge inheritance, and extract
(
value_expr: &'b Expression<'b>,
iter_type: &Option<PhpType>,
scope: &mut ScopeState,
ctx: &ForwardWalkCtx<'_>,
)
| 6526 | /// `@implements` generics. This mirrors what |
| 6527 | /// `try_resolve_foreach_value_type` does in the backward scanner. |
| 6528 | fn bind_foreach_value<'b>( |
| 6529 | value_expr: &'b Expression<'b>, |
| 6530 | iter_type: &Option<PhpType>, |
| 6531 | scope: &mut ScopeState, |
| 6532 | ctx: &ForwardWalkCtx<'_>, |
| 6533 | ) { |
| 6534 | // Unwrap `&$value` (by-reference foreach) to get the inner variable. |
| 6535 | let value_expr = if let Expression::UnaryPrefix(up) = value_expr |
| 6536 | && matches!(up.operator, UnaryPrefixOperator::Reference(_)) |
| 6537 | { |
| 6538 | up.operand |
| 6539 | } else { |
| 6540 | value_expr |
| 6541 | }; |
| 6542 | if let Expression::Variable(Variable::Direct(dv)) = value_expr { |
| 6543 | let var_name = dv.name.to_string(); |
| 6544 | if let Some(it) = iter_type { |
| 6545 | // Strategy 1: extract from the type's own generic parameters. |
| 6546 | let value_php_type = it.extract_value_type(false); |
| 6547 | if let Some(vt) = value_php_type { |
| 6548 | let resolved = crate::completion::type_resolution::type_hint_to_classes_typed( |
| 6549 | vt, |
| 6550 | &ctx.current_class.name, |
| 6551 | ctx.all_classes, |
| 6552 | ctx.class_loader, |
| 6553 | ); |
| 6554 | if !resolved.is_empty() { |
| 6555 | scope.set( |
| 6556 | &var_name, |
| 6557 | ResolvedType::from_classes_with_hint(resolved, vt.clone()), |
| 6558 | ); |
| 6559 | } else { |
| 6560 | scope.set(&var_name, vec![ResolvedType::from_type_string(vt.clone())]); |
| 6561 | } |
| 6562 | return; |
| 6563 | } |
| 6564 | |
| 6565 | // Strategy 2: class-based fallback for bare collection names. |
| 6566 | let element_via_class = resolve_iterable_element_via_class(it, ctx); |
| 6567 | if let Some(element_type) = element_via_class |
| 6568 | && !is_unsubstituted_template_param(&element_type) |
| 6569 | { |
| 6570 | let resolved = crate::completion::type_resolution::type_hint_to_classes_typed( |
| 6571 | &element_type, |
| 6572 | &ctx.current_class.name, |
| 6573 | ctx.all_classes, |
| 6574 | ctx.class_loader, |
| 6575 | ); |
| 6576 | if !resolved.is_empty() { |
| 6577 | scope.set( |
| 6578 | &var_name, |
| 6579 | ResolvedType::from_classes_with_hint(resolved, element_type), |
| 6580 | ); |
| 6581 | } else { |
| 6582 | scope.set( |
| 6583 | &var_name, |
| 6584 | vec![ResolvedType::from_type_string(element_type)], |
| 6585 | ); |
no test coverage detected