php_lsp/analysis/diagnostics.rs
1use std::sync::Arc;
2
3use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
4
5use crate::document::ast::ParsedDoc;
6
7pub const PHP_LSP_SOURCE: &str = "php-lsp";
8
9/// Parse `source` without converting parse errors into LSP `Diagnostic`s.
10///
11/// Hot-path callers (workspace scan, the salsa `parsed_doc` query) discard
12/// diagnostics — using this variant skips an O(errors) Vec allocation per
13/// file. Callers that actually publish diagnostics call [`parse_document`]
14/// instead.
15pub fn parse_document_no_diags(source: &str) -> ParsedDoc {
16 ParsedDoc::parse(Arc::from(source))
17}
18
19/// Build LSP diagnostics from an already-parsed document. Separated from
20/// [`parse_document_no_diags`] so the workspace-scan path can skip the
21/// allocation entirely.
22pub fn diagnostics_from_doc(doc: &ParsedDoc) -> Vec<Diagnostic> {
23 let sv = doc.view();
24 doc.errors
25 .iter()
26 .map(|e| {
27 let span = e.span();
28 let start = sv.position_of(span.start);
29 let end = if span.end > span.start {
30 sv.position_of(span.end)
31 } else {
32 // Zero-width span: advance by the UTF-16 width of the character
33 // at the error position so the range is never a mid-surrogate
34 // slice (characters outside the BMP take 2 UTF-16 code units).
35 let ch_width = sv.source()[span.start as usize..]
36 .chars()
37 .next()
38 .map(|c| c.len_utf16() as u32)
39 .unwrap_or(1);
40 Position {
41 line: start.line,
42 character: start.character + ch_width,
43 }
44 };
45 Diagnostic {
46 range: Range { start, end },
47 severity: Some(DiagnosticSeverity::ERROR),
48 source: Some(PHP_LSP_SOURCE.to_string()),
49 message: e.to_string(),
50 ..Default::default()
51 }
52 })
53 .collect()
54}
55
56/// Merge per-file diagnostic categories into one ordered Vec.
57///
58/// Consistent order: parse errors → semantic issues.
59/// All call sites that publish diagnostics for a single file use this function
60/// so the ordering is uniform across `did_open`, `did_change`, `document_diagnostic`,
61/// `workspace_diagnostic`, and the dependent-republish path.
62pub fn merge_file_diagnostics(
63 parse: Vec<Diagnostic>,
64 semantic: Vec<Diagnostic>,
65) -> Vec<Diagnostic> {
66 let mut all = parse;
67 all.extend(semantic);
68 all
69}
70
71/// Parse `source` and return the (owned) `ParsedDoc` plus any parse diagnostics.
72pub fn parse_document(source: &str) -> (ParsedDoc, Vec<Diagnostic>) {
73 let doc = parse_document_no_diags(source);
74 let diagnostics = diagnostics_from_doc(&doc);
75 (doc, diagnostics)
76}