Co-authored-by: Nilton Constantino <nilton.constantino@visma.com> Reviewed-on: #8
175 lines
4.7 KiB
Rust
175 lines
4.7 KiB
Rust
use crate::common::files::FileManager;
|
|
use crate::common::spans::Span;
|
|
use serde::{Serialize, Serializer};
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum DiagnosticLevel {
|
|
Error,
|
|
Warning,
|
|
}
|
|
|
|
impl Serialize for DiagnosticLevel {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
match self {
|
|
DiagnosticLevel::Error => serializer.serialize_str("error"),
|
|
DiagnosticLevel::Warning => serializer.serialize_str("warning"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct Diagnostic {
|
|
#[serde(rename = "severity")]
|
|
pub level: DiagnosticLevel,
|
|
pub code: Option<String>,
|
|
pub message: String,
|
|
pub span: Option<Span>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct DiagnosticBundle {
|
|
pub diagnostics: Vec<Diagnostic>,
|
|
}
|
|
|
|
impl DiagnosticBundle {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
diagnostics: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, diagnostic: Diagnostic) {
|
|
self.diagnostics.push(diagnostic);
|
|
}
|
|
|
|
pub fn error(message: String, span: Option<Span>) -> Self {
|
|
let mut bundle = Self::new();
|
|
bundle.push(Diagnostic {
|
|
level: DiagnosticLevel::Error,
|
|
code: None,
|
|
message,
|
|
span,
|
|
});
|
|
bundle
|
|
}
|
|
|
|
pub fn has_errors(&self) -> bool {
|
|
self.diagnostics
|
|
.iter()
|
|
.any(|d| matches!(d.level, DiagnosticLevel::Error))
|
|
}
|
|
|
|
/// Serializes the diagnostic bundle to canonical JSON, resolving file IDs via FileManager.
|
|
pub fn to_json(&self, file_manager: &FileManager) -> String {
|
|
#[derive(Serialize)]
|
|
struct CanonicalSpan {
|
|
file: String,
|
|
start: u32,
|
|
end: u32,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct CanonicalDiag {
|
|
severity: DiagnosticLevel,
|
|
code: String,
|
|
message: String,
|
|
span: Option<CanonicalSpan>,
|
|
}
|
|
|
|
let canonical_diags: Vec<CanonicalDiag> = self.diagnostics.iter().map(|d| {
|
|
let canonical_span = d.span.and_then(|s| {
|
|
file_manager.get_path(s.file_id).map(|p| {
|
|
CanonicalSpan {
|
|
file: p.file_name().unwrap().to_string_lossy().to_string(),
|
|
start: s.start,
|
|
end: s.end,
|
|
}
|
|
})
|
|
});
|
|
|
|
CanonicalDiag {
|
|
severity: d.level.clone(),
|
|
code: d.code.clone().unwrap_or_else(|| "E_UNKNOWN".to_string()),
|
|
message: d.message.clone(),
|
|
span: canonical_span,
|
|
}
|
|
}).collect();
|
|
|
|
serde_json::to_string_pretty(&canonical_diags).unwrap()
|
|
}
|
|
}
|
|
|
|
impl From<Diagnostic> for DiagnosticBundle {
|
|
fn from(diagnostic: Diagnostic) -> Self {
|
|
let mut bundle = Self::new();
|
|
bundle.push(diagnostic);
|
|
bundle
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::common::files::FileManager;
|
|
use crate::frontends::pbs::PbsFrontend;
|
|
use crate::frontends::Frontend;
|
|
use std::fs;
|
|
use tempfile::tempdir;
|
|
|
|
fn get_diagnostics(code: &str) -> String {
|
|
let mut file_manager = FileManager::new();
|
|
let temp_dir = tempdir().unwrap();
|
|
let file_path = temp_dir.path().join("main.pbs");
|
|
fs::write(&file_path, code).unwrap();
|
|
|
|
let frontend = PbsFrontend;
|
|
match frontend.compile_to_ir(&file_path, &mut file_manager) {
|
|
Ok(_) => "[]".to_string(),
|
|
Err(bundle) => bundle.to_json(&file_manager),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_parse_error() {
|
|
let code = "fn main() { let x = ; }";
|
|
let json = get_diagnostics(code);
|
|
assert!(json.contains("E_PARSE_UNEXPECTED_TOKEN"));
|
|
assert!(json.contains("Expected expression"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_lex_error() {
|
|
let code = "fn main() { let x = \"hello ; }";
|
|
let json = get_diagnostics(code);
|
|
assert!(json.contains("E_LEX_UNTERMINATED_STRING"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_resolve_error() {
|
|
let code = "fn main() { let x = undefined_var; }";
|
|
let json = get_diagnostics(code);
|
|
assert!(json.contains("E_RESOLVE_UNDEFINED"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_type_error() {
|
|
let code = "fn main() { let x: int = \"hello\"; }";
|
|
let json = get_diagnostics(code);
|
|
assert!(json.contains("E_TYPE_MISMATCH"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_golden_namespace_collision() {
|
|
let code = "
|
|
declare struct Foo {}
|
|
fn main() {
|
|
let Foo = 1;
|
|
}
|
|
";
|
|
let json = get_diagnostics(code);
|
|
assert!(json.contains("E_RESOLVE_NAMESPACE_COLLISION"));
|
|
}
|
|
}
|