use std::collections::HashMap;
use std::fmt::{self, Write};
use std::path::{Path, PathBuf};
use itertools::Itertools;
use crate::codespan;
use crate::diagnostic::{Diagnostic, DiagnosticCode};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid path: {path}")]
InvalidPath { path: PathBuf },
#[error("cannot find project root")]
CannotFindProjectRoot,
#[error("cannot read source file at {file}:\n {io}")]
CannotReadSourceFile {
file: std::path::PathBuf,
io: std::io::Error,
},
#[error("{}", DisplayMessages(.diagnostics))]
Compile { diagnostics: Vec<DiagnosticMessage> },
#[error("{message}")]
UnsupportedExternal { message: String },
}
impl Error {
pub(crate) fn from_diagnostics(
diagnostics: Vec<Diagnostic>,
sources: &impl crate::project::SourceProvider,
) -> Self {
let diagnostics = compose_diagnostic_messages(diagnostics, sources);
Error::Compile { diagnostics }
}
}
#[derive(Debug)]
pub struct DiagnosticMessage {
diagnostic: Diagnostic,
display: String,
range: codespan::Range,
additional_ranges: Vec<Option<codespan::Range>>,
}
impl DiagnosticMessage {
pub fn code(&self) -> &'static str {
self.diagnostic.code.get()
}
pub fn message(&self) -> &str {
&self.diagnostic.message
}
pub fn span(&self) -> &Option<crate::Span> {
&self.diagnostic.span
}
pub fn additional(&self) -> Vec<Additional<'_>> {
self.diagnostic
.additional
.iter()
.enumerate()
.map(|(i, additional)| Additional {
additional,
range: self.additional_ranges.get(i).and_then(|x| x.as_ref()),
})
.collect()
}
pub fn display(&self) -> &str {
&self.display
}
pub fn range(&self) -> &codespan::Range {
&self.range
}
}
#[derive(Debug)]
pub struct Additional<'d> {
additional: &'d crate::diagnostic::Additional,
range: Option<&'d codespan::Range>,
}
impl<'d> Additional<'d> {
pub fn message(&self) -> &'d str {
&self.additional.message
}
pub fn span(&self) -> Option<codespan::Span> {
self.additional.span
}
pub fn range(&self) -> Option<&'d codespan::Range> {
self.range
}
}
fn compose_diagnostic_messages(
diagnostics: Vec<Diagnostic>,
sources: &impl crate::project::SourceProvider,
) -> Vec<DiagnosticMessage> {
let mut cache = FileTreeCache::new(sources);
let mut messages = Vec::with_capacity(diagnostics.len());
for diagnostic in diagnostics {
let Some(span) = diagnostic.span else {
panic!(
"missing diagnostic span: [{:?}] {}, {:#?}",
diagnostic.code, diagnostic.message, diagnostic.additional
);
};
let range = compose_range(span, sources, &mut cache);
let display = compose_display(&diagnostic, sources, &mut cache);
let mut additional_ranges = Vec::with_capacity(diagnostic.additional.len());
for a in &diagnostic.additional {
let range = a.span.map(|s| compose_range(s, sources, &mut cache));
additional_ranges.push(range);
}
messages.push(DiagnosticMessage {
diagnostic,
display,
range,
additional_ranges,
});
}
messages
}
fn compose_display<S>(
diagnostic: &Diagnostic,
sources: &impl crate::project::SourceProvider,
cache: &mut FileTreeCache<S>,
) -> String
where
S: crate::project::SourceProvider,
{
use ariadne::{Config, Label, Report, ReportKind};
let config = Config::default().with_color(false);
let span = diagnostic.span.unwrap();
let (source_path, _) = sources.get_by_id(span.source_id).unwrap();
let span = std::ops::Range::from(span);
let kind = match diagnostic.code.get_severity() {
crate::diagnostic::Severity::Warning => ReportKind::Warning,
crate::diagnostic::Severity::Error => ReportKind::Error,
};
let mut report = Report::build(kind, (source_path, span.clone()))
.with_config(config)
.with_label(Label::new((source_path, span)).with_message(&diagnostic.message));
if diagnostic.code != DiagnosticCode::CUSTOM {
report = report.with_code(diagnostic.code.get());
}
let mut notes = String::new();
for additional in &diagnostic.additional {
if let Some(span) = additional.span {
let span = std::ops::Range::from(span);
report.add_label(Label::new((source_path, span)).with_message(&diagnostic.message))
} else {
notes += &additional.message;
notes += "\n";
}
}
if !notes.is_empty() {
report.set_note(notes);
}
let mut out = Vec::new();
report.finish().write(cache, &mut out).unwrap();
let out = String::from_utf8(out).unwrap();
out.lines().map(|l| l.trim_end()).join("\n")
}
fn compose_range<S>(
span: codespan::Span,
sources: &impl crate::project::SourceProvider,
cache: &mut FileTreeCache<S>,
) -> codespan::Range
where
S: crate::project::SourceProvider,
{
use ariadne::Cache;
let (source_path, _content) = sources.get_by_id(span.source_id).unwrap();
let source = cache.fetch(&source_path).unwrap();
let source_len = source.len();
let Some(start) = source.get_byte_line(span.start as usize) else {
panic!("span {span:?} is out of bounds of the source (len = {source_len})",);
};
let start = codespan::LineColumn {
line: start.1 as u32,
column: start.2 as u32,
};
let Some(end) = source.get_byte_line(span.start as usize + span.len as usize) else {
panic!("span {span:?} is out of bounds of the source (len = {source_len})",);
};
let end = codespan::LineColumn {
line: end.1 as u32,
column: end.2 as u32,
};
codespan::Range { start, end }
}
struct FileTreeCache<'a, S: crate::project::SourceProvider> {
provider: &'a S,
cache: HashMap<PathBuf, ariadne::Source>,
}
impl<'a, S: crate::project::SourceProvider> FileTreeCache<'a, S> {
fn new(file_tree: &'a S) -> Self {
FileTreeCache {
provider: file_tree,
cache: HashMap::new(),
}
}
}
impl<'a, S: crate::project::SourceProvider> ariadne::Cache<&Path> for FileTreeCache<'a, S> {
type Storage = String;
fn fetch(
&mut self,
path: &&Path,
) -> Result<&ariadne::Source<<Self as ariadne::Cache<&Path>>::Storage>, impl std::fmt::Debug>
{
let (_, content) = match self.provider.get_by_path(path) {
Some(v) => v,
None => return Err(format!("Unknown file `{path:?}`")),
};
Ok(self
.cache
.entry((*path).to_owned())
.or_insert_with(|| ariadne::Source::from(content.to_string())))
}
fn display<'b>(&self, id: &'b &Path) -> Option<impl std::fmt::Display + 'b> {
if id.as_os_str().is_empty() {
Some(self.provider.get_root().file_name()?.display().to_string())
} else {
Some(id.display().to_string())
}
}
}
struct DisplayMessages<'a>(&'a Vec<DiagnosticMessage>);
impl<'a> std::fmt::Display for DisplayMessages<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for d in self.0 {
f.write_str(&d.display)?;
f.write_char('\n')?;
}
Ok(())
}
}