use std::path::{Path, PathBuf};
#[derive(Clone, Debug)]
pub struct SourceLocation {
pub file: PathBuf,
pub line: u32,
pub col: u32,
}
impl std::fmt::Display for SourceLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}", self.file.display(), self.line, self.col)
}
}
#[derive(Clone, Debug)]
pub struct Diagnostic {
pub level: DiagnosticLevel,
pub message: String,
pub location: Option<SourceLocation>,
pub source_text: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DiagnosticLevel {
Error,
Warning,
Info,
}
#[derive(Clone, Debug, Default)]
pub struct DiagnosticCollector {
pub diagnostics: Vec<Diagnostic>,
current_file: Option<PathBuf>,
current_source: Option<String>,
line_offsets: Vec<u32>,
}
impl DiagnosticCollector {
pub fn new() -> Self {
Self::default()
}
pub fn set_file(&mut self, path: &Path, source: &str) {
self.current_file = Some(path.to_path_buf());
let mut offsets = vec![0u32];
for (i, b) in source.bytes().enumerate() {
if b == b'\n' {
offsets.push((i + 1) as u32);
}
}
self.line_offsets = offsets;
self.current_source = Some(source.to_string());
}
pub fn warn(&mut self, message: impl Into<String>) {
self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Warning,
message: message.into(),
location: None,
source_text: None,
});
}
pub fn warn_at(&mut self, message: impl Into<String>, offset: u32) {
let location = self.make_location(offset);
self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Warning,
message: message.into(),
location,
source_text: None,
});
}
pub fn warn_with_source(&mut self, message: impl Into<String>, source: impl Into<String>) {
self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Warning,
message: message.into(),
location: None,
source_text: Some(source.into()),
});
}
pub fn info(&mut self, message: impl Into<String>) {
self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Info,
message: message.into(),
location: None,
source_text: None,
});
}
pub fn error(&mut self, message: impl Into<String>) {
self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Error,
message: message.into(),
location: None,
source_text: None,
});
}
pub fn error_at(&mut self, message: impl Into<String>, offset: u32) {
let location = self.make_location(offset);
self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Error,
message: message.into(),
location,
source_text: None,
});
}
fn make_location(&self, offset: u32) -> Option<SourceLocation> {
let file = self.current_file.as_ref()?;
let (line, col) = self.offset_to_line_col(offset);
Some(SourceLocation {
file: file.clone(),
line,
col,
})
}
pub fn emit(&self) {
let mut seen = std::collections::HashSet::new();
for diag in &self.diagnostics {
let key = format!("{:?}:{}", diag.level, diag.message);
if !seen.insert(key) {
continue;
}
let prefix = match diag.level {
DiagnosticLevel::Error => "error",
DiagnosticLevel::Warning => "warning",
DiagnosticLevel::Info => "info",
};
if let Some(ref loc) = diag.location {
eprintln!("[ts-gen {prefix}]: {loc}: {}", diag.message);
} else {
eprintln!("[ts-gen {prefix}]: {}", diag.message);
}
if let Some(ref src) = diag.source_text {
eprintln!(" source: {src}");
}
}
}
fn offset_to_line_col(&self, offset: u32) -> (u32, u32) {
if self.line_offsets.is_empty() {
return (1, 1);
}
let line_idx = match self.line_offsets.binary_search(&offset) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
};
let line = (line_idx as u32) + 1;
let col = offset - self.line_offsets[line_idx] + 1;
(line, col)
}
pub fn has_warnings(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.level == DiagnosticLevel::Warning)
}
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.level == DiagnosticLevel::Error)
}
}