Skip to content

Commit

Permalink
Add child diagnostics
Browse files Browse the repository at this point in the history
When using the `diagnostics` feature, crate consumers can add custom
error, warning, note, and help messages to `Error` instances and have
those appear in the compiler's output.

Fixes #224
  • Loading branch information
TedDriggs committed Mar 9, 2023
1 parent 05de479 commit cd9a108
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 8 deletions.
75 changes: 75 additions & 0 deletions core/src/error/child.rs
@@ -0,0 +1,75 @@
use proc_macro::Level;
use proc_macro2::Span;

/// Supplemental message for an [`Error`](super::Error) when it's emitted as a `Diagnostic`.
///
/// # Example Output
/// The `note` and `help` lines below come from child diagnostics.
///
/// ```text
/// error: My custom error
/// --> my_project/my_file.rs:3:5
/// |
/// 13 | FooBar { value: String },
/// | ^^^^^^
/// |
/// = note: My note on the macro usage
/// = help: Try doing this instead
/// ```
#[derive(Debug, Clone)]
pub(in crate::error) struct ChildDiagnostic {
level: Level,
span: Option<Span>,
message: String,
}

impl ChildDiagnostic {
pub(in crate::error) fn new(level: Level, span: Option<Span>, message: String) -> Self {
Self {
level,
span,
message,
}
}
}

impl ChildDiagnostic {
/// Append this child diagnostic to a `Diagnostic`.
///
/// # Panics
/// This method panics if `self` has a span and is being invoked outside of
/// a proc-macro due to the behavior of [`Span::unwrap()`](Span).
pub fn append_to(self, diagnostic: proc_macro::Diagnostic) -> proc_macro::Diagnostic {
match self.level {
Level::Error => {
if let Some(span) = self.span {
diagnostic.span_error(span.unwrap(), self.message)
} else {
diagnostic.error(self.message)
}
}
Level::Warning => {
if let Some(span) = self.span {
diagnostic.span_warning(span.unwrap(), self.message)
} else {
diagnostic.warning(self.message)
}
}
Level::Note => {
if let Some(span) = self.span {
diagnostic.span_note(span.unwrap(), self.message)
} else {
diagnostic.note(self.message)
}
}
Level::Help => {
if let Some(span) = self.span {
diagnostic.span_help(span.unwrap(), self.message)
} else {
diagnostic.help(self.message)
}
}
_ => panic!("Unknown level: {:?}", self.level),
}
}
}
121 changes: 113 additions & 8 deletions core/src/error/mod.rs
Expand Up @@ -15,6 +15,8 @@ use std::vec;
use syn::spanned::Spanned;
use syn::{Lit, LitStr, Path};

#[cfg(feature = "diagnostics")]
mod child;
mod kind;

use crate::util::path_to_string;
Expand Down Expand Up @@ -62,6 +64,9 @@ pub struct Error {
locations: Vec<String>,
/// The span to highlight in the emitted diagnostic.
span: Option<Span>,
/// Additional diagnostic messages to show with the error.
#[cfg(feature = "diagnostics")]
children: Vec<child::ChildDiagnostic>,
}

/// Error creation functions
Expand All @@ -71,6 +76,8 @@ impl Error {
kind,
locations: Vec::new(),
span: None,
#[cfg(feature = "diagnostics")]
children: vec![],
}
}

Expand Down Expand Up @@ -234,6 +241,82 @@ impl Error {
}
}

/// Child diagnostic methods
#[cfg(feature = "diagnostics")]
impl Error {
pub fn error<T: fmt::Display>(mut self, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
proc_macro::Level::Error,
None,
message.to_string(),
));
self
}

pub fn warning<T: fmt::Display>(mut self, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
proc_macro::Level::Warning,
None,
message.to_string(),
));
self
}

pub fn note<T: fmt::Display>(mut self, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
proc_macro::Level::Note,
None,
message.to_string(),
));
self
}

pub fn help<T: fmt::Display>(mut self, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
proc_macro::Level::Help,
None,
message.to_string(),
));
self
}

pub fn span_error<S: Spanned, T: fmt::Display>(mut self, span: &S, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
proc_macro::Level::Error,
Some(span.span()),
message.to_string(),
));
self
}

pub fn span_warning<S: Spanned, T: fmt::Display>(mut self, span: &S, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
proc_macro::Level::Warning,
Some(span.span()),
message.to_string(),
));
self
}

pub fn span_note<S: Spanned, T: fmt::Display>(mut self, span: &S, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
proc_macro::Level::Note,
Some(span.span()),
message.to_string(),
));
self
}

pub fn span_help<S: Spanned, T: fmt::Display>(mut self, span: &S, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
proc_macro::Level::Help,
Some(span.span()),
message.to_string(),
));
self
}
}

/// Error instance methods
#[allow(clippy::len_without_is_empty)] // Error can never be empty
impl Error {
Expand Down Expand Up @@ -276,18 +359,36 @@ impl Error {
}

/// Recursively converts a tree of errors to a flattened list.
///
/// # Child Diagnostics
/// If the `diagnostics` feature is enabled, any child diagnostics on `self`
/// will be cloned down to all the errors within `self`.
pub fn flatten(self) -> Self {
Error::multiple(self.into_vec())
}

fn into_vec(self) -> Vec<Self> {
if let ErrorKind::Multiple(errors) = self.kind {
let mut flat = Vec::new();
for error in errors {
flat.extend(error.prepend_at(self.locations.clone()).into_vec());
}

flat
let locations = self.locations;

#[cfg(feature = "diagnostics")]
let children = self.children;

errors
.into_iter()
.flat_map(|error| {
// This is mutated if the diagnostics feature is enabled
#[allow(unused_mut)]
let mut error = error.prepend_at(locations.clone());

// Any child diagnostics in `self` are cloned down to all the distinct
// errors contained in `self`.
#[cfg(feature = "diagnostics")]
error.children.extend(children.iter().cloned());

error.into_vec()
})
.collect()
} else {
vec![self]
}
Expand Down Expand Up @@ -371,13 +472,17 @@ impl Error {
//
// If span information is available, don't include the error property path
// since it's redundant and not consistent with native compiler diagnostics.
match self.kind {
let diagnostic = match self.kind {
ErrorKind::UnknownField(euf) => euf.into_diagnostic(self.span),
_ => match self.span {
Some(span) => span.unwrap().error(self.kind.to_string()),
None => Diagnostic::new(Level::Error, self.to_string()),
},
}
};

self.children
.into_iter()
.fold(diagnostic, |out, child| child.append_to(out))
}

/// Transform this error and its children into a list of compiler diagnostics
Expand Down

0 comments on commit cd9a108

Please sign in to comment.