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
main
Mike Gerwitz 2022-04-28 13:24:36 -04:00
parent 331aada2bd
commit 3e06c9aaf3
3 changed files with 219 additions and 44 deletions

View File

@ -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<SpanLabel<'static>> {
fn system_lines(&self) -> Vec<SectionLine<'static>> {
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<SpanLabel<'d>>,
level: Level,
span: Span,
body: Vec<SectionLine<'d>>,
}
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<Column>,
label: Option<Label<'d>>,
}
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<NonZeroU32>,
col_num: Option<Column>,
context: Option<Context>,
src_lines: Option<Vec<SourceLine>>,
}
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<SourceLine> {
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()
)),
],
}
);
}

View File

@ -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

View File

@ -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<SourceLine>;
}
impl ResolvedSpanData for ResolvedSpan {
@ -186,6 +189,10 @@ impl ResolvedSpanData for ResolvedSpan {
fn unresolved_span(&self) -> Span {
self.span
}
fn into_lines(self) -> Vec<SourceLine> {
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<u8>,
}
impl SourceLine {
pub fn column(&self) -> Option<Column> {
self.column
}
#[cfg(test)]
pub fn new_stub(
num: NonZeroU32,
column: Option<Column>,
span: Span,
text: Vec<u8>,
) -> Self {
Self {
num,
column,
span,
text,
}
}
}
/// Resolve a [`Span`] using any generic [`BufRead`].
pub struct BufSpanResolver<R: BufRead + Seek> {
reader: R,