Skip to content

Commit

Permalink
[refactor] #3882: Add tests for derive(HasOrigin) macro, reduce repet…
Browse files Browse the repository at this point in the history
…ition in derive(IdEqOrdHash), fix error reporting on stable

Signed-off-by: Nikita Strygin <[email protected]>
  • Loading branch information
DCNick3 authored and 6r1d committed Oct 17, 2023
1 parent d1380a5 commit 53491a4
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 39 deletions.
25 changes: 3 additions & 22 deletions data_model/derive/src/id.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -20,27 +20,8 @@ enum IdAttr {
impl FromAttributes for IdAttr {
fn from_attributes(attrs: &[syn2::Attribute]) -> darling::Result<Self> {
let mut accumulator = darling::error::Accumulator::default();
let attrs = attrs
.iter()
.filter(|v| v.path().is_ident("id"))
.collect::<Vec<_>>();
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 {
Expand Down
53 changes: 53 additions & 0 deletions data_model/derive/tests/has_origin.rs
Original file line number Diff line number Diff line change
@@ -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<ObjectId> 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
);
}
}
Original file line number Diff line number Diff line change
@@ -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() {}
Original file line number Diff line number Diff line change
@@ -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)]
| |______________________________^
87 changes: 70 additions & 17 deletions macro/utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Body: syn2::parse::Parse>(
attr_name: &str,
attrs: &[syn2::Attribute],
) -> darling::Result<Option<Body>> {
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<Item = impl Into<proc_macro2::Span>>) -> 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<Item = impl Into<proc_macro2::Span>>) -> 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::<Vec<_>>();
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<Body: syn2::parse::Parse>(
attr_name: &str,
attrs: &[syn2::Attribute],
) -> darling::Result<Option<Body>> {
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 {
Expand All @@ -123,6 +170,12 @@ pub fn parse_single_list_attr_opt<Body: syn2::parse::Parse>(
/// 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<Body: syn2::parse::Parse>(
attr_name: &str,
attrs: &[syn2::Attribute],
Expand Down

0 comments on commit 53491a4

Please sign in to comment.