use crate::diagnostics::{DiagnosticKind, DiagnosticSeverity, ExtractedDiagnostic};
use miette::{Diagnostic, LabeledSpan, Severity, SourceSpan};
use std::collections::HashMap;
use std::path::Path;
use std::sync::{LazyLock, Mutex};
static SOURCE_CACHE: LazyLock<Mutex<HashMap<String, String>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn clear_source_cache() {
SOURCE_CACHE.lock().unwrap().clear();
}
pub fn load_source(file: &str) -> Option<String> {
let mut cache = SOURCE_CACHE.lock().unwrap();
if let Some(source) = cache.get(file) {
return Some(source.clone());
}
let path = Path::new(file);
if path.exists() && path.is_file() {
if let Ok(content) = std::fs::read_to_string(path) {
cache.insert(file.to_string(), content.clone());
return Some(content);
}
}
None
}
pub fn line_col_to_offset(source: &str, line: u32, column: u32) -> Option<usize> {
let lines: Vec<&str> = source.lines().collect();
if line == 0 || line as usize > lines.len() {
return None;
}
let line_idx = (line - 1) as usize;
let mut offset = 0;
for line in lines.iter().take(line_idx) {
offset += line.len() + 1; }
let target_line = lines[line_idx];
let col_bytes = if column == 0 {
0
} else {
target_line
.char_indices()
.nth((column - 1) as usize)
.map(|(pos, _)| pos)
.unwrap_or(target_line.len())
};
Some(offset + col_bytes)
}
pub fn calculate_span_length(source: &str, offset: usize) -> usize {
if offset >= source.len() {
return 1;
}
let remaining = &source[offset..];
if let Some(end) = remaining
.char_indices()
.find(|(_, c)| !c.is_alphanumeric() && *c != '_')
.map(|(pos, _)| pos)
{
end.max(1)
} else {
1
}
}
pub fn calculate_enhanced_span(source: &str, offset: usize, kind: &DiagnosticKind) -> usize {
if offset >= source.len() {
return 1;
}
let remaining = &source[offset..];
match kind {
DiagnosticKind::MissingExport => {
if let Some(import_pos) = remaining.find("import") {
if import_pos < 50 {
if let Some(end) = remaining[import_pos..].find([';', '\n']) {
return (import_pos + end).max(1);
}
}
}
calculate_span_length(source, offset)
}
DiagnosticKind::ParseError | DiagnosticKind::Transform => {
if let Some(quote_pos) = remaining.find(['"', '\'', '`']) {
if quote_pos < 20 {
let quote_char = remaining.as_bytes()[quote_pos] as char;
if let Some(end_quote) = remaining[quote_pos + 1..].find(quote_char) {
return (quote_pos + end_quote + 2).max(1);
}
}
}
calculate_span_length(source, offset)
}
_ => calculate_span_length(source, offset),
}
}
#[derive(Debug)]
pub struct DiagnosticError {
diag: ExtractedDiagnostic,
named_source: Option<miette::NamedSource<String>>,
related_errors: Vec<ChainedError>,
}
#[derive(Debug)]
pub struct ChainedError {
message: String,
}
impl std::error::Error for ChainedError {}
impl std::fmt::Display for ChainedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl Diagnostic for ChainedError {
fn code(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
None
}
fn severity(&self) -> Option<Severity> {
Some(Severity::Advice)
}
}
impl std::error::Error for DiagnosticError {}
impl std::fmt::Display for DiagnosticError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.diag.message)?;
if !self.diag.error_chain.is_empty() {
writeln!(f)?;
writeln!(f, "Caused by:")?;
for (i, cause) in self.diag.error_chain.iter().enumerate() {
writeln!(f, " {}: {}", i + 1, cause)?;
}
}
Ok(())
}
}
impl Diagnostic for DiagnosticError {
fn code(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
Some(Box::new(format!("{:?}", self.diag.kind)))
}
fn severity(&self) -> Option<Severity> {
Some(match self.diag.severity {
DiagnosticSeverity::Error => Severity::Error,
DiagnosticSeverity::Warning => Severity::Warning,
})
}
fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
self.diag
.help
.as_ref()
.map(|h| Box::new(h.clone()) as Box<dyn std::fmt::Display>)
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.named_source
.as_ref()
.map(|s| s as &dyn miette::SourceCode)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
if let (Some(_file), Some(line), Some(column)) =
(&self.diag.file, self.diag.line, self.diag.column)
{
if let Some(source) = &self.named_source {
let source_str = source.inner();
if let Some(offset) = line_col_to_offset(source_str, line, column) {
let length = calculate_enhanced_span(source_str, offset, &self.diag.kind);
let span = SourceSpan::new(offset.into(), length);
let label = match &self.diag.kind {
DiagnosticKind::MissingExport => "Missing export",
DiagnosticKind::ParseError => "Parse error",
DiagnosticKind::CircularDependency => "Circular dependency",
DiagnosticKind::UnresolvedEntry => "Unresolved entry",
DiagnosticKind::UnresolvedImport => "Unresolved import",
DiagnosticKind::InvalidOption => "Invalid option",
DiagnosticKind::Plugin => "Plugin error",
DiagnosticKind::Transform => "Transform error",
DiagnosticKind::Other(_) => "Error",
};
return Some(Box::new(std::iter::once(LabeledSpan::new(
Some(label.to_string()),
span.offset(),
span.len(),
))));
}
}
}
None
}
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
if self.related_errors.is_empty() {
None
} else {
Some(Box::new(
self.related_errors.iter().map(|e| e as &dyn Diagnostic),
))
}
}
}
pub fn to_diagnostic_error(diag: ExtractedDiagnostic) -> DiagnosticError {
let named_source = diag.file.as_ref().and_then(|file| {
load_source(file).map(|source| miette::NamedSource::new(file.clone(), source))
});
let related_errors: Vec<ChainedError> = diag
.error_chain
.iter()
.map(|msg| ChainedError {
message: msg.clone(),
})
.collect();
DiagnosticError {
diag,
named_source,
related_errors,
}
}