use ariadne::{Color, Config, IndexType, Label, Report, ReportKind};
use crate::span::Span;
#[derive(Debug, Clone)]
pub struct CompileError {
pub category: &'static str,
pub span: Span,
pub message: String,
pub labels: Vec<(Span, String)>,
pub notes: Vec<String>,
pub suggestions: Vec<Suggestion>,
}
#[derive(Debug, Clone)]
pub struct Suggestion {
pub message: String,
pub edits: Vec<(Span, String)>,
pub applicability: Applicability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Applicability {
MachineApplicable,
HasPlaceholders,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
}
impl Severity {
pub fn for_error(err: &CompileError) -> Severity {
crate::diagnostics::lookup(err.category)
.map(|d| d.severity)
.unwrap_or(Severity::Error)
}
}
pub fn partition_by_severity(
diagnostics: Vec<CompileError>,
) -> (Vec<CompileError>, Vec<CompileError>) {
diagnostics
.into_iter()
.partition(|d| Severity::for_error(d) == Severity::Error)
}
impl CompileError {
pub fn new(category: &'static str, span: Span, message: impl Into<String>) -> Self {
Self {
category,
span,
message: message.into(),
labels: Vec::new(),
notes: Vec::new(),
suggestions: Vec::new(),
}
}
pub fn offset_spans(mut self, delta: usize) -> Self {
self.span = self.span.offset(delta);
for (span, _) in &mut self.labels {
*span = span.offset(delta);
}
for suggestion in &mut self.suggestions {
for (span, _) in &mut suggestion.edits {
*span = span.offset(delta);
}
}
self
}
pub fn with_label(mut self, span: Span, label: impl Into<String>) -> Self {
self.labels.push((span, label.into()));
self
}
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
pub fn with_suggestion(
mut self,
message: impl Into<String>,
edits: Vec<(Span, String)>,
applicability: Applicability,
) -> Self {
self.suggestions.push(Suggestion {
message: message.into(),
edits,
applicability,
});
self
}
pub fn report_for<'a>(
&'a self,
filename: &'a str,
source: &str,
) -> Report<'a, (&'a str, std::ops::Range<usize>)> {
self.report_with_config(filename, Config::default(), source)
}
pub fn report_plain_for<'a>(
&'a self,
filename: &'a str,
source: &str,
) -> Report<'a, (&'a str, std::ops::Range<usize>)> {
self.report_with_config(filename, Config::default().with_color(false), source)
}
fn label_fits(span: &Span, source: &str) -> bool {
span.end <= source.len()
&& source.is_char_boundary(span.start)
&& source.is_char_boundary(span.end)
}
fn report_with_config<'a>(
&'a self,
filename: &'a str,
config: Config,
source: &str,
) -> Report<'a, (&'a str, std::ops::Range<usize>)> {
let primary_span = (filename, self.span.range());
let kind = match Severity::for_error(self) {
Severity::Error => ReportKind::Error,
Severity::Warning => ReportKind::Warning,
};
let mut builder = Report::build(kind, primary_span.clone())
.with_config(config.with_index_type(IndexType::Byte))
.with_code(self.category)
.with_message(&self.message)
.with_label(
Label::new(primary_span)
.with_message(&self.message)
.with_color(Color::Red),
);
for (span, label) in &self.labels {
if !Self::label_fits(span, source) {
builder = builder.with_note(label);
continue;
}
builder = builder.with_label(
Label::new((filename, span.range()))
.with_message(label)
.with_color(Color::Yellow),
);
}
for note in &self.notes {
builder = builder.with_note(note);
}
for suggestion in &self.suggestions {
builder = builder.with_note(format!("help: {}", suggestion.message));
}
builder.finish()
}
}
#[cfg(test)]
mod warning_channel_tests {
use super::*;
use crate::span::Span;
#[test]
fn partition_splits_by_severity() {
let warn = CompileError::new("bynk.given.unused_capability", Span::default(), "unused");
let err = CompileError::new("bynk.types.argument_mismatch", Span::default(), "bad");
let (errors, warnings) = partition_by_severity(vec![warn, err]);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].category, "bynk.types.argument_mismatch");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].category, "bynk.given.unused_capability");
}
#[test]
fn report_for_renders_warning_severity_as_a_warning_not_an_error() {
let source = "commons w\n\nfn f() -> Int { 1 }\n";
let warn = CompileError::new("bynk.given.unused_capability", Span::default(), "unused");
let rendered = {
let mut out = Vec::new();
let mut cache = ("w.bynk", ariadne::Source::from(source));
warn.report_plain_for("w.bynk", source)
.write(&mut cache, &mut out)
.unwrap();
String::from_utf8(out).unwrap()
};
assert!(
rendered.contains("Warning:"),
"expected a `Warning:` report for a warning-severity category, got:\n{rendered}"
);
assert!(
!rendered.contains("Error:"),
"a warning-severity category must not render as `Error:`, got:\n{rendered}"
);
let err = CompileError::new("bynk.types.argument_mismatch", Span::default(), "mismatch");
let rendered_err = {
let mut out = Vec::new();
let mut cache = ("w.bynk", ariadne::Source::from(source));
err.report_plain_for("w.bynk", source)
.write(&mut cache, &mut out)
.unwrap();
String::from_utf8(out).unwrap()
};
assert!(
rendered_err.contains("Error:"),
"an error-severity category must still render as `Error:`, got:\n{rendered_err}"
);
}
}