156 lines
4.5 KiB
Rust
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
|
|
}
|
|
}
|