diff --git a/tamer/src/diagnose.rs b/tamer/src/diagnose.rs index 90b0d56b..4c078db0 100644 --- a/tamer/src/diagnose.rs +++ b/tamer/src/diagnose.rs @@ -22,6 +22,58 @@ //! This system is heavily motivated by Rust's. //! While the data structures and organization may differ, //! the diagnostic output is visually similar. +//! +//! Visual Report +//! ------------- +//! The primary output of the system is a [`Report`](report::Report). +//! A report consists not only of the diagnostic information provided by the +//! system +//! (such as error messages), +//! but also annotated source code. +//! Reports attempt to create a narrative that walks the user through why +//! and how problems arose, +//! and hopefully provides information on how to resolve the problem. +//! +//! Here is an example report: +//! +//! ```text +//! error: expected closing tag for `classify` +//! --> /home/user/program/foo.xml:16:5 +//! | +//! 16 | +//! | --------- note: element `classify` is opened here +//! +//! --> /home/user/program/foo.xml:24:5 +//! | +//! 24 | +//! | ^^^^^^^^ error: expected `` +//! ``` +//! +//! A single report is produced for each error +//! (or other suitable diagnostic event). +//! The system may produce multiple reports at a time if it discovers +//! multiple issues and is able to recover enough to continue to discover +//! others. +//! +//! Each report is separated into a number of sections, +//! with each section delimited by a header. +//! Each section describes one or more spans related to a range of related +//! source lines. +//! Those source lines are annotated using the [`Span`]s associated with the +//! emitted diagnostic data, +//! such as an error. +//! +//! Each section has a _gutter_ containing the line number of source lines. +//! The area below the section header and to the right of the gutter is +//! called the _body_ of the section. +//! The gutter will expand as needed to fit the line number. +//! +//! _Warning: Reports do not yet strip terminal escape sequences, +//! which may interfere with the diagnostic output. +//! This may represent a security risk depending on your threat model, +//! but does require access to the source code being compiled._ +//! +//! See the [`report`] module for more information. mod report; mod resolver; diff --git a/tamer/src/diagnose/report.rs b/tamer/src/diagnose/report.rs index b479f9ec..183e7918 100644 --- a/tamer/src/diagnose/report.rs +++ b/tamer/src/diagnose/report.rs @@ -17,7 +17,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Rendering of diagnostic information. +//! Rendering of diagnostic report. +//! +//! This module is responsible for [resolving](super::resolver) and +//! rendering spans into a formatted [`Report`], +//! which in turn can be rendered into a string with [`Display::fmt`]. +//! +//! See the [parent module](super) for more summary information. // NB: `write!` together with `\n` is preferred to `writeln!` so that there // is only a single sequence of characters to search for while tracking @@ -38,6 +44,10 @@ use std::{ ops::Add, }; +/// Render a [`Report`] with detailed diagnostic information. +/// +/// Rendering of reports is layered---this +/// report can be further rendered into a string using [`Display::fmt`]. pub trait Reporter { /// Render diagnostic report. /// @@ -199,15 +209,49 @@ impl<'d, S: ResolvedSpanData> MaybeResolvedSpan<'d, S> { } } +/// Diagnostic report. +/// +/// This contains the raw data that, +/// when rendered with [`Display::fmt`], +/// will produce a robust report to help guide the user through diagnosing +/// and resolving problems. #[derive(Debug)] pub struct Report<'d, D: Diagnostic> { + /// Summary message of the contents of the report. + /// + /// This message should be suitable on its own, + /// e.g. a typical one-line error message. msg: Message<'d, D>, - secs: Vec>, + + /// The largest severity level found within all of the [`Section`]s of + /// the report. + /// + /// This level should be used alongside the summary message to describe + /// how severe of a problem this report represents. level: Level, + + /// Sections of the report. + /// + /// A section contains a header describing a location in the source + /// code (line and column numbers), + /// followed by annotated source code and descriptive labels. + secs: Vec>, + + /// The maximum line number encountered in each of the [`Section`]s of + /// the report. + /// + /// This number is used to determine the gutter width, + /// which contains the line numbers of the annotated source lines. + /// It can be propagated to all [`Section`]s using + /// [`normalize_gutters`](Report::normalize_gutters). line_max: NonZeroU32, } impl<'d, D: Diagnostic> Report<'d, D> { + /// Create an empty report. + /// + /// To add to the body of the report, + /// use [`Extend::extend`]. fn empty(msg: Message<'d, D>) -> Self { Self { msg, @@ -229,6 +273,12 @@ impl<'d, D: Diagnostic> Report<'d, D> { } impl<'d, D: Diagnostic> Extend> for Report<'d, D> { + /// Extend the body of the report. + /// + /// This tracks the most severe [`Level`] and highest line number seen. + /// Further, + /// adjacent sections may be squashed if they meet certain criteria + /// (see [`Section::maybe_squash_into`]). fn extend>>(&mut self, secs: T) { for sec in secs { self.level = self.level.min(sec.level()); @@ -258,6 +308,7 @@ impl<'d, D: Diagnostic> Display for Report<'d, D> { } } +/// Summary diagnostic message. #[derive(Debug)] struct Message<'d, D: Diagnostic>(&'d D); @@ -281,10 +332,27 @@ impl<'d, D: Diagnostic> Display for Message<'d, D> { /// messages quickly. #[derive(Debug, PartialEq, Eq)] struct Section<'d> { + /// Heading that delimits the beginning of each section. + /// + /// The heading describes the location of its principal [`Span`]. heading: SpanHeading, + + /// The most severe [`Level`] encountered in this section body. level: Level, + + /// The principal [`Span`] that this section describes. + /// + /// If a section contains information about multiple spans, + /// this represents the one that the user should focus on. span: Span, + + /// Annotated source lines and labels. body: Vec>, + + /// The largest line number encountered in this section. + /// + /// This is used to determine how wide to render the gutter, + /// which contain the line numbers for source lines. line_max: NonZeroU32, } @@ -598,9 +666,25 @@ impl Display for HeadingColNum { /// Line of output in a [`Section`] body. #[derive(Debug, PartialEq, Eq)] enum SectionLine<'d> { + /// Padding for a possibly annotated source line. + /// + /// A padding line is intended to add extra space above or below a + /// source line to make it easier to read. + /// Padding lines contain a gutter, + /// but no line number. SourceLinePadding, + + /// A line of source code. SourceLine(SectionSourceLine), + + /// Source line annotations + /// (marks and labels). SourceLineMark(LineMark<'d>), + + /// A label that is not rendered as a line annotation. + /// + /// Footnotes are intended too appear at the end of a [`Section`] and + /// contain supplemental information. Footnote(Level, Label<'d>), } @@ -637,6 +721,10 @@ impl<'d> SectionLine<'d> { } } + /// Attempt to convert a line into a footnote. + /// + /// If there is no [`Level`] and [`Label`] available, + /// [`None`] is returned. fn into_footnote(self) -> Option { match self { Self::SourceLinePadding => None, @@ -661,6 +749,7 @@ impl<'d> Display for SectionLine<'d> { } } +/// A [`SourceLine`] displayed within a [`Section`]. #[derive(Debug, PartialEq, Eq)] struct SectionSourceLine(SourceLine); @@ -684,6 +773,12 @@ impl Display for SectionSourceLine { /// A type of line annotation that marks columns and provides labels, /// if available. +/// +/// Marks are displayed below a [`SectionSourceLine`] and are intended to +/// visually display a [`Span`]. +/// Column resolution +/// (see [`super::resolver`]) +/// exists primarily for mark rendering. #[derive(Debug, PartialEq, Eq)] struct LineMark<'d> { level: Level, @@ -715,13 +810,14 @@ impl<'d> Display for LineMark<'d> { } } +/// Mark styling. trait MarkChar { + /// Character used to underline the columns applicable to a given span + /// underneath a source line. fn mark_char(&self) -> char; } impl MarkChar for Level { - /// Character used to underline the columns applicable to a given span - /// underneath a source line. fn mark_char(&self) -> char { match self { Level::InternalError => '!',