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 6a616d6
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog

## Unreleased
- Add support for child diagnostics when `diagnostics` feature enabled [#224](https://github.com/TedDriggs/darling/issues/224)

## v0.14.3 (February 3, 2023)

- Re-export `syn` from `darling` to avoid requiring that consuming crates have a `syn` dependency.
Expand Down
82 changes: 82 additions & 0 deletions core/src/error/child.rs
@@ -0,0 +1,82 @@
use proc_macro2::Span;

/// Exhaustive mirror of [`proc_macro::Level`].
#[derive(Debug, Clone)]
pub(in crate::error) enum Level {
Error,
Warning,
Note,
Help,
}

/// 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)
}
}
}
}
}
115 changes: 107 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 @@ -276,18 +283,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 +396,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 Expand Up @@ -419,6 +448,76 @@ impl Error {
}
}

#[cfg(feature = "diagnostics")]
macro_rules! add_child {
($unspanned:ident, $spanned:ident, $level:ident) => {
#[doc = concat!("Add a child ", stringify!($unspanned), " message to this error.")]
#[doc = "# Example"]
#[doc = "```rust"]
#[doc = "# use darling_core::Error;"]
#[doc = concat!(r#"Error::custom("Example")."#, stringify!($unspanned), r#"("message content");"#)]
#[doc = "```"]
pub fn $unspanned<T: fmt::Display>(mut self, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
child::Level::$level,
None,
message.to_string(),
));
self
}

#[doc = concat!("Add a child ", stringify!($unspanned), " message to this error with its own span.")]
#[doc = "# Example"]
#[doc = "```rust"]
#[doc = "# use darling_core::Error;"]
#[doc = "# let item_to_span = proc_macro2::Span::call_site();"]
#[doc = concat!(r#"Error::custom("Example")."#, stringify!($spanned), r#"(&item_to_span, "message content");"#)]
#[doc = "```"]
pub fn $spanned<S: Spanned, T: fmt::Display>(mut self, span: &S, message: T) -> Self {
self.children.push(child::ChildDiagnostic::new(
child::Level::$level,
Some(span.span()),
message.to_string(),
));
self
}
};
}

/// Add child diagnostics to the error.
///
/// # Example
///
/// ## Code
///
/// ```rust
/// # use darling_core::Error;
/// # let struct_ident = proc_macro2::Span::call_site();
/// Error::custom("this is a demo")
/// .with_span(&struct_ident)
/// .note("we wrote this")
/// .help("try doing this instead");
/// ```
/// ## Output
///
/// ```text
/// error: this is a demo
/// --> my_project/my_file.rs:3:5
/// |
/// 13 | FooBar { value: String },
/// | ^^^^^^
/// |
/// = note: we wrote this
/// = help: try doing this instead
/// ```
#[cfg(feature = "diagnostics")]
impl Error {
add_child!(error, span_error, Error);
add_child!(warning, span_warning, Warning);
add_child!(note, span_note, Note);
add_child!(help, span_help, Help);
}

impl StdError for Error {
fn description(&self) -> &str {
self.kind.description()
Expand Down

0 comments on commit 6a616d6

Please sign in to comment.