diff --git a/data_model/derive/src/id.rs b/data_model/derive/src/id.rs index 18af318dcf7..39983e1ad8e 100644 --- a/data_model/derive/src/id.rs +++ b/data_model/derive/src/id.rs @@ -1,7 +1,7 @@ #![allow(clippy::str_to_string, clippy::mixed_read_write_in_expression)] use darling::{FromAttributes, FromDeriveInput, FromField}; -use iroha_macro_utils::Emitter; +use iroha_macro_utils::{find_single_attr_opt, Emitter}; use manyhow::emit; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -20,27 +20,8 @@ enum IdAttr { impl FromAttributes for IdAttr { fn from_attributes(attrs: &[syn2::Attribute]) -> darling::Result { let mut accumulator = darling::error::Accumulator::default(); - let attrs = attrs - .iter() - .filter(|v| v.path().is_ident("id")) - .collect::>(); - let attr = match attrs.as_slice() { - [] => { - return accumulator.finish_with(IdAttr::Missing); - } - [attr] => attr, - [attr, ref tail @ ..] => { - accumulator.push( - darling::Error::custom("Only one `#[id]` attribute is allowed!").with_span( - &tail - .iter() - .map(syn2::spanned::Spanned::span) - .reduce(|a, b| a.join(b).unwrap()) - .unwrap(), - ), - ); - attr - } + let Some(attr) = find_single_attr_opt(&mut accumulator, "id", attrs) else { + return accumulator.finish_with(IdAttr::Missing); }; let result = match &attr.meta { diff --git a/data_model/derive/tests/has_origin.rs b/data_model/derive/tests/has_origin.rs new file mode 100644 index 00000000000..8522c4268fd --- /dev/null +++ b/data_model/derive/tests/has_origin.rs @@ -0,0 +1,53 @@ +use iroha_data_model::prelude::{HasOrigin, Identifiable}; +use iroha_data_model_derive::{HasOrigin, IdEqOrdHash}; + +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] +struct ObjectId(pub i32); + +// fake impl for `#[derive(IdEqOrdHash)]` +impl From for iroha_data_model::IdBox { + fn from(_: ObjectId) -> Self { + unimplemented!("fake impl") + } +} + +#[derive(Debug, IdEqOrdHash)] +struct Object { + id: ObjectId, +} + +impl Object { + fn id(&self) -> &ObjectId { + &self.id + } +} + +#[allow(clippy::enum_variant_names)] // it's a test, duh +#[derive(Debug, HasOrigin)] +#[has_origin(origin = Object)] +enum ObjectEvent { + EventWithId(ObjectId), + #[has_origin(event => &event.0)] + EventWithExtractor((ObjectId, i32)), + #[has_origin(obj => obj.id())] + EventWithAnotherExtractor(Object), +} + +#[test] +fn has_origin() { + let events = vec![ + ObjectEvent::EventWithId(ObjectId(1)), + ObjectEvent::EventWithExtractor((ObjectId(2), 2)), + ObjectEvent::EventWithAnotherExtractor(Object { id: ObjectId(3) }), + ]; + let expected_ids = vec![ObjectId(1), ObjectId(2), ObjectId(3)]; + + for (event, expected_id) in events.into_iter().zip(expected_ids) { + assert_eq!( + event.origin_id(), + &expected_id, + "mismatched origin id for event {:?}", + event + ); + } +} diff --git a/data_model/derive/tests/ui_fail/has_origin_multiple_attributes.rs b/data_model/derive/tests/ui_fail/has_origin_multiple_attributes.rs new file mode 100644 index 00000000000..ad09416af20 --- /dev/null +++ b/data_model/derive/tests/ui_fail/has_origin_multiple_attributes.rs @@ -0,0 +1,9 @@ +use iroha_data_model_derive::HasOrigin; + +#[derive(HasOrigin)] +#[has_origin(origin = Object)] +#[has_origin(origin = Object)] +#[has_origin(origin = Object)] +enum MultipleAttributes {} + +fn main() {} diff --git a/data_model/derive/tests/ui_fail/has_origin_multiple_attributes.stderr b/data_model/derive/tests/ui_fail/has_origin_multiple_attributes.stderr new file mode 100644 index 00000000000..35511493350 --- /dev/null +++ b/data_model/derive/tests/ui_fail/has_origin_multiple_attributes.stderr @@ -0,0 +1,6 @@ +error: Only one #[has_origin] attribute is allowed! + --> tests/ui_fail/has_origin_multiple_attributes.rs:5:1 + | +5 | / #[has_origin(origin = Object)] +6 | | #[has_origin(origin = Object)] + | |______________________________^ diff --git a/macro/utils/src/lib.rs b/macro/utils/src/lib.rs index 9f19785d07c..09eba6f07e8 100644 --- a/macro/utils/src/lib.rs +++ b/macro/utils/src/lib.rs @@ -70,42 +70,89 @@ macro_rules! attr_struct { }; } -/// Parses a single attribute of the form `#[attr_name(...)]` for darling using a `syn::parse::Parse` implementation. +/// Extension trait for [`darling::Error`]. /// -/// If no attribute with specified name is found, returns `Ok(None)`. -pub fn parse_single_list_attr_opt( - attr_name: &str, - attrs: &[syn2::Attribute], -) -> darling::Result> { - let mut accumulator = darling::error::Accumulator::default(); +/// Currently exists to add `with_spans` method. +pub trait DarlingErrorExt: Sized { + /// Attaches a combination of multiple spans to the error. + /// + /// Note that it only attaches the first span on stable rustc, as the `Span::join` method is not yet stabilized (https://github.com/rust-lang/rust/issues/54725#issuecomment-649078500). + fn with_spans(self, spans: impl IntoIterator>) -> Self; +} - // first, ensure there is only one attribute with the requested name - // take the first one if there are multiple +impl DarlingErrorExt for darling::Error { + fn with_spans(self, spans: impl IntoIterator>) -> Self { + // Unfortunately, the story for combining multiple spans in rustc proc macro is not yet complete. + // (see https://github.com/rust-lang/rust/issues/54725#issuecomment-649078500, https://github.com/rust-lang/rust/issues/54725#issuecomment-1547795742) + // syn does some hacks to get error reporting that is a bit better: https://docs.rs/syn/2.0.37/src/syn/error.rs.html#282 + // we can't to that because darling's error type does not let us do that. + + // on nightly, we are fine, as `.join` method works. On stable, we fall back to returning the first span. + + let mut iter = spans.into_iter(); + let Some(first) = iter.next() else { + return self; + }; + let first: proc_macro2::Span = first.into(); + let r = iter + .try_fold(first, |a, b| a.join(b.into())) + .unwrap_or(first); + + self.with_span(&r) + } +} + +/// Finds an optional single attribute with specified name. +/// +/// Returns `None` if no attributes with specified name are found. +/// +/// Emits an error into accumulator if multiple attributes with specified name are found. +#[must_use] +pub fn find_single_attr_opt<'a>( + accumulator: &mut darling::error::Accumulator, + attr_name: &str, + attrs: &'a [syn2::Attribute], +) -> Option<&'a syn2::Attribute> { let matching_attrs = attrs .iter() .filter(|a| a.path().is_ident(attr_name)) .collect::>(); let attr = match *matching_attrs.as_slice() { [] => { - return accumulator.finish_with(None); + return None; } [attr] => attr, [attr, ref tail @ ..] => { // allow parsing to proceed further to collect more errors accumulator.push( darling::Error::custom(format!("Only one #[{}] attribute is allowed!", attr_name)) - .with_span( - &tail - .iter() - .map(syn2::spanned::Spanned::span) - .reduce(|a, b| a.join(b).unwrap()) - .unwrap(), - ), + .with_spans(tail.iter().map(syn2::spanned::Spanned::span)), ); attr } }; + Some(attr) +} + +/// Parses a single attribute of the form `#[attr_name(...)]` for darling using a `syn::parse::Parse` implementation. +/// +/// If no attribute with specified name is found, returns `Ok(None)`. +/// +/// # Errors +/// +/// - If multiple attributes with specified name are found +/// - If attribute is not a list +pub fn parse_single_list_attr_opt( + attr_name: &str, + attrs: &[syn2::Attribute], +) -> darling::Result> { + let mut accumulator = darling::error::Accumulator::default(); + + let Some(attr) = find_single_attr_opt(&mut accumulator, attr_name, attrs) else { + return accumulator.finish_with(None); + }; + let mut kind = None; match &attr.meta { @@ -123,6 +170,12 @@ pub fn parse_single_list_attr_opt( /// Parses a single attribute of the form `#[attr_name(...)]` for darling using a `syn::parse::Parse` implementation. /// /// If no attribute with specified name is found, returns an error. +/// +/// # Errors +/// +/// - If multiple attributes with specified name are found +/// - If attribute is not a list +/// - If attribute is not found pub fn parse_single_list_attr( attr_name: &str, attrs: &[syn2::Attribute],