Check a single scope (function/method body) for undefined variable reads. For each variable read, we check whether the variable has been written (assigned, declared as a parameter, etc.) at a **lower byte offset** in the same frame. Writes inside control-flow branches (if/else, switch, try/catch) still count — we are conservative about branches but strict about source order. This catches the co
(
scope: &ScopeMap,
statements: &[Statement<'_>],
ctx: &mut DiagnosticCtx<'_>,
this_is_defined: bool,
)
| 322 | /// common "used before assigned" typo while still avoiding false |
| 323 | /// positives from branch-dependent definitions. |
| 324 | fn check_scope( |
| 325 | scope: &ScopeMap, |
| 326 | statements: &[Statement<'_>], |
| 327 | ctx: &mut DiagnosticCtx<'_>, |
| 328 | this_is_defined: bool, |
| 329 | ) { |
| 330 | // Bail out early if the function uses features that make static |
| 331 | // analysis unsound. |
| 332 | if has_dynamic_variables(statements) || has_extract_call(statements) { |
| 333 | return; |
| 334 | } |
| 335 | |
| 336 | // Collect variable names referenced by compact() calls — these |
| 337 | // variables are used by string name and should be treated as |
| 338 | // defined. |
| 339 | let compact_vars = collect_compact_vars(statements); |
| 340 | |
| 341 | // Collect variable names annotated with `/** @var Type $var */` |
| 342 | // inline docblocks. |
| 343 | let var_annotated = collect_var_annotations(ctx.content); |
| 344 | |
| 345 | // Collect byte offsets suppressed by the `@` error control |
| 346 | // operator (e.g. `@$var`). |
| 347 | let error_suppressed_offsets = collect_error_suppressed_offsets(statements); |
| 348 | |
| 349 | // Collect byte offsets of variables inside `isset()` and `empty()`. |
| 350 | let guarded_offsets = collect_guarded_offsets(statements); |
| 351 | |
| 352 | // Bail out if there are no frames at all. |
| 353 | if scope.frames.is_empty() { |
| 354 | return; |
| 355 | } |
| 356 | |
| 357 | // Build a set of "always-defined" names that do not require a |
| 358 | // prior write: superglobals, compact-referenced vars, @var |
| 359 | // annotations, and optionally $this. |
| 360 | let mut always_defined: HashSet<&str> = HashSet::new(); |
| 361 | for sg in SUPERGLOBALS { |
| 362 | always_defined.insert(sg); |
| 363 | } |
| 364 | if this_is_defined { |
| 365 | always_defined.insert("$this"); |
| 366 | } |
| 367 | for cv in &compact_vars { |
| 368 | always_defined.insert(cv.as_str()); |
| 369 | } |
| 370 | for av in &var_annotated { |
| 371 | always_defined.insert(av.as_str()); |
| 372 | } |
| 373 | |
| 374 | // Pre-compute the "own writes" for each frame: writes that are |
| 375 | // directly inside the frame (not inside a nested sub-frame). |
| 376 | let frame_own_writes: Vec<Vec<(&str, u32)>> = scope |
| 377 | .frames |
| 378 | .iter() |
| 379 | .map(|frame| { |
| 380 | let mut writes: Vec<(&str, u32)> = Vec::new(); |
| 381 | // Parameters (offset 0 = always before any read). |
no test coverage detected