use crate::common::files::FileManager; use crate::common::spans::{FileId, Span}; use serde::{Serialize, Serializer}; #[derive(Debug, Clone, PartialEq)] pub enum Severity { Error, Warning, } impl Serialize for Severity { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Severity::Error => serializer.serialize_str("error"), Severity::Warning => serializer.serialize_str("warning"), } } } #[derive(Debug, Clone, Serialize)] pub struct Diagnostic { pub severity: Severity, pub code: String, pub message: String, pub span: Span, pub related: Vec<(String, Span)>, } #[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(code: &str, message: String, span: Span) -> Self { let mut bundle = Self::new(); bundle.push(Diagnostic { severity: Severity::Error, code: code.to_string(), message, span, related: Vec::new(), }); bundle } pub fn has_errors(&self) -> bool { self.diagnostics .iter() .any(|d| matches!(d.severity, Severity::Error)) } /// Serializes the diagnostic bundle to canonical JSON, resolving file IDs via FileManager. /// The output is deterministic: diagnostics are sorted by (file_id, start, end, code). pub fn to_json(&self, file_manager: &FileManager) -> String { #[derive(Serialize)] struct CanonicalSpan { file: String, start: u32, end: u32, } #[derive(Serialize)] struct CanonicalDiag { severity: Severity, code: String, message: String, span: CanonicalSpan, related: Vec<(String, CanonicalSpan)>, } let mut diags = self.diagnostics.clone(); diags.sort_by(|a, b| { ( a.span.file.as_usize(), a.span.start, a.span.end, &a.code, ) .cmp(&(b.span.file.as_usize(), b.span.start, b.span.end, &b.code)) }); let canonical_diags: Vec = diags .iter() .map(|d| { let s = &d.span; let file = if s.file == FileId::INVALID { "".to_string() } else { file_manager .get_path(s.file.as_usize()) .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) .unwrap_or_else(|| format!("file_{}", s.file.as_usize())) }; let canonical_span = CanonicalSpan { file, start: s.start, end: s.end, }; let related = d .related .iter() .map(|(msg, sp)| { let file = if sp.file == FileId::INVALID { "".to_string() } else { file_manager .get_path(sp.file.as_usize()) .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) .unwrap_or_else(|| format!("file_{}", sp.file.as_usize())) }; let rsp = CanonicalSpan { file, start: sp.start, end: sp.end, }; (msg.clone(), rsp) }) .collect(); CanonicalDiag { severity: d.severity.clone(), code: d.code.clone(), message: d.message.clone(), span: canonical_span, related, } }) .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 } }