Check a catch frame for unused catch variables only. Catch frames inherit the parent scope, so we only flag variables that are in the catch frame's own `parameters` list (the catch variable itself, e.g. `$e` in `catch (Exception $e)`).
(
frame: &crate::scope_collector::Frame,
scope: &ScopeMap,
ctx: &mut DiagnosticCtx<'_>,
always_skip: &HashSet<&str>,
)
| 345 | /// that are in the catch frame's own `parameters` list (the catch |
| 346 | /// variable itself, e.g. `$e` in `catch (Exception $e)`). |
| 347 | fn check_catch_frame( |
| 348 | frame: &crate::scope_collector::Frame, |
| 349 | scope: &ScopeMap, |
| 350 | ctx: &mut DiagnosticCtx<'_>, |
| 351 | always_skip: &HashSet<&str>, |
| 352 | ) { |
| 353 | for param in &frame.parameters { |
| 354 | let var_name = param.as_str(); |
| 355 | |
| 356 | if always_skip.contains(var_name) { |
| 357 | continue; |
| 358 | } |
| 359 | if var_name == "$_" || var_name.starts_with("$_") { |
| 360 | continue; |
| 361 | } |
| 362 | |
| 363 | // Check for reads inside the catch block body. |
| 364 | let has_read = scope.accesses.iter().any(|a| { |
| 365 | a.name == var_name |
| 366 | && matches!(a.kind, AccessKind::Read | AccessKind::ReadWrite) |
| 367 | && a.offset >= frame.start |
| 368 | && a.offset <= frame.end |
| 369 | }); |
| 370 | |
| 371 | if has_read { |
| 372 | continue; |
| 373 | } |
| 374 | |
| 375 | // Find the catch variable's write offset — the closest write |
| 376 | // *before* this frame's start. Using `.find()` would return the |
| 377 | // first write in the file, which for nested catches with the same |
| 378 | // variable name points at the wrong catch clause. |
| 379 | let diag_offset = scope |
| 380 | .accesses |
| 381 | .iter() |
| 382 | .filter(|a| { |
| 383 | a.name == var_name && matches!(a.kind, AccessKind::Write) && a.offset <= frame.start |
| 384 | }) |
| 385 | .max_by_key(|a| a.offset) |
| 386 | .or_else(|| { |
| 387 | scope |
| 388 | .accesses |
| 389 | .iter() |
| 390 | .find(|a| a.name == var_name && matches!(a.kind, AccessKind::Write)) |
| 391 | }) |
| 392 | .map(|a| a.offset) |
| 393 | .unwrap_or(frame.start); |
| 394 | |
| 395 | let var_len = var_name.len(); |
| 396 | let range = match ctx.backend.offset_range_to_lsp_range( |
| 397 | ctx.uri, |
| 398 | ctx.content, |
| 399 | diag_offset as usize, |
| 400 | diag_offset as usize + var_len, |
| 401 | ) { |
| 402 | Some(r) => r, |
| 403 | None => continue, |
| 404 | }; |
no test coverage detected