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(&self, serializer: S) -> Result 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, pub message: String, pub span: Option, } #[derive(Debug, Clone, Serialize)] pub struct DiagnosticBundle { pub diagnostics: Vec, } 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) -> 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, } let canonical_diags: Vec = 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 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")); } }