use tower_lsp::lsp_types::*;
use crate::Backend;
impl Backend {
pub fn collect_syntax_error_diagnostics(
&self,
uri: &str,
content: &str,
out: &mut Vec<Diagnostic>,
) {
let errors = {
let map = self.parse_errors.read();
match map.get(uri) {
Some(errs) => errs.clone(),
None => return,
}
};
for (message, start_byte, end_byte) in &errors {
let range = if *start_byte == 0 && *end_byte == 0 {
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
}
} else {
match super::offset_range_to_lsp_range(
content,
*start_byte as usize,
*end_byte as usize,
) {
Some(r) => r,
None => {
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
}
}
}
};
out.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(NumberOrString::String("syntax_error".to_string())),
code_description: None,
source: Some("phpantom".to_string()),
message: message.clone(),
related_information: None,
tags: None,
data: None,
});
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use tower_lsp::lsp_types::*;
use crate::Backend;
fn collect(php: &str) -> Vec<Diagnostic> {
let backend = Backend::new_test();
let uri = "file:///test.php";
backend.update_ast(uri, &Arc::new(php.to_string()));
let mut out = Vec::new();
backend.collect_syntax_error_diagnostics(uri, php, &mut out);
out
}
#[test]
fn no_errors_for_valid_php() {
let php = r#"<?php
function greet(string $name): string {
return "Hello, " . $name;
}
"#;
let diags = collect(php);
assert!(
diags.is_empty(),
"Valid PHP should produce no syntax errors"
);
}
#[test]
fn error_for_unexpected_token() {
let php = "<?php\nfunction { broken }\n";
let diags = collect(php);
assert!(
!diags.is_empty(),
"Should produce at least one syntax error"
);
assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR));
}
#[test]
fn error_for_missing_semicolon() {
let php = "<?php\n$x = 1\n$y = 2;\n";
let diags = collect(php);
assert!(
!diags.is_empty(),
"Missing semicolon should produce a syntax error"
);
}
#[test]
fn error_has_correct_code_and_source() {
let php = "<?php\nfunction { broken }\n";
let diags = collect(php);
assert!(!diags.is_empty());
assert_eq!(
diags[0].code,
Some(NumberOrString::String("syntax_error".to_string()))
);
assert_eq!(diags[0].source, Some("phpantom".to_string()));
}
#[test]
fn error_has_nonempty_message() {
let php = "<?php\nfunction { broken }\n";
let diags = collect(php);
assert!(!diags.is_empty());
assert!(
!diags[0].message.is_empty(),
"Syntax error should have a descriptive message"
);
}
#[test]
fn error_range_is_on_correct_line() {
let php = "<?php\nfunction { broken }\n";
let diags = collect(php);
assert!(!diags.is_empty());
assert!(
diags[0].range.start.line >= 1,
"Error should be on line 1 or later, got line {}",
diags[0].range.start.line
);
}
#[test]
fn multiple_errors_reported() {
let php = "<?php\nfunction { }\nclass { }\n";
let diags = collect(php);
assert!(
diags.len() >= 2,
"Expected at least 2 syntax errors, got {}",
diags.len()
);
}
#[test]
fn valid_class_produces_no_errors() {
let php = r#"<?php
class Foo {
public function bar(): void {}
}
"#;
let diags = collect(php);
assert!(diags.is_empty());
}
#[test]
fn unclosed_string_produces_error() {
let php = "<?php\n$x = \"unclosed string\n";
let diags = collect(php);
assert!(
!diags.is_empty(),
"Unclosed string should produce a syntax error"
);
}
#[test]
fn parser_panic_produces_fallback_diagnostic() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\n";
{
let mut errors = backend.parse_errors.write();
errors.insert(
uri.to_string(),
vec![("Parse failed (internal error)".to_string(), 0, 0)],
);
}
let mut out = Vec::new();
backend.collect_syntax_error_diagnostics(uri, content, &mut out);
assert_eq!(out.len(), 1);
assert!(out[0].message.contains("Parse failed"));
assert_eq!(out[0].range.start.line, 0);
assert_eq!(out[0].range.start.character, 0);
}
}