diff --git a/tamer/src/diagnose/report.rs b/tamer/src/diagnose/report.rs index 1f4f47c6..22936b72 100644 --- a/tamer/src/diagnose/report.rs +++ b/tamer/src/diagnose/report.rs @@ -20,9 +20,7 @@ //! Rendering of diagnostic information. use super::{ - resolver::{ - ResolvedSpan, ResolvedSpanData, SpanResolver, SpanResolverError, - }, + resolver::{ResolvedSpanData, SpanResolver, SpanResolverError}, AnnotatedSpan, Diagnostic, Label, Level, }; use crate::span::{Context, Span, UNKNOWN_SPAN}; @@ -95,7 +93,7 @@ impl Reporter for VisualReporter { self.resolver.resolve(span).map_err(|e| (e, span)), ); - write!(to, " {}", mspan.header())?; + write!(to, " {}", DefaultSpanHeader::from(&mspan))?; for label in mspan.system_labels() { write!(to, "{label}\n")?; @@ -113,7 +111,7 @@ impl Reporter for VisualReporter { } } -/// A [`Span`] that may have been resolved into a [`ResolvedSpan`]. +/// A [`Span`] that may have been resolved. /// /// The span will remain unresolved if an error occurred, /// in which case the error will be provided. @@ -126,25 +124,12 @@ impl Reporter for VisualReporter { /// (e.g. error) /// never be masked by an error of our own. #[derive(Debug)] -enum MaybeResolvedSpan { - Resolved(ResolvedSpan), +enum MaybeResolvedSpan { + Resolved(S), Unresolved(Span, SpanResolverError), } -impl MaybeResolvedSpan { - /// Span header containing the (hopefully resolved) context. - fn header(&self) -> SpanHeader { - match self { - Self::Resolved(rspan) => { - SpanHeader(rspan.context(), HeaderLineNum::Resolved(&rspan)) - } - - Self::Unresolved(span, _) => { - SpanHeader(span.context(), HeaderLineNum::Unresolved(*span)) - } - } - } - +impl MaybeResolvedSpan { /// We should never mask an error with our own; /// the diagnostic system is supposed to _help_ the user in diagnosing /// problems, @@ -182,10 +167,10 @@ impl MaybeResolvedSpan { } } -impl From> - for MaybeResolvedSpan +impl From> + for MaybeResolvedSpan { - fn from(result: Result) -> Self { + fn from(result: Result) -> Self { match result { Ok(rspan) => Self::Resolved(rspan), Err((e, span)) => Self::Unresolved(span, e), @@ -193,6 +178,8 @@ impl From> } } +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 @@ -200,15 +187,34 @@ impl From> /// visually distinguishable from surrounding lines to allow the user to /// quickly skip between reports. #[derive(Debug)] -struct SpanHeader<'s>(Context, HeaderLineNum<'s>); +struct SpanHeader(Context, L); -impl<'s> Display for SpanHeader<'s> { +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, @@ -217,12 +223,12 @@ impl<'s> Display for SpanHeader<'s> { /// If a span could not be resolved, /// offsets should be rendered in place of lines and columns. #[derive(Debug)] -enum HeaderLineNum<'s> { +enum HeaderLineNum<'s, S: ResolvedSpanData> { Unresolved(Span), - Resolved(&'s ResolvedSpan), + Resolved(&'s S), } -impl<'s> Display for HeaderLineNum<'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, @@ -241,25 +247,35 @@ impl<'s> Display for HeaderLineNum<'s> { } Self::Resolved(rspan) => { - let col = HeaderColNum(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 ResolvedSpan); +struct HeaderColNum<'s, S: ResolvedSpanData>(&'s S); -impl<'s> Display for HeaderColNum<'s> { +impl<'s, S: ResolvedSpanData> Display for HeaderColNum<'s, S> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self(rspan) = self; - let span = rspan.unresolved_span(); match rspan.col_num() { Some(col) => write!(f, ":{}", col), @@ -268,7 +284,8 @@ impl<'s> Display for HeaderColNum<'s> { // 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 = span + let rel = rspan + .unresolved_span() .relative_to(rspan.first_line_span()) .unwrap_or(UNKNOWN_SPAN); @@ -297,6 +314,163 @@ impl<'l> Display for SpanLabel<'l> { #[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", + ); + } }