How to detect generic parameter in field type inside procedural macro

Oh whoops; missed your other thread.


For the visitor:

I think you probably want a TypePath visitor that you feed the ty field in the Fields on your ItemStruct.

I'm still a little spooked at all the different variants on syn::Type but ultimately all roads do seem to lead back to TypePath. Filtering out Paths that aren't single path element + no path arguments and then comparing Idents with the list of generic Idents does seem like it'd work just fine.

Edit: I just re-read what you wrote and I think this is exactly what you were describing :man_facepalming:. Just in case though: I think you probably want to have a visitor that just overrides Visit::visit_field that then shells out to another visitor that just overrides Visit::visit_type_path; that way you can associate the type paths that are generic params that you discover with a particular field.

A few other things, though:

  • a single field can reference multiple generic params so you'll want to collect a list of params per field
    • as in, your input struct could look like this:
      struct Foo<A, B> { f1: (A, B), f2: usize }
      
    • for which you'd want to produce something like this:
      impl<F2> FooBuilder<Unset, F2> {
          fn f1<A, B>(self, val: (A, B)) -> FooBuilder<Set<(A, B)>, F2> { ... }
      }
      
  • if type parameters on your source struct have bounds you'll need to replicate these on your Builder's setters or at least on your Builder's build method (the thing that goes from FooBuilder to Foo:
    • i.e. for:
      struct Foo<A: Clone, B: Hash> { f1: A, f2: B }
      
    • if you produced:
      impl<A, B> FooBuilder<Set<A>, Set<B>> {
          fn build(self) -> Foo<A, B> { ... }
      }
      
    • you'd get a type error since A and B in the latter code block above aren't necessarily Clone and Hash respectively
    • to get this to work you'd need to do this at the minimum:
      impl<A: Clone, B: Hash> FooBuilder<Set<A>, Set<B>> {
          fn build(self) -> Foo<A, B> { ... }
      }
      
    • and you'd probably want to copy the bounds to the individual setter functions too (raising the type error only when you call build makes it less immediately apparent to users what the problematic field/builder call was, I think)
  • the above case isn't so bad since you can just literally copy the bounds to a couple of places but it actually gets worse (sorry)
    • consider:
      struct Foo<A: Clone, B: ?Sized + dyn FnOnce(A)> { a: A, b: Box<B> }
      
      • here you have to copy A everywhere B is used since the bound for B references A; this is especially problematic because it means that you can't set b before setting a (since A isn't a type on FooBuilder yet)
    • and also:
      struct Foo<'s, A: Clone + 's, B: ?Sized + Fn(&'s dyn FnOnce(&'s Box<A>))> { a: &'s A, b: Box<B> }
      
      • here there's now a lifetime parameter that has to be copied around where it's used
      • it's the same challenge as the previous example but with some indirection; you have to infer that B's bound involves 's and that 's is used in a therefore b can't be set until a is set
      • if you add multiple usages for parameters this gets worse; i.e.
        struct Foo<'s, A: 's, B: Fn(&'s u8)> { a: &'s A, b: Box<B>, c: &'s u8 }
        
        • here setting c or setting a should be enough to allow setting b but I don't think we can even represent this in the type system without specialization
    • and finally, const generics
      • these can be used in other types (i.e. arrays like [u8; N]) or used nowhere else in the struct's fields' types at all; not even in a PhantomData equivalent because the compiler doesn't need to infer variance for them
      • this means for types like:
        struct Foo<A, B, const N: usize> { a: A, b: B }
        
      • you'll have to know to copy const N: usize onto the build method or to carry it along on the Builder type
      • for types where the const params are used in other types/bounds, you'll need to infer that too

Apologies; that was definitely way longer than I intended. Anyways the point is that I think just carrying the type parameters on the Builder type lets you sidestep having to handle lots of different edge cases.

That said, It's totally understandable if you ultimately decide that you don't want to support all the different things described above; while I don't think they're particularly esoteric bits of Rust code it's probably valid to decide that it's unlikely someone will want an auto-generated builder for something with lifetime bounds in it.


One last thing though. I think this was what you were referring to re: limitations with putting the type parameters on the builder type.

I just wanted to note that it's totally possible to do what you were describing; i.e. pass around incomplete builders without having to concretely specify types for fields that haven't been set yet. For the example in the previous message:

fn half_set<P>() -> FooBuilder<P, Unset, Set<u16>> {
    FooBuilder::new().field2(23)
}

fn main() {
    let f: Foo<_> = half_set().field1("👋").build();
}

(Playground)

Not sure if that's good enough for your use case but I thought I'd mention it.


Regardless, I'd love to know what you ultimately choose to go with/how it turns out! I'm emotionally invested now :stuck_out_tongue:.