use std::io::{IsTerminal, Write};
use codespan_reporting::diagnostic::{Diagnostic, Label, Severity};
use codespan_reporting::files::SimpleFiles;
use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
use codespan_reporting::term::{self, Config};
use crol_eval::Sources;
use crol_syntax::SyntaxError;
use crate::{ColorWhen, MessageFormat};
pub(crate) struct Reporter {
stderr: StandardStream,
config: Config,
format: MessageFormat,
}
impl Reporter {
pub(crate) fn new(color: ColorWhen, format: MessageFormat) -> Self {
let choice = match color {
ColorWhen::Always => ColorChoice::Always,
ColorWhen::Never => ColorChoice::Never,
ColorWhen::Auto if std::io::stderr().is_terminal() => ColorChoice::Auto,
ColorWhen::Auto => ColorChoice::Never,
};
Self {
stderr: StandardStream::stderr(choice),
config: Config::default(),
format,
}
}
pub(crate) fn syntax_errors(&mut self, name: &str, text: &str, errors: &[SyntaxError]) {
let mut sources = Sources::new();
let file = sources.add(name, text);
let diagnostics: Vec<_> = errors
.iter()
.map(|e| {
crol_eval::Diagnostic::error(crol_eval::Span::new(file, e.range()), e.message())
})
.collect();
self.diagnostics(&sources, &diagnostics);
}
pub(crate) fn diagnostics(&mut self, sources: &Sources, diagnostics: &[crol_eval::Diagnostic]) {
if self.format == MessageFormat::Json {
let mut stdout = std::io::stdout().lock();
for diagnostic in diagnostics {
let _ = writeln!(stdout, "{}", json(sources, diagnostic));
}
return;
}
let mut files = SimpleFiles::new();
for (_, file) in sources.files() {
files.add(file.name().to_owned(), file.text().to_owned());
}
for diagnostic in diagnostics {
let range = |span: crol_eval::Span| {
usize::from(span.range.start())..usize::from(span.range.end())
};
let in_a_file = |span: crol_eval::Span| sources.get(span.file).is_some();
let mut labels = Vec::new();
if in_a_file(diagnostic.span) {
labels.push(Label::primary(
diagnostic.span.file.index(),
range(diagnostic.span),
));
}
for related in diagnostic.related.iter().filter(|r| in_a_file(r.span)) {
labels.push(
Label::secondary(related.span.file.index(), range(related.span))
.with_message(&related.message),
);
}
let severity = match diagnostic.severity {
crol_eval::Severity::Error => Severity::Error,
crol_eval::Severity::Warning => Severity::Warning,
};
let mut rendered = Diagnostic::new(severity)
.with_message(&diagnostic.message)
.with_labels(labels);
if let Some(context) = &diagnostic.context {
rendered = rendered.with_notes(vec![format!("while evaluating {context}")]);
}
let emitted =
term::emit_to_write_style(&mut self.stderr.lock(), &self.config, &files, &rendered);
if emitted.is_err() {
self.note(&diagnostic.display(sources).to_string());
}
}
}
pub(crate) fn error(&mut self, message: &str) {
if self.format == MessageFormat::Json {
let diagnostic = crol_eval::Diagnostic::error(crol_eval::Span::detached(), message);
let line = json(&Sources::new(), &diagnostic);
let _ = writeln!(std::io::stdout().lock(), "{line}");
return;
}
let diagnostic = Diagnostic::<()>::error().with_message(message);
let files = codespan_reporting::files::SimpleFile::new("", "");
let _ =
term::emit_to_write_style(&mut self.stderr.lock(), &self.config, &files, &diagnostic);
}
pub(crate) fn note(&mut self, message: &str) {
let _ = writeln!(self.stderr.lock(), "{message}");
}
}
fn json(sources: &Sources, diagnostic: &crol_eval::Diagnostic) -> String {
let place = |span: crol_eval::Span| {
let Some(file) = sources.get(span.file) else {
let none = "null";
return format!(
"\"file\":{none},\"line\":{none},\"column\":{none},\"end_line\":{none},\"end_column\":{none}"
);
};
let start = file.line_col(span.range.start());
let end = file.line_col(span.range.end());
format!(
"\"file\":{},\"line\":{},\"column\":{},\"end_line\":{},\"end_column\":{}",
json_string(file.name()),
start.line,
start.col,
end.line,
end.col
)
};
let severity = match diagnostic.severity {
crol_eval::Severity::Error => "error",
crol_eval::Severity::Warning => "warning",
};
let related: Vec<String> = diagnostic
.related
.iter()
.filter(|r| sources.get(r.span.file).is_some())
.map(|r| {
format!(
"{{{},\"message\":{}}}",
place(r.span),
json_string(&r.message)
)
})
.collect();
let context = diagnostic
.context
.as_deref()
.map_or_else(|| "null".to_owned(), json_string);
format!(
"{{\"severity\":\"{severity}\",\"message\":{},{},\"context\":{context},\"related\":[{}]}}",
json_string(&diagnostic.message),
place(diagnostic.span),
related.join(",")
)
}
fn json_string(text: &str) -> String {
let mut out = String::with_capacity(text.len() + 2);
out.push('"');
for c in text.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}