tame/tamer/src/diagnose.rs

327 lines
11 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

// Diagnostic system
//
// Copyright (C) 2014-2023 Ryan Specialty, 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 <http://www.gnu.org/licenses/>.
//! Diagnostic system for error reporting.
//!
//! 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 | <classify as="foo" desc="Example classification">
//! | --------- note: element `classify` is opened here
//!
//! --> /home/user/program/foo.xml:24:5
//! |
//! 24 | </wrong>
//! | ^^^^^^^^ error: expected `</classify>`
//! ```
//!
//! 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.
#[macro_use]
pub mod panic;
mod report;
mod resolve;
pub use report::{Reporter, VisualReporter};
pub use resolve::FsSpanResolver;
use core::fmt;
use std::{borrow::Cow, convert::Infallible, error::Error, fmt::Display};
use crate::span::Span;
/// No annotated description is applicable for the diagnostic message.
///
/// The system should strive to give the user as much relevant information
/// as is useful to resolve the problem.
/// Whether or not the absence of this description represents a deficiency
/// depends on the error context.
pub const NO_DESC: Vec<AnnotatedSpan> = vec![];
/// Diagnostic report.
///
/// This describes an error condition or other special event using a series
/// of [`Span`]s to describe the source, cause, and circumstances around
/// an event.
pub trait Diagnostic: Error + Sized {
/// Produce a series of [`AnnotatedSpan`]s describing the source and
/// circumstances of the diagnostic event.
fn describe(&self) -> Vec<AnnotatedSpan>;
}
impl Diagnostic for Infallible {
fn describe(&self) -> Vec<AnnotatedSpan> {
// This should never actually happen unless someone is explicitly
// invoking this method on `Infallible`.
unreachable!("Infallible is not supposed to fail")
}
}
/// Diagnostic severity level.
///
/// Levels are used both for entire reports and for styling of individual
/// [`AnnotatedSpan`]s.
///
/// Higher severity levels are represented by lower integer values
/// (e.g. level 1 is the worst),
/// like DEFCON levels.
/// The rationale here is that,
/// provided that you remember that these are 1-indexed,
/// you do not need to know how many levels exist to know how severe it
/// is.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)]
#[repr(u8)]
pub enum Level {
/// An error internal to TAMER that the user cannot resolve,
/// but may be able to work around.
InternalError = 1,
/// A user-resolvable error.
///
/// These represent errors resulting from the user's input.
Error,
/// Useful information that supplements other messages.
///
/// This is most often used when multiple spans are in play for a given
/// diagnostic report.
Note,
/// Additional advice to the user that may help in debugging or fixing a
/// problem.
///
/// These messages may suggest concrete fixes and are intended to
/// hopefully replace having to request advice from a human.
/// Unlike other severity levels which provide concrete factual
/// information,
/// help messages may be more speculative.
#[default]
Help,
}
impl Level {
/// Whether this error level represents an error.
pub fn is_error(self) -> bool {
self <= Self::Error
}
}
impl Display for Level {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Level::InternalError => write!(f, "internal error"),
Level::Error => write!(f, "error"),
Level::Note => write!(f, "note"),
Level::Help => write!(f, "help"),
}
}
}
/// A label associated with a report or [`Span`].
///
/// See [`AnnotatedSpan`].
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Label<'a>(Cow<'a, str>);
impl<'a> Display for Label<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl<'a> From<String> for Label<'a> {
fn from(s: String) -> Self {
Self(Cow::Owned(s))
}
}
impl<'a> From<&'a String> for Label<'a> {
fn from(s: &'a String) -> Self {
Self(Cow::Borrowed(s))
}
}
impl<'a> From<&'a str> for Label<'a> {
fn from(s: &'a str) -> Self {
Self(Cow::Borrowed(s))
}
}
/// A span with an associated severity level and optional label.
///
/// Annotated spans are intended to guide users through debugging a
/// diagnostic message by describing important source locations that
/// contribute to a given diagnostic event.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AnnotatedSpan<'l>(Span, Level, Option<Label<'l>>);
impl<'l> AnnotatedSpan<'l> {
pub fn with_help<L: Into<Label<'l>>>(
self,
label: L,
) -> [AnnotatedSpan<'l>; 2] {
let span = self.0;
[self, span.help(label)]
}
/// The [`Span`] with which the annotation is associated.
pub fn span(&self) -> Span {
match self {
AnnotatedSpan(span, ..) => *span,
}
}
/// A reference to the label of the annotation,
/// if available.
pub fn label(&self) -> Option<&Label<'l>> {
match self {
AnnotatedSpan(.., label) => label.as_ref(),
}
}
}
impl<'l> From<AnnotatedSpan<'l>> for Vec<AnnotatedSpan<'l>> {
fn from(x: AnnotatedSpan<'l>) -> Self {
vec![x]
}
}
pub trait Annotate: Sized {
/// Annotate a [`Span`] with a severity [`Level`] and an optional
/// [`Label`] to display alongside of it.
///
/// You may wish to use one of the more specific methods that provide a
/// more pleasent interface.
fn annotate(self, level: Level, label: Option<Label>) -> AnnotatedSpan;
/// Annotate a span as an internal error that the user is not expected
/// to be able to resolve,
/// but may be able to work around.
///
/// Since internal errors are one of the work things that can happen to
/// a user of a programming language,
/// given that they can only work around it,
/// this method mandates a help label that provides additional
/// context and a possible workaround.
fn internal_error<'l, L: Into<Label<'l>>>(
self,
label: L,
) -> AnnotatedSpan<'l> {
self.annotate(Level::InternalError, Some(label.into()))
}
/// Annotate a span with a clarifying label styled as an error.
///
/// This label is intended to augment the error message to help guide
/// the user to a resolution.
/// If the label does not include additional _useful_ information over
/// the generic message,
/// then it may be omitted in favor of `Annotate::mark_error` to
/// simply mark the location of the error.
///
/// (This is not named `err` since it does not return an [`Err`].)
fn error<'l, L: Into<Label<'l>>>(self, label: L) -> AnnotatedSpan<'l> {
self.annotate(Level::Error, Some(label.into()))
}
/// Like [`Annotate::error`],
/// but only styles the span as a [`Level::Error`] without attaching a
/// label.
///
/// This may be appropriate when a label would provide no more useful
/// information and would simply repeat the generic error text.
/// With that said,
/// if the repeat message seems psychologically beneficial in context,
/// you may wish to use [`Annotate::error`] anyway.
fn mark_error(self) -> AnnotatedSpan<'static> {
self.annotate(Level::Error, None)
}
/// Supplemental annotated span providing additional context for another
/// span.
///
/// For example,
/// if an error is related to a conflict with how an identifier is
/// defined,
/// then a note span may indicate the location of the identifier
/// definition.
fn note<'l, L: Into<Label<'l>>>(self, label: L) -> AnnotatedSpan<'l> {
self.annotate(Level::Note, Some(label.into()))
}
/// Provide additional information that may be used to help the user in
/// debugging or fixing a diagnostic.
///
/// While the other severity levels denote factual information,
/// this provides more loose guidance.
/// It may also include concrete suggested fixes.
fn help<'l, L: Into<Label<'l>>>(self, label: L) -> AnnotatedSpan<'l> {
self.annotate(Level::Help, Some(label.into()))
}
}
impl<S: Into<Span>> Annotate for S {
fn annotate(self, level: Level, label: Option<Label>) -> AnnotatedSpan {
AnnotatedSpan(self.into(), level, label)
}
}