use std::path::Path;
use ariadne::Source;
use bynk_syntax::error::Severity;
use bynk_syntax::{CompileError, span};
pub fn render_errors(errors: &[CompileError], source: &str, filename: &str) -> String {
let mut out = Vec::new();
let mut cache = (filename, Source::from(source));
for err in errors {
err.report_for(filename, source.len())
.write(&mut cache, &mut out)
.expect("write to Vec<u8> cannot fail");
}
String::from_utf8_lossy(&out).into_owned()
}
pub fn render_errors_plain(errors: &[CompileError], source: &str, filename: &str) -> String {
let mut out = Vec::new();
let mut cache = (filename, Source::from(source));
for err in errors {
err.report_plain_for(filename, source.len())
.write(&mut cache, &mut out)
.expect("write to Vec<u8> cannot fail");
}
String::from_utf8_lossy(&out).into_owned()
}
pub fn print_errors(errors: &[CompileError], source: &str, filename: &str) {
let mut cache = (filename, Source::from(source));
for err in errors {
let _ = err.report_for(filename, source.len()).eprint(&mut cache);
}
}
pub fn print_project_errors(root: &Path, errors: &[CompileError]) {
let _ = root;
for err in errors {
eprintln!("[{}] {}", err.category, err.message);
for note in &err.notes {
eprintln!(" note: {note}");
}
}
}
pub fn print_errors_short(errors: &[CompileError], source: &str, filename: &str) {
eprint!("{}", render_errors_short(errors, source, filename));
}
pub fn render_errors_short(errors: &[CompileError], source: &str, filename: &str) -> String {
let mut out = String::new();
for err in errors {
out.push_str(&short_line(filename, source, err));
out.push('\n');
}
out
}
pub fn short_line(filename: &str, source: &str, err: &CompileError) -> String {
let (line, col) = span::line_col(source, err.span.start);
format!(
"{filename}:{line}:{col}: {}[{}]: {}",
severity_word(err),
err.category,
err.message
)
}
pub fn severity_word(err: &CompileError) -> &'static str {
match Severity::for_error(err) {
Severity::Error => "error",
Severity::Warning => "warning",
}
}
pub fn render_project_errors(errors: &[CompileError]) -> String {
let mut out = String::new();
for err in errors {
out.push_str(&format!("[{}] {}\n", err.category, err.message));
for note in &err.notes {
out.push_str(&format!(" note: {note}\n"));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use bynk_syntax::span::Span;
#[test]
fn underline_is_byte_indexed_on_non_ascii_lines() {
let source = "-- caféxyz bad\n";
let start = source.find("bad").unwrap();
let err = CompileError::new(
"bynk.test.example",
Span::new(start, start + 3),
"bad thing",
);
let rendered = render_errors_plain(&[err], source, "probe.bynk");
let source_line = rendered
.lines()
.find(|l| l.contains("caféxyz"))
.expect("snippet line present");
let marker_line = rendered
.lines()
.find(|l| l.contains('┬'))
.expect("marker line present");
let col_of = |line: &str, target: char| line.chars().take_while(|&c| c != target).count();
let b_col = col_of(source_line, 'b');
let caret_col = col_of(marker_line, '┬');
assert!(
(b_col..b_col + 3).contains(&caret_col),
"caret at display column {caret_col}, expected within `bad` at {b_col}..{}:\n{rendered}",
b_col + 3
);
}
#[test]
fn out_of_bounds_label_demotes_to_note() {
let source = "commons demo\n";
let err = CompileError::new("bynk.test.example", Span::new(0, 7), "problem here")
.with_label(
Span::new(5_000, 5_010),
"parameter declared here (in another file)",
);
let rendered = render_errors_plain(&[err], source, "probe.bynk");
assert!(
rendered.contains("parameter declared here"),
"label text survives as a note:\n{rendered}"
);
}
}