bquarkz 565fc0e451 dev/pbs (#8)
Co-authored-by: Nilton Constantino <nilton.constantino@visma.com>
Reviewed-on: #8
2026-02-03 15:28:30 +00:00

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"));
}
}