Process a `foreach` statement.
(foreach: &'b Foreach<'b>, scope: &mut ScopeState, ctx: &ForwardWalkCtx<'_>)
| 6107 | |
| 6108 | /// Process a `foreach` statement. |
| 6109 | fn process_foreach<'b>(foreach: &'b Foreach<'b>, scope: &mut ScopeState, ctx: &ForwardWalkCtx<'_>) { |
| 6110 | let loop_depth = enter_loop(); |
| 6111 | |
| 6112 | // Hard limit: skip the body entirely at excessive nesting depth. |
| 6113 | if loop_depth > MAX_LOOP_DEPTH { |
| 6114 | leave_loop(loop_depth); |
| 6115 | return; |
| 6116 | } |
| 6117 | |
| 6118 | // Apply any standalone `/** @var Type $var */` docblocks that precede |
| 6119 | // the foreach keyword. These are not separate AST statements (the |
| 6120 | // parser attaches them as comments to the foreach), so they won't be |
| 6121 | // processed by `process_expression_statement`. Without this, variables |
| 6122 | // typed only via docblock (common in Blade templates) won't be in scope |
| 6123 | // when the iterable expression is resolved. |
| 6124 | // |
| 6125 | // We extract all variables referenced in the foreach expression and |
| 6126 | // check for @var annotations for each one. |
| 6127 | let foreach_offset = foreach.foreach.span().start.offset as usize; |
| 6128 | if let Expression::Variable(Variable::Direct(dv)) = foreach.expression { |
| 6129 | let var_name = format!("${}", dv.name); |
| 6130 | if scope.get(dv.name).is_empty() |
| 6131 | && let Some(var_type) = |
| 6132 | crate::docblock::find_var_raw_type_in_source(ctx.content, foreach_offset, &var_name) |
| 6133 | { |
| 6134 | let resolved = resolve_type_to_resolved_types( |
| 6135 | &crate::util::resolve_php_type_names(&var_type, ctx.class_loader), |
| 6136 | ctx, |
| 6137 | ); |
| 6138 | scope.set(dv.name, resolved); |
| 6139 | } |
| 6140 | } else { |
| 6141 | // For complex expressions like `$users->active()->byName()`, |
| 6142 | // extract the base variable and resolve its type from @var. |
| 6143 | let expr_start = foreach.expression.span().start.offset as usize; |
| 6144 | let expr_end = foreach.expression.span().end.offset as usize; |
| 6145 | if let Some(expr_text) = ctx.content.get(expr_start..expr_end) { |
| 6146 | // Extract the base variable (e.g. "$users" from "$users->active()->byName()") |
| 6147 | if let Some(base_end) = expr_text.find("->").or_else(|| expr_text.find("::")) { |
| 6148 | let base_var = expr_text[..base_end].trim(); |
| 6149 | if let Some(scope_key) = base_var.strip_prefix('$') |
| 6150 | && scope.get(scope_key).is_empty() |
| 6151 | && let Some(var_type) = crate::docblock::find_var_raw_type_in_source( |
| 6152 | ctx.content, |
| 6153 | foreach_offset, |
| 6154 | base_var, |
| 6155 | ) |
| 6156 | { |
| 6157 | let resolved = resolve_type_to_resolved_types( |
| 6158 | &crate::util::resolve_php_type_names(&var_type, ctx.class_loader), |
| 6159 | ctx, |
| 6160 | ); |
| 6161 | scope.set(scope_key, resolved); |
| 6162 | } |
| 6163 | } |
| 6164 | } |
| 6165 | } |
| 6166 |
no test coverage detected