Find the docblock text for the function/method enclosing `diag_line`. Searches backward from `diag_line` to find the nearest `function` keyword (which may be on the diagnostic line itself, e.g. on the signature, or on a preceding line when the diagnostic is inside the body or in the docblock above). Then looks for a preceding `/** ... */` block. Returns the raw docblock text (from `/**` to `*/`
(content: &str, diag_line: usize)
| 529 | /// `/** ... */` block. Returns the raw docblock text (from `/**` to |
| 530 | /// `*/` inclusive) if found, or an empty string if no docblock exists. |
| 531 | fn enclosing_docblock_text(content: &str, diag_line: usize) -> String { |
| 532 | use crate::util::{contains_function_keyword, strip_trailing_modifiers}; |
| 533 | |
| 534 | let lines: Vec<&str> = content.lines().collect(); |
| 535 | if diag_line >= lines.len() { |
| 536 | return String::new(); |
| 537 | } |
| 538 | |
| 539 | // Scan backward from `diag_line` looking for a line that contains |
| 540 | // the `function` keyword. This handles three cases: |
| 541 | // 1. Diagnostic inside the function body → walks up to the |
| 542 | // signature line. |
| 543 | // 2. Diagnostic on the signature line → matches immediately. |
| 544 | // 3. Diagnostic on the docblock above → walks down would be |
| 545 | // needed, but PHPStan diagnostics land on the signature or |
| 546 | // body, not the docblock lines. If we reach the docblock |
| 547 | // line we still need to find the function below it. As a |
| 548 | // pragmatic fallback we also scan forward a few lines. |
| 549 | let mut func_line: Option<usize> = None; |
| 550 | for idx in (0..=diag_line).rev() { |
| 551 | if contains_function_keyword(lines[idx]) { |
| 552 | func_line = Some(idx); |
| 553 | break; |
| 554 | } |
| 555 | } |
| 556 | |
| 557 | // Fallback: if the diagnostic is on a docblock line above the |
| 558 | // function, scan forward a few lines to find the signature. |
| 559 | if func_line.is_none() { |
| 560 | let start = diag_line + 1; |
| 561 | let limit = (diag_line + 10).min(lines.len()); |
| 562 | for (i, line) in lines[start..limit].iter().enumerate() { |
| 563 | if contains_function_keyword(line) { |
| 564 | func_line = Some(start + i); |
| 565 | break; |
| 566 | } |
| 567 | } |
| 568 | } |
| 569 | |
| 570 | let func_line = match func_line { |
| 571 | Some(l) => l, |
| 572 | None => return String::new(), |
| 573 | }; |
| 574 | |
| 575 | // Compute the byte offset of the `function` keyword on that line. |
| 576 | let line_byte_start: usize = lines.iter().take(func_line).map(|l| l.len() + 1).sum(); |
| 577 | let func_kw_rel = match lines[func_line].find("function") { |
| 578 | Some(p) => p, |
| 579 | None => return String::new(), |
| 580 | }; |
| 581 | let func_kw_pos = line_byte_start + func_kw_rel; |
| 582 | |
| 583 | // Look for a `/** ... */` block before the function keyword |
| 584 | // (skipping modifiers and whitespace). |
| 585 | let before_func = &content[..func_kw_pos]; |
| 586 | let trimmed = before_func.trim_end(); |
| 587 | |
| 588 | let after_mods = strip_trailing_modifiers(trimmed); |