From 3e06c9aaf37a8c21be3f6a11537d587ec260e8cd Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 28 Apr 2022 13:24:36 -0400 Subject: [PATCH] tamer: diagnose::report: Prepare Section for output of source lines This lowers the resolved span data into `Section` for display. The next step is to actually output it. DEV-12151 --- tamer/src/diagnose/report.rs | 215 ++++++++++++++---- tamer/src/diagnose/report/test/integration.rs | 18 ++ tamer/src/diagnose/resolver.rs | 30 ++- 3 files changed, 219 insertions(+), 44 deletions(-) diff --git a/tamer/src/diagnose/report.rs b/tamer/src/diagnose/report.rs index 73b69f3b..7492b798 100644 --- a/tamer/src/diagnose/report.rs +++ b/tamer/src/diagnose/report.rs @@ -25,7 +25,9 @@ // rather than using both. use super::{ - resolver::{Column, ResolvedSpanData, SpanResolver, SpanResolverError}, + resolver::{ + Column, ResolvedSpanData, SourceLine, SpanResolver, SpanResolverError, + }, AnnotatedSpan, Diagnostic, Label, Level, }; use crate::span::{Context, Span, UNKNOWN_SPAN}; @@ -130,32 +132,32 @@ impl<'d, S: ResolvedSpanData> MaybeResolvedSpan<'d, S> { /// the diagnostic system is supposed to _help_ the user in diagnosing /// problems, /// not hinder them by masking it. - fn system_labels(&self) -> Vec> { + fn system_lines(&self) -> Vec> { match self { Self::Resolved(rspan, _) if rspan.col_num().is_none() => vec![ - SpanLabel( + SectionLine::Footnote(SpanLabel( Level::Help, "unable to calculate columns because the line is \ not a valid UTF-8 string" .into(), - ), - SpanLabel( + )), + SectionLine::Footnote(SpanLabel( Level::Help, "you have been provided with 0-indexed \ line-relative inclusive byte offsets" .into(), - ), + )), ], Self::Unresolved(_, _, e) => { - vec![SpanLabel( + vec![SectionLine::Footnote(SpanLabel( Level::Help, format!( "an error occurred while trying to look up \ information about this span: {e}" ) .into(), - )] + ))] } _ => vec![], @@ -223,9 +225,9 @@ impl<'d, D: Diagnostic> Display for Message<'d, D> { #[derive(Debug, PartialEq, Eq)] struct Section<'d> { heading: SpanHeading, - labels: Vec>, level: Level, span: Span, + body: Vec>, } impl<'s, 'd> Section<'d> { @@ -253,7 +255,7 @@ impl<'s, 'd> Section<'d> { // TODO: At the time of writing this will cause duplication of // system labels, // which is not desirable. - extend_sec.labels.extend(self.labels); + extend_sec.body.extend(self.body); None } @@ -268,24 +270,53 @@ where { fn from(mspan: MaybeResolvedSpan<'d, S>) -> Self { let heading = SpanHeading::from(&mspan); - let mut labels = mspan.system_labels(); + let mut body = mspan.system_lines(); - let (span, olabel) = match mspan { - MaybeResolvedSpan::Resolved(rspan, olabel) => { - (rspan.unresolved_span(), olabel) + let (span, level) = match mspan { + MaybeResolvedSpan::Resolved(rspan, oslabel) => { + let span = rspan.unresolved_span(); + let src = rspan.into_lines(); + + let (level, mut olabel) = match oslabel { + Some(SpanLabel(level, label)) => (level, Some(label)), + None => (Default::default(), None), + }; + + let nlines = src.len(); + + body.extend(src.into_iter().enumerate().map(|(i, srcline)| { + let col = srcline.column(); + + SectionLine::SourceLine(SectionSourceLine { + src: srcline, + mark: LineMark { + col, + level, + label: if i == nlines - 1 { + olabel.take() + } else { + None + }, + }, + }) + })); + + (span, level) + } + MaybeResolvedSpan::Unresolved(span, olabel, _) => { + let level = + olabel.as_ref().map(SpanLabel::level).unwrap_or_default(); + + body.extend(olabel.map(SectionLine::Footnote)); + (span, level) } - MaybeResolvedSpan::Unresolved(span, olabel, _) => (span, olabel), }; - let level = olabel.as_ref().map(SpanLabel::level).unwrap_or_default(); - - labels.extend(olabel); - Section { heading, - labels, span, level, + body, } } } @@ -294,8 +325,10 @@ impl<'d> Display for Section<'d> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, " {heading}\n", heading = self.heading)?; - for label in self.labels.iter() { - write!(f, "{label}\n")?; + for line in self.body.iter() { + // Let each line have control over its own newline so that it + // can fully suppress itself if it's not relevant. + line.fmt(f)?; } Ok(()) @@ -442,6 +475,59 @@ impl<'d> Display for SpanLabel<'d> { } } +/// A possibly-annotated line of output. +/// +/// Note that a section line doesn't necessarily correspond to a single line +/// of output on a terminal; +/// lines are likely to be annotated. +#[derive(Debug, PartialEq, Eq)] +enum SectionLine<'d> { + SourceLine(SectionSourceLine<'d>), + Footnote(SpanLabel<'d>), +} + +impl<'d> Display for SectionLine<'d> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::SourceLine(line) => line.fmt(f), + Self::Footnote(label) => write!(f, "{label}\n"), + } + } +} + +/// A line representing possibly-annotated source code. +#[derive(Debug, PartialEq, Eq)] +struct SectionSourceLine<'d> { + src: SourceLine, + mark: LineMark<'d>, +} + +impl<'d> Display for SectionSourceLine<'d> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO + write!(f, "{}", self.mark) + } +} + +/// A type of line annotation that marks columns and provides labels, +/// if available. +#[derive(Debug, PartialEq, Eq)] +struct LineMark<'d> { + level: Level, + col: Option, + label: Option>, +} + +impl<'d> Display for LineMark<'d> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(label) = self.label.as_ref() { + write!(f, " {level}: {label}\n", level = self.level)?; + } + + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; @@ -461,6 +547,7 @@ mod test { line_num: Option, col_num: Option, context: Option, + src_lines: Option>, } impl ResolvedSpanData for StubResolvedSpan { @@ -483,6 +570,10 @@ mod test { fn unresolved_span(&self) -> Span { self.span.expect("missing stub unresolved span") } + + fn into_lines(self) -> Vec { + self.src_lines.unwrap_or_default() + } } #[test] @@ -562,17 +653,33 @@ mod test { let ctx = Context::from("mspan/sec"); let span = ctx.span(2, 3); + let col_1 = Column::Endpoints(2.unwrap_into(), 3.unwrap_into()); + let col_2 = Column::Endpoints(1.unwrap_into(), 4.unwrap_into()); + + let src_lines = vec![ + SourceLine::new_stub( + 1.unwrap_into(), + Some(col_1), + span, + "line 1".into(), + ), + SourceLine::new_stub( + 2.unwrap_into(), + Some(col_2), + span, + "line 2".into(), + ), + ]; + assert_eq!( Section::from(MaybeResolvedSpan::Resolved( StubResolvedSpan { context: Some(ctx), line_num: Some(1.unwrap_into()), - col_num: Some(Column::Endpoints( - 2.unwrap_into(), - 3.unwrap_into() - )), + col_num: Some(col_1), first_line_span: Some(DUMMY_SPAN), span: Some(span), + src_lines: Some(src_lines.clone()), }, Some(SpanLabel(Level::Note, "test label".into())), )), @@ -587,10 +694,29 @@ mod test { )) ) ), - labels: vec![SpanLabel(Level::Note, "test label".into())], span, // Derived from label. level: Level::Note, + body: vec![ + SectionLine::SourceLine(SectionSourceLine { + src: src_lines[0].clone(), + mark: LineMark { + level: Level::Note, + col: Some(col_1), + // Label goes on the last source line. + label: None, + } + }), + SectionLine::SourceLine(SectionSourceLine { + src: src_lines[1].clone(), + mark: LineMark { + level: Level::Note, + col: Some(col_2), + // Label at last source line + label: Some("test label".into()), + } + }), + ], } ); } @@ -611,6 +737,7 @@ mod test { )), first_line_span: Some(DUMMY_SPAN), span: Some(span), + src_lines: None, }, None, )), @@ -625,11 +752,11 @@ mod test { )) ) ), - labels: vec![], span, // Level is normally derived from the label, // so in this case it gets defaulted. level: Level::default(), + body: vec![], } ); } @@ -645,27 +772,29 @@ mod test { SpanResolverError::Io(io::ErrorKind::NotFound), ); - let syslabels = mspan.system_labels(); - assert_eq!( Section::from(mspan), Section { heading: SpanHeading(ctx, HeadingLineNum::Unresolved(span),), - labels: vec![ - SpanLabel( - Level::Help, - // Clone inner so that we don't need to implement - // `Clone` for `SpanLabel`. - syslabels - .first() - .expect("missing system label") - .1 - .clone(), - ), - SpanLabel(Level::Note, "test label".into()), - ], span, level: Level::Note, + body: vec![ + SectionLine::Footnote(SpanLabel( + Level::Help, + // This hard-coding is not ideal, + // as it makes the test fragile. + format!( + "an error occurred while trying to look up \ + information about this span: {}", + io::ErrorKind::NotFound + ) + .into() + )), + SectionLine::Footnote(SpanLabel( + Level::Note, + "test label".into() + )), + ], } ); } diff --git a/tamer/src/diagnose/report/test/integration.rs b/tamer/src/diagnose/report/test/integration.rs index d717a08d..0e559249 100644 --- a/tamer/src/diagnose/report/test/integration.rs +++ b/tamer/src/diagnose/report/test/integration.rs @@ -316,6 +316,24 @@ internal error: multiple spans with labels of different severity level ); } +#[test] +fn multi_line_span() { + let ctx = Context::from("foo/bar"); + + // First two lines. + let span = ctx.span(0, 29); + + assert_report!( + "multi-line span", + vec![span.error("label to be on last line")], + "\ +error: multi-line span + --> foo/bar:1:1 + error: label to be on last line +" + ); +} + // 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 diff --git a/tamer/src/diagnose/resolver.rs b/tamer/src/diagnose/resolver.rs index 3fc38727..3c8feb52 100644 --- a/tamer/src/diagnose/resolver.rs +++ b/tamer/src/diagnose/resolver.rs @@ -164,6 +164,9 @@ pub trait ResolvedSpanData { /// The original [`Span`] before resolution. fn unresolved_span(&self) -> Span; + + /// Consume self and yield owned inner [`SourceLine`]s. + fn into_lines(self) -> Vec; } impl ResolvedSpanData for ResolvedSpan { @@ -186,6 +189,10 @@ impl ResolvedSpanData for ResolvedSpan { fn unresolved_span(&self) -> Span { self.span } + + fn into_lines(self) -> Vec { + self.lines.0 + } } /// Source column offsets. @@ -220,7 +227,7 @@ impl Display for Column { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct SourceLine { /// 1-indexed line number relative to the entire source [`Context`]. num: NonZeroU32, @@ -238,6 +245,27 @@ pub struct SourceLine { text: Vec, } +impl SourceLine { + pub fn column(&self) -> Option { + self.column + } + + #[cfg(test)] + pub fn new_stub( + num: NonZeroU32, + column: Option, + span: Span, + text: Vec, + ) -> Self { + Self { + num, + column, + span, + text, + } + } +} + /// Resolve a [`Span`] using any generic [`BufRead`]. pub struct BufSpanResolver { reader: R,