Derive Macro Determine if field implements trait

In a proc_macro I am looping through fields.named on a struct in order to implement the ratatui::StatefulWidgetRef trait. I need to special case any field that is a vector or hashmap but I have not found a good way of doing that yet. I first thought I could just get the type directly from field.ty but that only gives certain subtypes. I then tried matching on syn::Type::ImplTrait as seen below to check if something implements std::iter::Iterator but I was always getting a false return value from the below code:

let is_iterator = if let ImplTrait(impl_trait) = &f.ty {
    impl_trait == (&TypeImplTrait {
                    impl_token: Token!(impl)(proc_macro2::Span::mixed_site()),
                    bounds: {
                        let mut punct = Punctuated::new();
                        punct.push_value(TypeParamBound::Trait(TraitBound {
                            paren_token: None,
                            modifier: TraitBoundModifier::None,
                            lifetimes: None,
                            path: {
                                let mut segments = Punctuated::new();
                                segments.push_value(PathSegment {
                                    ident: format_ident! {"std"},
                                    arguments: PathArguments::None,
                                });
                                segments.push_punct(Token![::](proc_macro2::Span::mixed_site()));
                                segments.push_value(PathSegment {
                                    ident: format_ident! {"iter"},
                                    arguments: PathArguments::None,
                                });
                                segments.push_punct(Token![::](proc_macro2::Span::mixed_site()));
                                segments.push_value(PathSegment {
                                    ident: format_ident! {"Iterator"},
                                    arguments: PathArguments::None,
                                });
                                Path {
                                    leading_colon: None,
                                    segments,
                                }
                            },
                        }));
                        punct
                    },
                })
        } else {
            true
        };

I don't know if I screwed something up in the creation of the TypeImplTrait struct or this just doesn't work the way I expect it to.

This method of checking for impl std::iter::Iterator is not ideal as it allows other fields like Strings to pass the check but it will at least allow the macro to successfully function.

If there is an alternate solution for this problem, I am very interested.

The complete code is found here

Thank you.

Macros are run before the trait solver, so the information about what traits are implemented for a type isn’t available— What you’re trying to do isn’t possible in the general case. The code here isn’t checking for an Iterator implementation, but instead an opaque impl std::iter::Iterator type.

You have basically two ways forward:

  1. Add a FieldType:Iterator bound to the emitted code, so that the implementation is disabled by the trait solver later.
  2. Define a field annotation, and switch your implementation based on that instead
2 Likes

Macros don't have access to type information, so you'll need a solution that works without. Generally, this is done by having a trait which all field types are required to implement with provides the per-field behavior needed my the generated impl. Often it's the same trait as is being implemented.

If you must special case certain fields, the simple way is to add marker attributes, like how serde allows you to #[serde(skip)] fields.

If you absolutely must specialize behavior for some field types, you might be able to do so via autoderef specialization, but that should be a last resort and is far from straightforward.

2 Likes

I am actually using attributes on just about every field to define the visual aspects of the Widget implementation. I want to be able to flag fields with a special attribute that grabs them and then calls the len() method on them to get the number of items in the vector and display that item_count in a special place in the TUI layout. I want to produce a syn::Error::new_spanned() that errors when the user defines these special attributes on fields that do not support a len() method to avoid the hassle of debugging the method call errors within a proc_macro. Ideally I would be able to have this status of having a len() method available within the proc_macro code so I can enable/disable other warnings/errors using that boolean status, but it is not critical.

Thanks for the reply.

I am actually more concerned about just having a len() method on the field than actually having the field implement Iterator. I was using the Iterator trait as a potential indicator for the len() method.

Any way to enforce a bound on a method being present? Ideally I would like to error during the proc_macro rather than during the compilation of the generated code to reduce debugging headaches.

Edit: after doing more reading based on the 2 previous replies, it seems that I can implement a Length trait myself but I still can't use that in the proc_macro for logic and producing errors.

Correct. Macros -- proc or otherwise -- fundamentally can't check name resolution issues during the macro.

The general answer for this is to emit something that makes the error as obvious as possible. For example, if you need a .len() method that returns usize, emit an unused function like

fn _must_have_len_method_returning_usize(x: &MyType) -> usize { x.len() }

Then sure, it's not a failure in the proc macro, but an error like

error[E0599]: no method named `len` found for reference `&MyType` in the current scope
 --> src/lib.rs:2:67
  |
2 | fn _must_have_len_method_returning_usize(x: &MyType) -> usize { x.len() }
  |                                                                   ^^^ method not found in `&MyType`

is generally pretty clear for people.

2 Likes

Unfortunately, the error will still be

error[E0599]: no method named `len` found for reference `&MyType` in the current scope
  --> src/lib.rs:2:67
   |
LL | #[derive(StatefulWidgetRef)]
   |          ^^^^^^^^^^^^^^^^^ method not found in `&MyType`

if the tokens are quoted with call site hygiene (the default). If however, you use quote_spanned!{field=> #field.len()} to create the method call, the error will instead be

error[E0599]: no method named `len` found for type `MyType` in the current scope
  --> src/lib.rs:LL:CC
   |
LL | field: MyType,
   | ^^^^^ method not found in `MyType`

which is about as good as a missing trait error and difficult to do better than, tbh

4 Likes

quote_spanned() is quite interesting. That would help a lot, and I can add some documentation that explains where that error is coming from as well.

Thank you

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.