php_lsp/analysis/
diagnostics.rs1use std::sync::Arc;
2
3use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
4
5use crate::ast::ParsedDoc;
6
7pub const PHP_LSP_SOURCE: &str = "php-lsp";
8
9pub fn parse_document_no_diags(source: &str) -> ParsedDoc {
16 ParsedDoc::parse(Arc::from(source))
17}
18
19pub 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 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
56pub 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
71pub 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}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn valid_php_produces_no_diagnostics() {
84 let (doc, diags) = parse_document("<?php\nfunction greet() {}");
85 assert!(diags.is_empty());
86 assert!(!doc.program().stmts.is_empty());
87 }
88
89 #[test]
90 fn syntax_error_produces_diagnostic() {
91 let (_, diags) = parse_document("<?php\nclass {");
92 assert!(!diags.is_empty(), "expected at least one diagnostic");
93 assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR));
94 }
95
96 #[test]
100 fn probe_zero_width_spans() {
101 let cases: &[(&str, &str)] = &[
102 ("class_no_name", "<?php\nclass {"),
103 ("fn_no_name", "<?php\nfunction ("),
104 ("assign_no_rhs", "<?php\n$x ="),
105 ("bare_emoji", "<?php\n\u{1F600}"),
106 ("emoji_class", "<?php\nclass \u{1F600} {"),
107 ("emoji_then_valid", "<?php\n\u{1F600}\nfunction f() {}"),
109 ("emoji_in_string_ctx", "<?php\n$x = \u{1F600};"),
110 ];
111 for (label, src) in cases {
112 let doc = crate::ast::ParsedDoc::parse(src.to_string());
113 for e in &doc.errors {
114 let span = e.span();
115 let ch = src[span.start as usize..].chars().next();
116 println!(
117 "{label}: span=({},{}) zero_width={} char={ch:?} src_len={}",
118 span.start,
119 span.end,
120 span.end == span.start,
121 src.len(),
122 );
123 }
124 }
125 }
126}