Process a `while` loop. Uses the same two-pass strategy as `process_foreach` and `process_for`: the first pass discovers all variable assignments inside the loop body, the results are merged back into the pre-loop scope, and the final pass re-walks with full visibility of loop-carried assignments.
(while_stmt: &'b While<'b>, scope: &mut ScopeState, ctx: &ForwardWalkCtx<'_>)
| 6912 | /// pre-loop scope, and the final pass re-walks with full visibility |
| 6913 | /// of loop-carried assignments. |
| 6914 | fn process_while<'b>(while_stmt: &'b While<'b>, scope: &mut ScopeState, ctx: &ForwardWalkCtx<'_>) { |
| 6915 | let loop_depth = enter_loop(); |
| 6916 | |
| 6917 | // Hard limit: skip the body entirely at excessive nesting depth. |
| 6918 | if loop_depth > MAX_LOOP_DEPTH { |
| 6919 | leave_loop(loop_depth); |
| 6920 | return; |
| 6921 | } |
| 6922 | |
| 6923 | // Record `&&` chain snapshots for the while condition. |
| 6924 | record_and_chain_snapshots(while_stmt.condition, scope, ctx); |
| 6925 | |
| 6926 | let pre_loop_scope = scope.clone(); |
| 6927 | |
| 6928 | // The while body executes when the condition is truthy, so apply |
| 6929 | // condition narrowing (instanceof, phpstan-assert-if-true, etc.). |
| 6930 | // This must happen AFTER saving pre_loop_scope so the narrowing |
| 6931 | // only affects the loop body, not the post-loop scope. |
| 6932 | apply_condition_narrowing(while_stmt.condition, scope, ctx); |
| 6933 | |
| 6934 | // When the cursor is inside the loop body (completion path), discovery |
| 6935 | // passes must walk the ENTIRE body; the final pass uses the real |
| 6936 | // cursor_offset so it stops at the cursor as usual. |
| 6937 | let body_span = match &while_stmt.body { |
| 6938 | WhileBody::Statement(inner) => inner.span(), |
| 6939 | WhileBody::ColonDelimited(body) => body.span(), |
| 6940 | }; |
| 6941 | let cursor_in_body = |
| 6942 | ctx.cursor_offset >= body_span.start.offset && ctx.cursor_offset <= body_span.end.offset; |
| 6943 | let discovery_ctx = if cursor_in_body && !is_diagnostic_scope_active() { |
| 6944 | ctx.with_cursor_offset(u32::MAX) |
| 6945 | } else { |
| 6946 | ctx.with_cursor_offset(ctx.cursor_offset) |
| 6947 | }; |
| 6948 | |
| 6949 | // Assignment in condition: `while ($x = expr())` |
| 6950 | process_condition_assignment(while_stmt.condition, scope, ctx); |
| 6951 | |
| 6952 | // Pass-by-reference in condition: `while (preg_match(..., $matches))` |
| 6953 | seed_pass_by_ref_in_condition(while_stmt.condition, scope, ctx); |
| 6954 | |
| 6955 | // Record a snapshot after condition processing (same reasoning as |
| 6956 | // the corresponding snapshot in `process_if`). |
| 6957 | if is_diagnostic_scope_active() { |
| 6958 | let body_start = match &while_stmt.body { |
| 6959 | WhileBody::Statement(inner) => inner.span().start.offset, |
| 6960 | WhileBody::ColonDelimited(body) => body.colon.start.offset, |
| 6961 | }; |
| 6962 | record_scope_snapshot(body_start, scope); |
| 6963 | } |
| 6964 | |
| 6965 | // ── Assignment-depth-bounded loop iteration ───────────────── |
| 6966 | let body_stmts: Vec<&Statement<'b>> = match &while_stmt.body { |
| 6967 | WhileBody::Statement(inner) => vec![*inner], |
| 6968 | WhileBody::ColonDelimited(body) => body.statements.iter().collect(), |
| 6969 | }; |
| 6970 | let assignment_depth = |
| 6971 | clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth); |
no test coverage detected