From 6a616d64f745935518afa2b79b09a968003605ab Mon Sep 17 00:00:00 2001 From: Ted Driggs Date: Wed, 8 Mar 2023 11:03:44 -0800 Subject: [PATCH] Add child diagnostics 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 --- CHANGELOG.md | 3 ++ core/src/error/child.rs | 82 ++++++++++++++++++++++++++++ core/src/error/mod.rs | 115 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 core/src/error/child.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 208ca80..8408407 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/core/src/error/child.rs b/core/src/error/child.rs new file mode 100644 index 0000000..c0e46f0 --- /dev/null +++ b/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, + message: String, +} + +impl ChildDiagnostic { + pub(in crate::error) fn new(level: Level, span: Option, 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) + } + } + } + } +} diff --git a/core/src/error/mod.rs b/core/src/error/mod.rs index f86d58e..8d88864 100644 --- a/core/src/error/mod.rs +++ b/core/src/error/mod.rs @@ -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; @@ -62,6 +64,9 @@ pub struct Error { locations: Vec, /// The span to highlight in the emitted diagnostic. span: Option, + /// Additional diagnostic messages to show with the error. + #[cfg(feature = "diagnostics")] + children: Vec, } /// Error creation functions @@ -71,6 +76,8 @@ impl Error { kind, locations: Vec::new(), span: None, + #[cfg(feature = "diagnostics")] + children: vec![], } } @@ -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 { 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] } @@ -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 @@ -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(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(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()