From bc2a286bceafc528ca54d46589ff7abb61132688 Mon Sep 17 00:00:00 2001 From: Ted Driggs Date: Wed, 8 Mar 2023 11:03:44 -0800 Subject: [PATCH] WIP: Add child diagnostics Fixes #224 --- core/src/error/child.rs | 75 +++++++++++++++++++++++++ core/src/error/mod.rs | 121 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 core/src/error/child.rs diff --git a/core/src/error/child.rs b/core/src/error/child.rs new file mode 100644 index 0000000..8bea134 --- /dev/null +++ b/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, + 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) + } + } + _ => panic!("Unknown level: {:?}", self.level), + } + } +} diff --git a/core/src/error/mod.rs b/core/src/error/mod.rs index f86d58e..36af372 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![], } } @@ -234,6 +241,82 @@ impl Error { } } +/// Child diagnostic methods +#[cfg(feature = "diagnostics")] +impl Error { + pub fn error(mut self, message: &T) -> Self { + self.children.push(child::ChildDiagnostic::new( + proc_macro::Level::Error, + None, + message.to_string(), + )); + self + } + + pub fn warning(mut self, message: &T) -> Self { + self.children.push(child::ChildDiagnostic::new( + proc_macro::Level::Warning, + None, + message.to_string(), + )); + self + } + + pub fn note(mut self, message: &T) -> Self { + self.children.push(child::ChildDiagnostic::new( + proc_macro::Level::Note, + None, + message.to_string(), + )); + self + } + + pub fn help(mut self, message: &T) -> Self { + self.children.push(child::ChildDiagnostic::new( + proc_macro::Level::Help, + None, + message.to_string(), + )); + self + } + + pub fn span_error(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(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(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(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 { @@ -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 { 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 +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