// 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::{ resolver::{ResolvedSpanData, SpanResolver, SpanResolverError}, AnnotatedSpan, Diagnostic, Label, Level, }; use crate::span::{Context, Span, UNKNOWN_SPAN}; use std::fmt::{self, Display, 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 } } } 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 { let mspan = MaybeResolvedSpan::from( self.resolver.resolve(span).map_err(|e| (e, span)), ); write!(to, " {}", DefaultSpanHeader::from(&mspan))?; for label in mspan.system_labels() { write!(to, "{label}\n")?; } } if let Some(label) = olabel { writeln!(to, "{}", SpanLabel(level, label))?; } prev_span = span; } Ok(()) } } /// A [`Span`] that may have been resolved. /// /// The span will remain unresolved if an error occurred, /// in which case the error will be provided. /// The idea is to provide as much fallback information as is useful to the /// user so that they can still debug the problem without the benefit of /// the resolved context. /// /// Furthermore, /// it is important that the underlying diagnostic message /// (e.g. error) /// never be masked by an error of our own. #[derive(Debug)] enum MaybeResolvedSpan { Resolved(S), Unresolved(Span, SpanResolverError), } impl MaybeResolvedSpan { /// 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. fn system_labels(&self) -> Vec> { match self { Self::Resolved(rspan) if rspan.col_num().is_none() => vec![ SpanLabel( Level::Help, "unable to calculate columns because the line is \ not a valid UTF-8 string" .into(), ), SpanLabel( Level::Help, "you have been provided with 0-indexed \ line-relative inclusive byte offsets" .into(), ), ], Self::Unresolved(_, e) => { vec![SpanLabel( Level::Help, format!( "there was an error trying to look up \ information about this span: {e}" ) .into(), )] } _ => vec![], } } } impl From> for MaybeResolvedSpan { fn from(result: Result) -> Self { match result { Ok(rspan) => Self::Resolved(rspan), Err((e, span)) => Self::Unresolved(span, e), } } } type DefaultSpanHeader<'s, S> = SpanHeader>; /// Header describing the context of a (hopefully resolved) span. /// /// The ideal header contains the context along with the line, and column /// numbers, /// visually distinguishable from surrounding lines to allow the user to /// quickly skip between reports. #[derive(Debug)] struct SpanHeader(Context, L); impl Display for SpanHeader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self(ctx, line) = self; write!(f, "--> {ctx}{line}\n") } } impl<'s, S, L> From<&'s MaybeResolvedSpan> for SpanHeader where S: ResolvedSpanData, L: Display + From<&'s MaybeResolvedSpan>, { /// Span header containing the (hopefully resolved) context. fn from(mspan: &'s MaybeResolvedSpan) -> Self { match mspan { MaybeResolvedSpan::Resolved(rspan) => { SpanHeader(rspan.context(), L::from(mspan)) } MaybeResolvedSpan::Unresolved(span, _) => { SpanHeader(span.context(), L::from(mspan)) } } } } /// Span line number or fallback representation. /// /// This is also responsible for attempting to produce a column number, /// provided that a line number is available. /// /// If a span could not be resolved, /// offsets should be rendered in place of lines and columns. #[derive(Debug)] enum HeaderLineNum<'s, S: ResolvedSpanData> { Unresolved(Span), Resolved(&'s S), } impl<'s, S: ResolvedSpanData> Display for HeaderLineNum<'s, S> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { // 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. Self::Unresolved(span) => { write!( f, " offset {}--{}", span.offset(), span.endpoints_saturated().1.offset(), ) } Self::Resolved(rspan) => { let col = HeaderColNum(*rspan); write!(f, ":{}{col}", rspan.line_num()) } } } } impl<'s, S: ResolvedSpanData> From<&'s MaybeResolvedSpan> for HeaderLineNum<'s, S> { fn from(mspan: &'s MaybeResolvedSpan) -> Self { match mspan { MaybeResolvedSpan::Resolved(rspan) => Self::Resolved(rspan), MaybeResolvedSpan::Unresolved(span, _) => Self::Unresolved(*span), } } } /// Column number or fallback representation. /// /// If a column could not be resolved, /// it should fall back to displaying byte offsets relative to the start /// of the line. #[derive(Debug)] struct HeaderColNum<'s, S: ResolvedSpanData>(&'s S); impl<'s, S: ResolvedSpanData> Display for HeaderColNum<'s, S> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self(rspan) = self; match rspan.col_num() { Some(col) => write!(f, ":{}", col), // The column is unavailable, // which means that the line must have contained invalid UTF-8. // Output what we can in an attempt to help the user debug. None => { let rel = rspan .unresolved_span() .relative_to(rspan.first_line_span()) .unwrap_or(UNKNOWN_SPAN); write!( f, " bytes {}--{}", rel.offset(), rel.endpoints_saturated().1.offset() ) } } } } /// A label describing a span. #[derive(Debug)] struct SpanLabel<'l>(Level, Label<'l>); impl<'l> Display for SpanLabel<'l> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self(level, label) = self; write!(f, " {level}: {label}") } } #[cfg(test)] mod test { use super::*; use crate::{ convert::ExpectInto, diagnose::resolver::Column, span::DUMMY_CONTEXT, }; use std::num::NonZeroU32; mod integration; #[derive(Default)] struct StubResolvedSpan { span: Option, first_line_span: Option, line_num: Option, col_num: Option, context: Option, } impl ResolvedSpanData for StubResolvedSpan { fn line_num(&self) -> NonZeroU32 { self.line_num.expect("missing stub line_num") } fn col_num(&self) -> Option { self.col_num } fn first_line_span(&self) -> Span { self.first_line_span.expect("missing stub first_line_span") } fn context(&self) -> Context { self.context.expect("missing stub ctx") } fn unresolved_span(&self) -> Span { self.span.expect("missing stub unresolved span") } } #[test] fn header_col_with_available_col() { let rspan = StubResolvedSpan { col_num: Some(Column::Endpoints(5.unwrap_into(), 5.unwrap_into())), ..Default::default() }; let sut = HeaderColNum(&rspan); assert_eq!(":5", format!("{}", sut)); } #[test] fn header_col_without_available_col() { let rspan = StubResolvedSpan { span: Some(DUMMY_CONTEXT.span(5, 2)), first_line_span: Some(DUMMY_CONTEXT.span(3, 7)), col_num: None, ..Default::default() }; let sut = HeaderColNum(&rspan); assert_eq!(" bytes 2--4", format!("{}", sut)); } // Note that line is coupled with `HeaderColNum`, // tested above. // The coupling is not ideal, // but it keeps it simple and we don't concretely benefit from the // decoupling for now. #[test] fn line_with_resolved_span() { let rspan = StubResolvedSpan { line_num: Some(5.unwrap_into()), col_num: Some(Column::Endpoints(3.unwrap_into(), 3.unwrap_into())), ..Default::default() }; let sut = HeaderLineNum::Resolved(&rspan); assert_eq!(":5:3", format!("{}", sut)); } // Does _not_ use `HeaderColNum`, // unlike the above, // because the line was not resolved. #[test] fn line_with_unresolved_span_without_resolved_col() { let sut = HeaderLineNum::Unresolved::( DUMMY_CONTEXT.span(3, 4), ); assert_eq!(" offset 3--7", format!("{}", sut)); } #[test] fn span_header() { struct StubLine; impl Display for StubLine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[:stub line]") } } let ctx = "header".unwrap_into(); let sut = SpanHeader(ctx, StubLine); assert_eq!("--> header[:stub line]\n", format!("{}", sut)); } #[test] fn span_header_from_mspan() { struct StubLine(String); impl Display for StubLine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[:stub {}]", self.0) } } impl<'s, S: ResolvedSpanData> From<&'s MaybeResolvedSpan> for StubLine { fn from(mspan: &'s MaybeResolvedSpan) -> Self { match mspan { MaybeResolvedSpan::Resolved(_) => Self("resolved".into()), MaybeResolvedSpan::Unresolved(..) => { Self("unresolved".into()) } } } } let ctx = Context::from("mspan/header"); assert_eq!( format!( "{}", SpanHeader::::from(&MaybeResolvedSpan::Resolved( StubResolvedSpan { context: Some(ctx), ..Default::default() }, )) ), "--> mspan/header[:stub resolved]\n", ); assert_eq!( format!( "{}", SpanHeader::::from(&MaybeResolvedSpan::< StubResolvedSpan, >::Unresolved( ctx.span(0, 0), SpanResolverError::OutOfRange(0), )) ), "--> mspan/header[:stub unresolved]\n", ); } }