tamer: diagnose::report: Error tracking

This extracts error tracking into the Reporter itself, which is already
shared between lowering operations.  This can then be used to display the
number of errors.

A new formatter (in tamer::fmt) will be added to handle the singular/plural
conversion in place of "error(s)" in the future; I have more important
things to work on right now.

DEV-13158
main
Mike Gerwitz 2022-10-21 15:35:27 -04:00
parent f049da4496
commit 2ccdaf80fe
4 changed files with 114 additions and 41 deletions

View File

@ -111,16 +111,12 @@ fn compile<R: Reporter>(
let escaper = DefaultEscaper::default();
let ebuf = RefCell::new(String::new());
let has_err = RefCell::new(false);
fn report_err<E: Diagnostic, R: Reporter>(
e: &E,
reporter: &R,
has_err: &RefCell<bool>,
ebuf: &mut String,
) -> Result<(), TamecError> {
*has_err.borrow_mut() = true;
// See below note about buffering.
ebuf.clear();
writeln!(ebuf, "{}", reporter.render(e))?;
@ -155,7 +151,6 @@ fn compile<R: Reporter>(
report_err(
&e,
reporter,
&has_err,
&mut ebuf.borrow_mut(),
)
.unwrap();
@ -169,7 +164,6 @@ fn compile<R: Reporter>(
report_err(
&e,
reporter,
&has_err,
&mut ebuf.borrow_mut(),
)?;
x
@ -180,15 +174,17 @@ fn compile<R: Reporter>(
},
)?;
// TODO: Proper error summary,
// and roll into rest of lowering pipeline.
match has_err.into_inner() {
false => Ok(()),
true => {
println!("fatal: failed to compile `{}`", dest_path);
std::process::exit(1);
}
// TODO: Proper error summary and exit in `main`.
if reporter.has_errors() {
println!(
"fatal: failed to compile `{}` due to previous {} error(s)",
dest_path,
reporter.error_count(),
);
std::process::exit(1);
}
Ok(())
}
/// Entrypoint for the compiler

View File

@ -113,8 +113,13 @@ impl Diagnostic for Infallible {
/// Levels are used both for entire reports and for styling of individual
/// [`AnnotatedSpan`]s.
///
/// Lower levels are more severe
/// (e.g. level 1 is the worst).
/// Higher severity levels are represented by lower integer values
/// (e.g. level 1 is the worst),
/// like DEFCON levels.
/// The rationale here is that,
/// provided that you remember that these are 1-indexed,
/// you do not need to know how many levels exist to know how severe it
/// is.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)]
#[repr(u8)]
pub enum Level {
@ -145,6 +150,13 @@ pub enum Level {
Help,
}
impl Level {
/// Whether this error level represents an error.
pub fn is_error(self) -> bool {
self <= Self::Error
}
}
impl Display for Level {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {

View File

@ -74,6 +74,13 @@ pub trait Reporter {
/// and that we're not inadvertently suppressing the actual
/// diagnostic messages that were requested.
fn render<'d, D: Diagnostic>(&self, diagnostic: &'d D) -> Report<'d, D>;
/// Whether any reports have been rendered with an error level or higher.
fn has_errors(&self) -> bool;
/// Number of reports with an error level or higher that have been
/// rendered.
fn error_count(&self) -> usize;
}
/// Render diagnostic report in a highly visual way.
@ -96,12 +103,16 @@ pub struct VisualReporter<R: SpanResolver> {
/// shared resolver caching is far more important than saving on a
/// cell lock check.
resolver: RefCell<R>,
/// Number of reports with a severity level of error or higher.
err_n: RefCell<usize>,
}
impl<R: SpanResolver> VisualReporter<R> {
pub fn new(resolver: R) -> Self {
Self {
resolver: RefCell::new(resolver),
err_n: RefCell::new(0),
}
}
}
@ -120,8 +131,22 @@ impl<R: SpanResolver> Reporter for VisualReporter<R> {
// which is more aesthetically pleasing.
report.normalize_gutters();
if report.level.is_error() {
// Not worried about overflow panic
// (you have bigger problems if there are that many errors).
*self.err_n.borrow_mut() += 1;
}
report
}
fn has_errors(&self) -> bool {
self.error_count() > 0
}
fn error_count(&self) -> usize {
*self.err_n.borrow()
}
}
/// Request a diagnostic description and immediately resolve the provided

View File

@ -114,33 +114,37 @@ const FILE_MANY_LINES: &[u8] = b"\
\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\
\n100";
fn new_sut() -> impl Reporter {
let mut resolver = HashMap::<Context, BufSpanResolver<_>>::new();
let ctx_foo_bar = Context::from("foo/bar");
let ctx_bar_baz = Context::from("bar/baz");
let ctx_inv_utf = Context::from("invalid/utf8");
let ctx_mny_lns = Context::from("many/lines");
resolver.insert(
ctx_foo_bar,
BufSpanResolver::new(Cursor::new(FILE_FOO_BAR), ctx_foo_bar),
);
resolver.insert(
ctx_bar_baz,
BufSpanResolver::new(Cursor::new(FILE_BAR_BAZ), ctx_bar_baz),
);
resolver.insert(
ctx_inv_utf,
BufSpanResolver::new(Cursor::new(FILE_INVALID_UTF8), ctx_inv_utf),
);
resolver.insert(
ctx_mny_lns,
BufSpanResolver::new(Cursor::new(FILE_MANY_LINES), ctx_mny_lns),
);
VisualReporter::new(resolver)
}
macro_rules! assert_report {
($msg:expr, $aspans:expr, $expected:expr) => {
let mut resolver = HashMap::<Context, BufSpanResolver<_>>::new();
let ctx_foo_bar = Context::from("foo/bar");
let ctx_bar_baz = Context::from("bar/baz");
let ctx_inv_utf = Context::from("invalid/utf8");
let ctx_mny_lns = Context::from("many/lines");
resolver.insert(
ctx_foo_bar,
BufSpanResolver::new(Cursor::new(FILE_FOO_BAR), ctx_foo_bar),
);
resolver.insert(
ctx_bar_baz,
BufSpanResolver::new(Cursor::new(FILE_BAR_BAZ), ctx_bar_baz),
);
resolver.insert(
ctx_inv_utf,
BufSpanResolver::new(Cursor::new(FILE_INVALID_UTF8), ctx_inv_utf),
);
resolver.insert(
ctx_mny_lns,
BufSpanResolver::new(Cursor::new(FILE_MANY_LINES), ctx_mny_lns),
);
let sut = VisualReporter::new(resolver);
let sut = new_sut();
assert_eq!(
sut.render(&StubError($msg.into(), $aspans)).to_string(),
@ -589,3 +593,39 @@ error: wide gutter
"
);
}
#[test]
fn visual_reporter_tracks_errors() {
let sut = new_sut();
let ctx = Context::from("error/tracking");
fn feed_aspan(sut: &impl Reporter, aspan: AnnotatedSpan<'static>) {
// We do not care about the report value;
// we're only interested in how it tracks errors for this test.
let _ = sut.render(&StubError("ignored".into(), vec![aspan]));
}
// We should start with no errors.
assert_eq!(sut.error_count(), 0);
assert!(!sut.has_errors());
// Help must not increment.
feed_aspan(&sut, ctx.span(0, 1).help("no increment"));
assert_eq!(sut.error_count(), 0);
assert!(!sut.has_errors());
// Note must not increment.
feed_aspan(&sut, ctx.span(0, 1).note("no increment"));
assert_eq!(sut.error_count(), 0);
assert!(!sut.has_errors());
// Error must increment.
feed_aspan(&sut, ctx.span(0, 1).error("increment"));
assert_eq!(sut.error_count(), 1);
assert!(sut.has_errors());
// Internal error must increment.
feed_aspan(&sut, ctx.span(0, 1).error("increment"));
assert_eq!(sut.error_count(), 2);
assert!(sut.has_errors());
}