156 lines
4.5 KiB
Rust

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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<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(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<CanonicalDiag> = diags
.iter()
.map(|d| {
let s = &d.span;
let file = if s.file == FileId::INVALID {
"<virtual>".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 {
"<virtual>".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<Diagnostic> for DiagnosticBundle {
fn from(diagnostic: Diagnostic) -> Self {
let mut bundle = Self::new();
bundle.push(diagnostic);
bundle
}
}