// Diagnostic system rendering // // Copyright (C) 2014-2021 Ryan Specialty Group, LLC. // // This file is part of TAME. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Rendering of diagnostic information. use super::{AnnotatedSpan, Diagnostic, Label, Level, SpanResolver}; use crate::span::{Span, SpanOffsetSize, UNKNOWN_SPAN}; use std::fmt::{self, Write}; pub trait Reporter { /// Render diagnostic report. /// /// Please be mindful of where this report is being rendered `to`. /// For example, /// if rendering to standard out, /// it is a good idea to buffer the entire report before flushing to /// stdout, /// otherwise the report may become interleaved with other /// concurrent processes /// (e.g. if TAMER is being invoked using `make -jN`). fn render( &mut self, diagnostic: &impl Diagnostic, to: &mut impl Write, ) -> Result<(), fmt::Error>; /// Render a diagnostic report into an owned [`String`]. /// /// This invokes [`Reporter::render`] on a newly allocated [`String`]. fn render_to_string( &mut self, diagnostic: &impl Diagnostic, ) -> Result { let mut str = String::new(); self.render(diagnostic, &mut str)?; Ok(str) } } /// Render diagnostic report in a highly visual way. /// /// This report is modeled after Rust's default error reporting, /// most notable for including sections of source code associated with /// spans, /// underlining spans, /// and including helpful information that walks the user through /// understanding why the error occurred and how to approach resolving /// it. pub struct VisualReporter { resolver: R, } impl VisualReporter { pub fn new(resolver: R) -> Self { Self { resolver } } /// Render a raw [`Span`] that could not be resolved into a [`ResolvedSpan`]. /// /// This is not ideal, /// but provides reasonable fallback information in a situation where /// the diagnostic system fails. /// The user still has enough information to diagnose the problem, /// albeit presented in a significantly less friendly way. fn render_fallback_span_offset( to: &mut impl Write, span: Span, ) -> fmt::Result { writeln!( to, " offset {}--{}", span.offset(), span.offset() + span.len() as SpanOffsetSize ) } fn render_label( to: &mut impl Write, level: Level, label: Label, ) -> fmt::Result { writeln!(to, " {level}: {label}") } } impl Reporter for VisualReporter { // _TODO: This is a proof-of-concept._ fn render( &mut self, diagnostic: &impl Diagnostic, to: &mut impl Write, ) -> fmt::Result { // TODO: not only errors; get the max level from the annotated spans writeln!(to, "error: {}", diagnostic)?; let mut prev_span = UNKNOWN_SPAN; for AnnotatedSpan(span, level, olabel) in diagnostic.describe() { if span != prev_span { write!(to, " --> {}", span.ctx(),)?; match self.resolver.resolve(span) { // We should never mask an error with our own; // the diagnostic system is supposed to _help_ the // user in diagnosing problems, // not hinder them by masking it. Err(e) => { Self::render_fallback_span_offset(to, span)?; // Let the user know that something bad happened, // even though this probably won't make any sense. Self::render_label( to, Level::Help, format!( "there was an error trying to look up \ information about this span: {e}" ) .into(), )?; } Ok(rspan) => match rspan.line() { Some(line) => { writeln!(to, ":{}", line)?; } None => Self::render_fallback_span_offset(to, span)?, }, } } if let Some(label) = olabel { Self::render_label(to, level, label)?; } prev_span = span; } Ok(()) } } #[cfg(test)] mod test { //! Integration tests for the diagnostic system. //! //! These tests have the potential to be rather rigid, //! but the proper rendering of the diagnostic system is very //! important, //! given that the slightest misrendering could cause significant //! confusion. //! //! While it may be tempting to use format strings to reduce the //! maintenance burden, //! this should be avoided most cases, //! since writing out the entire expected value will allow the //! developer to visualize what output will be produced and //! whether it is appropriate for the user. //! //! In essence: //! this diagnostic report is effectively another compiler target, //! and it must be byte-for-byte identical to what is expected. //! With that said, //! _do not interpret these tests as providing a stable, //! unchanging diagnostic report output_. //! This can and will change over time, //! and separate reporters will be provided for machine-readable //! formats if that is what is needed. use crate::{ diagnose::{Annotate, BufSpanResolver}, span::Context, }; use super::*; use std::{ collections::HashMap, error::Error, fmt::Display, io::{self, Cursor}, }; #[derive(Debug)] struct StubError(String, Vec); impl Display for StubError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl Error for StubError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } impl Diagnostic for StubError { fn describe(&self) -> Vec { self.1.clone() } } const FILE_FOO_BAR: &[u8] = b"foo/bar line 1\nfoo/bar line 2\nfoo/bar line 3\nfoo/bar line 4"; // |------------| |------------| |------------| |------------| // 0 13 15 28 30 43 45 58 // len: 14 const FILE_BAR_BAZ: &[u8] = b"bar/baz line 1\nbar/baz line2\nbar/baz line3\nbar/baz line4"; // Offsets for this are the same as `FILE_FOO_BAR`. macro_rules! assert_report { ($msg:expr, $aspans:expr, $expected:expr) => { let mut resolver = HashMap::>::new(); let ctx_foo_bar = Context::from("foo/bar"); let ctx_bar_baz = Context::from("bar/baz"); 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), ); let mut sut = VisualReporter::new(resolver); assert_eq!( sut.render_to_string(&StubError($msg.into(), $aspans)), Ok($expected.into()), ); }; } #[test] fn no_spans() { assert_report!( "test with no spans", vec![], // No spans will result in the `Display` of the error only. "error: test with no spans\n" ); } #[test] fn span_error_no_label() { let span = Context::from("foo/bar").span(50, 5); assert_report!( "single span no label", vec![span.mark_error()], // Context and span are rendered without a label. "\ error: single span no label --> foo/bar:4 " ); } #[test] fn span_error_with_label() { let span = Context::from("bar/baz").span(30, 2); assert_report!( "single span with label", vec![span.error("span label here")], // Context and span are rendered without a label. "\ error: single span with label --> bar/baz:3 error: span label here " ); } #[test] fn adjacent_eq_span_no_labels_collapsed() { let ctx = Context::from("foo/bar"); let span = ctx.span(50, 1); assert_report!( "multiple adjacent same span no label", vec![span.mark_error(), span.mark_error()], // Collapsed into one `-->` line since the spans are the same. // This is unlikely to happen, // given that there is not much use in having multiple // duplicate spans without some additional context. "\ error: multiple adjacent same span no label --> foo/bar:4 " ); } #[test] fn adjacent_eq_span_labels_collapsed() { let ctx = Context::from("bar/baz"); let span = ctx.span(10, 5); assert_report!( "multiple adjacent same span with labels", vec![ span.error("A label"), span.mark_error(), // no label span.error("C label"), ], // Labels are collapsed under the same `-->` line since the // spans are the same. "\ error: multiple adjacent same span with labels --> bar/baz:1 error: A label error: C label " ); } #[test] fn adjacent_eq_context_neq_offset_len_spans_not_collapsed() { let ctx = Context::from("bar/baz"); assert_report!( "eq context neq offset/len", vec![ // --> ctx.span(10, 5).mark_error(), ctx.span(10, 5).error("A, first label"), // collapse // --> ctx.span(10, 6).error("B, different length"), ctx.span(10, 6).mark_error(), // collapse ctx.span(10, 6).error("B, collapse"), // --> ctx.span(15, 6).error("C, different offset"), // --> // Back to (10, 6), but not adjacent to previous ctx.span(10, 6).error("B', not adjacent"), ], "\ error: eq context neq offset/len --> bar/baz:1 error: A, first label --> bar/baz:1 error: B, different length error: B, collapse --> bar/baz:2 error: C, different offset --> bar/baz:1 error: B', not adjacent " ); } #[test] fn adjacent_neq_context_spans_not_collapsed() { // Note that the offsets and lengths are purposefully the same to // ensure that the differentiator is exclusively the context. let span_a = Context::from("foo/bar").span(10, 3); let span_b = Context::from("bar/baz").span(10, 3); assert_report!( "multiple adjacent different context", vec![ // --> span_a.mark_error(), span_a.error("A, first"), span_a.error("A, collapsed"), span_a.mark_error(), // collapsed, same // --> span_b.error("B, first"), span_b.error("B, collapsed"), span_b.mark_error(), // collapsed, same // --> // Back to 'a' again, but we can't collapse now since we're // adjacent to 'b' span_a.error("A, not collapsed"), // Back to 'b' again, but we can't collapse now since we're // adjacent to 'a' // (same as prev but without label this time) span_b.mark_error(), // And just so we have two adjacent label-less spans span_a.mark_error(), ], "\ error: multiple adjacent different context --> foo/bar:1 error: A, first error: A, collapsed --> bar/baz:1 error: B, first error: B, collapsed --> foo/bar:1 error: A, not collapsed --> bar/baz:1 --> foo/bar:1 " ); } #[test] fn severity_levels_reflected() { let ctx = Context::from("foo/bar"); let span = ctx.span(50, 5); assert_report!( "multiple spans with labels of different severity level", vec![ span.internal_error("an internal error"), span.error("an error"), span.note("a note"), span.help("a help message"), ], "\ error: multiple spans with labels of different severity level --> foo/bar:4 internal error: an internal error error: an error note: a note help: a help message " ); } // If a span fails to resolve // (maybe the file cannot be read for some reason, // or maybe there's some bug in TAMER such that the context is // incorrect and cannot be resolved), // we should still provide what information we _can_ rather than // masking the original error with an error of our own. // The diagnostic system is supposed to aid the user in resolving // issues, // not make them _more_ difficult to resolve. #[test] fn fallback_when_span_fails_to_resolve() { let ctx = Context::from("unknown/context"); // Doesn't matter what this is. let span = ctx.span(50, 5); // This could change between Rust versions or OSes. let ioerr = io::ErrorKind::NotFound; // It's not ideal that the help appears first, // but this should only happen under very exceptional // circumstances so it's not worth trying to resolve. // If you're reading this and it's trivial to swap these with the // current state of the system, // go for it. assert_report!( "unresolvable context fallback", vec![span.error("an error we do not want to suppress"),], format!("\ error: unresolvable context fallback --> unknown/context offset 50--55 help: there was an error trying to look up information about this span: {ioerr} error: an error we do not want to suppress ") ); } }