use std::error::Error as StdError;
use std::io;
use codespan_reporting::diagnostic::{Diagnostic as CsDiagnostic, Label};
use crate::common::source::{FileId, SourceMap, Span};
pub const BUG_URL: &str = "https://github.com/flowlog-rs/flowlog/issues/new";
pub(crate) fn primary_label(span: Span) -> Option<Label<FileId>> {
(!span.is_dummy()).then(|| Label::primary(span.file, span.range()))
}
pub(crate) fn secondary_label(span: Span) -> Option<Label<FileId>> {
(!span.is_dummy()).then(|| Label::secondary(span.file, span.range()))
}
pub(crate) fn labels(span: Span, msg: impl Into<String>) -> Vec<Label<FileId>> {
primary_label(span)
.map(|l| l.with_message(msg.into()))
.into_iter()
.collect()
}
pub trait Diagnostic: StdError + Send + Sync + 'static {
fn to_diagnostic(&self) -> CsDiagnostic<FileId>;
fn is_internal(&self) -> bool {
false
}
}
pub type BoxError = Box<dyn Diagnostic>;
impl<E: Diagnostic> From<E> for BoxError {
fn from(e: E) -> Self {
Box::new(e)
}
}
#[derive(Debug)]
pub struct InternalError {
pub(crate) stage: &'static str,
pub(crate) detail: String,
pub(crate) bug_url: &'static str,
}
impl InternalError {
pub fn new(stage: &'static str, detail: impl Into<String>, bug_url: &'static str) -> Self {
Self {
stage,
detail: detail.into(),
bug_url,
}
}
}
impl std::fmt::Display for InternalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"internal compiler error at stage `{}`: {}",
self.stage, self.detail
)
}
}
impl StdError for InternalError {}
impl Diagnostic for InternalError {
fn to_diagnostic(&self) -> CsDiagnostic<FileId> {
CsDiagnostic::bug()
.with_message(format!(
"internal compiler error at stage `{}`: {}",
self.stage, self.detail
))
.with_notes(vec![format!("please file a bug at {}", self.bug_url)])
}
fn is_internal(&self) -> bool {
true
}
}
pub fn emit(err: &BoxError, sources: &SourceMap, writer: &mut dyn io::Write) -> io::Result<()> {
let diag = err.to_diagnostic();
let config = codespan_reporting::term::Config::default();
codespan_reporting::term::emit_to_io_write(writer, &config, sources, &diag)
.map_err(io::Error::other)
}
pub fn emit_and_exit(err: impl Into<BoxError>, sources: &SourceMap) -> ! {
use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
let boxed: BoxError = err.into();
let code = if boxed.is_internal() { 2 } else { 1 };
let writer = StandardStream::stderr(ColorChoice::Auto);
let diag = boxed.to_diagnostic();
let config = codespan_reporting::term::Config::default();
let _ =
codespan_reporting::term::emit_to_write_style(&mut writer.lock(), &config, sources, &diag);
std::process::exit(code);
}
#[cfg(test)]
mod tests {
use super::*;
use codespan_reporting::diagnostic::Label;
#[derive(Debug)]
struct DemoError {
span: crate::common::source::Span,
msg: &'static str,
}
impl std::fmt::Display for DemoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.msg)
}
}
impl StdError for DemoError {}
impl Diagnostic for DemoError {
fn to_diagnostic(&self) -> CsDiagnostic<FileId> {
CsDiagnostic::error()
.with_message(self.msg)
.with_labels(vec![Label::primary(self.span.file, self.span.range())])
}
}
#[test]
fn question_mark_boxes_stage_error() {
fn inner() -> Result<(), DemoError> {
Err(DemoError {
span: crate::common::source::Span::new(FileId(0), 0, 1),
msg: "inner",
})
}
fn outer() -> Result<(), BoxError> {
inner()?;
Ok(())
}
let err = outer().unwrap_err();
assert_eq!(err.to_string(), "inner");
assert!(!err.is_internal());
}
#[test]
fn emit_renders_user_error_with_source_label() {
let mut sm = SourceMap::new();
let f = sm.add("demo.dl".into(), "abc def ghi".into());
let err: BoxError = DemoError {
span: crate::common::source::Span::new(f, 4, 7),
msg: "bad token",
}
.into();
let mut buf: Vec<u8> = Vec::new();
emit(&err, &sm, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("bad token"), "got: {out}");
assert!(out.contains("demo.dl"), "got: {out}");
assert!(out.contains("def"), "got: {out}");
}
#[test]
fn emit_renders_internal_error_as_bug() {
let sm = SourceMap::new();
let err: BoxError =
InternalError::new("codegen", "missing fingerprint", "https://example/bugs").into();
assert!(err.is_internal());
let mut buf: Vec<u8> = Vec::new();
emit(&err, &sm, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("bug"), "got: {out}");
assert!(out.contains("codegen"), "got: {out}");
assert!(out.contains("https://example/bugs"), "got: {out}");
}
}