php_lsp/analysis/
diagnostics.rs1use std::sync::Arc;
2
3use php_rs_parser::diagnostics::ParseError;
4use tower_lsp_server::ls_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range};
5
6use crate::document::ast::{ParsedDoc, SourceView};
7
8pub const PHP_LSP_SOURCE: &str = "php-lsp";
9
10const PARSE_ERROR_CODE: &str = "SyntaxError";
12
13fn parse_error_message(e: &ParseError, sv: SourceView<'_>) -> String {
15 match e {
16 ParseError::UnclosedDelimiter {
17 delimiter,
18 opened_at,
19 ..
20 } => {
21 let pos = sv.position_of(opened_at.start);
22 format!(
23 "unclosed {delimiter} opened at {}:{}",
24 pos.line, pos.character
25 )
26 }
27 _ => e.to_string(),
28 }
29}
30
31pub fn parse_document_no_diags(source: &str) -> ParsedDoc {
38 ParsedDoc::parse(Arc::from(source))
39}
40
41pub fn diagnostics_from_doc(doc: &ParsedDoc, target: mir_analyzer::PhpVersion) -> Vec<Diagnostic> {
45 let sv = doc.view();
46 doc.errors
47 .iter()
48 .filter(|e| !supported_by_target(e, target))
49 .map(|e| {
50 let span = e.span();
51 let start = sv.position_of(span.start);
52 let end = if span.end > span.start {
53 sv.position_of(span.end)
54 } else {
55 let ch_width = sv.source()[span.start as usize..]
59 .chars()
60 .next()
61 .map(|c| c.len_utf16() as u32)
62 .unwrap_or(1);
63 Position {
64 line: start.line,
65 character: start.character + ch_width,
66 }
67 };
68 Diagnostic {
69 range: Range { start, end },
70 severity: Some(DiagnosticSeverity::ERROR),
71 source: Some(PHP_LSP_SOURCE.to_string()),
72 code: Some(NumberOrString::String(PARSE_ERROR_CODE.to_string())),
73 message: parse_error_message(e, sv),
74 ..Default::default()
75 }
76 })
77 .collect()
78}
79
80pub fn merge_file_diagnostics(
88 parse: Vec<Diagnostic>,
89 semantic: Vec<Diagnostic>,
90) -> Vec<Diagnostic> {
91 let mut all = parse;
92 all.extend(semantic);
93 all
94}
95
96fn supported_by_target(e: &ParseError, target: mir_analyzer::PhpVersion) -> bool {
99 match e {
100 ParseError::VersionTooLow { required, .. } => required
101 .parse::<mir_analyzer::PhpVersion>()
102 .is_ok_and(|required| required <= target),
103 _ => false,
104 }
105}
106
107pub fn parse_document(
109 source: &str,
110 target: mir_analyzer::PhpVersion,
111) -> (ParsedDoc, Vec<Diagnostic>) {
112 let doc = parse_document_no_diags(source);
113 let diagnostics = diagnostics_from_doc(&doc, target);
114 (doc, diagnostics)
115}