use std::{fmt, mem::transmute};
use miette::{GraphicalReportHandler, Severity, SourceOffset, SourceSpan};
use swc_common::{
errors::{Diagnostic, DiagnosticId, Level, SubDiagnostic},
BytePos, FileName, SourceMap, Span,
};
pub struct PrettyDiagnostic<'a> {
source_code: PrettySourceCode<'a>,
d: &'a Diagnostic,
children: Vec<PrettySubDiagnostic<'a>>,
}
impl<'a> PrettyDiagnostic<'a> {
pub fn new(d: &'a Diagnostic, cm: &'a SourceMap, skip_filename: bool) -> Self {
let source_code = PrettySourceCode { cm, skip_filename };
let children = d
.children
.iter()
.filter(|d| !matches!(d.level, Level::Help))
.map(|d| PrettySubDiagnostic { source_code, d })
.collect();
Self {
source_code,
d,
children,
}
}
}
impl miette::Diagnostic for PrettyDiagnostic<'_> {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.d
.code
.as_ref()
.map(|v| match v {
DiagnosticId::Error(v) => v,
DiagnosticId::Lint(v) => v,
})
.map(|code| Box::new(code) as Box<dyn fmt::Display>)
}
fn severity(&self) -> Option<Severity> {
level_to_severity(self.d.level)
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.d
.children
.iter()
.filter(|s| s.level == Level::Help)
.map(|s| Box::new(&s.message[0].0) as Box<_>)
.next()
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
if let Some(span) = self.d.span.primary_span() {
if span.lo.is_dummy() || span.hi.is_dummy() {
return None;
}
} else {
return None;
}
Some(&self.source_code as &dyn miette::SourceCode)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let iter = self.d.span.span_labels().into_iter().map(|span_label| {
miette::LabeledSpan::new_with_span(span_label.label, convert_span(span_label.span))
});
Some(Box::new(iter))
}
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
if self.children.is_empty() {
None
} else {
Some(Box::new(
self.children.iter().map(|d| d as &dyn miette::Diagnostic),
))
}
}
}
impl<'a> PrettyDiagnostic<'a> {
pub fn to_pretty_string(&self, handler: &'a GraphicalReportHandler) -> String {
let mut wr = String::new();
handler.render_report(&mut wr, self).unwrap();
wr
}
}
impl std::error::Error for PrettyDiagnostic<'_> {}
impl fmt::Debug for PrettyDiagnostic<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(&self.d, f)
}
}
impl fmt::Display for PrettyDiagnostic<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.d.message[0].0.fmt(f)
}
}
#[derive(Clone, Copy)]
pub struct PrettySourceCode<'a> {
cm: &'a SourceMap,
skip_filename: bool,
}
impl miette::SourceCode for PrettySourceCode<'_> {
fn read_span<'a>(
&'a self,
span: &SourceSpan,
context_lines_before: usize,
context_lines_after: usize,
) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
let lo = span.offset();
let hi = lo + span.len();
let mut span = Span::new(BytePos(lo as _), BytePos(hi as _));
span = self
.cm
.with_span_to_prev_source(span, |src| {
let len = src
.rsplit('\n')
.take(context_lines_before + 1)
.map(|s| s.len() + 1)
.sum::<usize>();
span.lo.0 -= (len as u32) - 1;
span
})
.unwrap_or(span);
span = self
.cm
.with_span_to_next_source(span, |src| {
let len = src
.split('\n')
.take(context_lines_after + 1)
.map(|s| s.len() + 1)
.sum::<usize>();
span.hi.0 += (len as u32) - 1;
span
})
.unwrap_or(span);
span = self
.cm
.with_snippet_of_span(span, |src| {
if src.lines().next().is_some() {
return span;
}
let lo = src.len() - src.trim_start().len();
let hi = src.len() - src.trim_end().len();
span.lo.0 += lo as u32;
span.hi.0 -= hi as u32;
span
})
.unwrap_or(span);
let mut src = self
.cm
.with_snippet_of_span(span, |s| unsafe { transmute::<&str, &str>(s) })
.unwrap_or(" ");
if span.lo == span.hi {
src = " ";
}
let loc = self.cm.lookup_char_pos(span.lo());
let line_count = loc.file.analyze().lines.len();
let name = if self.skip_filename {
None
} else {
match &*loc.file.name {
FileName::Real(ref path) => Some(path.to_string_lossy().into_owned()),
FileName::Custom(ref name) => Some(name.clone()),
FileName::Anon => None,
_ => Some(loc.file.name.to_string()),
}
};
Ok(Box::new(SpanContentsImpl {
_cm: self.cm,
data: src,
span: convert_span(span),
line: loc.line.saturating_sub(1),
column: loc.col_display,
line_count,
name,
}))
}
}
pub fn to_pretty_source_code(cm: &SourceMap, skip_filename: bool) -> PrettySourceCode<'_> {
PrettySourceCode { cm, skip_filename }
}
struct PrettySubDiagnostic<'a> {
source_code: PrettySourceCode<'a>,
d: &'a SubDiagnostic,
}
impl std::error::Error for PrettySubDiagnostic<'_> {}
impl fmt::Debug for PrettySubDiagnostic<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(&self.d, f)
}
}
impl fmt::Display for PrettySubDiagnostic<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.d.message[0].0, f)
}
}
impl miette::Diagnostic for PrettySubDiagnostic<'_> {
fn severity(&self) -> Option<Severity> {
level_to_severity(self.d.level)
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.source_code)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let iter = self.d.span.span_labels().into_iter().map(|span_label| {
miette::LabeledSpan::new_with_span(span_label.label, convert_span(span_label.span))
});
Some(Box::new(iter))
}
}
struct SpanContentsImpl<'a> {
_cm: &'a SourceMap,
data: &'a str,
span: SourceSpan,
line: usize,
column: usize,
line_count: usize,
name: Option<String>,
}
impl<'a> miette::SpanContents<'a> for SpanContentsImpl<'a> {
fn data(&self) -> &'a [u8] {
self.data.as_bytes()
}
fn span(&self) -> &SourceSpan {
&self.span
}
fn line(&self) -> usize {
self.line
}
fn column(&self) -> usize {
self.column
}
fn line_count(&self) -> usize {
self.line_count
}
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
}
fn level_to_severity(level: Level) -> Option<Severity> {
match level {
Level::FailureNote | Level::Bug | Level::Fatal | Level::PhaseFatal | Level::Error => {
Some(Severity::Error)
}
Level::Warning => Some(Severity::Warning),
Level::Note | Level::Help => Some(Severity::Advice),
Level::Cancelled => None,
}
}
pub fn convert_span(span: Span) -> SourceSpan {
let len = span.hi - span.lo;
let start = SourceOffset::from(span.lo.0 as usize);
SourceSpan::new(start, len.0 as usize)
}
pub trait ToPrettyDiagnostic {
fn to_pretty_diagnostic<'a>(
&'a self,
cm: &'a SourceMap,
skip_filename: bool,
) -> PrettyDiagnostic<'a>;
fn to_pretty_string<'a>(
&self,
cm: &'a SourceMap,
skip_filename: bool,
handler: &'a GraphicalReportHandler,
) -> String;
}
impl ToPrettyDiagnostic for Diagnostic {
fn to_pretty_diagnostic<'a>(
&'a self,
cm: &'a SourceMap,
skip_filename: bool,
) -> PrettyDiagnostic<'a> {
PrettyDiagnostic::new(self, cm, skip_filename)
}
fn to_pretty_string<'a>(
&self,
cm: &'a SourceMap,
skip_filename: bool,
handler: &'a GraphicalReportHandler,
) -> String {
let pretty_diagnostic = PrettyDiagnostic::new(self, cm, skip_filename);
pretty_diagnostic.to_pretty_string(handler)
}
}