Proc-macro-attribute: Where and how to modify/replace code in a function

I'm using the Ferrous Systems pipeline when creating a [#proc-macro-attribute]:

I have worked out the parsing of the attribute parameters.
The challenge is to replace a struct, say MyStruct, with some Rust code, where:

  1. MyStruct could be under any module hierarchy, say my::mod::MyStruct or yours::MyStruct
  2. The Struct could be anywhere within the function

Example:

[#my-attribute(p=1, q=a)]
fn my_fn(x: String) {
    if true {
        let s = my::mod::MyStruct {
            f1: &p,
            f2: &q,
            f3: &x,
        }
    } else {
        let s = yours::MyStruct {
            f1: &p,
            f2: &q,
            f3: &x,
            ...
        }
    }
}

Before I investigate how best to do this it seems I should first arrive at where. So this is a two part question:

  1. Where in the above pipeline it is best to do this
  2. Can anyone point to a blog post, crate, tutorial or book where something like this generic struct replacement is covered?

As best I can tell, in either of the parse or analyze stages of the pipeline.

The most informative detail I've found is the playground example that accompanied this user forum answer by @Yandros:

While not as complete as @Yandros earlier response, the syn modules syn::visit and syn::visit_mut have some useful details:

HTH someone else.

1 Like

Indeed. In this instance, you may be interested in the .{fold,visit}_expr_struct{,_mut}() variants of the diverse AST-traversal traits, since that's the Expr "kind" that matches a literal struct construction.

From there, you can (after sub-recursing!), look at the .path field, and see if the .last() of its (path .segments) has an .ident that is equal to MyStruct.

But be aware that basing the analysis on syntax only and not semantics is a bit brittle (what if somebody has their::own::MyStruct which has nothing to do with your macro? Inversely, what if somebody did use MyStruct as Renamed;, and then used Renamed { ...}?)

Thanks again @Yandros for the additional insights that answers some follow-on questions, and will help.

I take your point.

Yes, if my proposal was accepted MyStruct would become something of a reserved word in the library. As I reflect on it I don't suppose there is anything lost by adding a segment to the path, and matching on, buyin::MyStruct. Is this what you meant by also using semantics?

Correct. I don't think I can guard against this - unless the AST holds this information?
In which case I'd have to add a feature to the roadmap, about making this more robust.

This "feature" does require you "buy-in" and commit to respecting a naming convention. It is an opt-in feature, so hopefully won't be too controversial for the community.

Overall, I'm not sure how else I could set things up so that library users could 'plugin' their own desired behavior - MyStruct { f1, f2, f3 } is effectively serving as compile-time plugin API (along with some functions) with which they can Cargo [patch] in their implementation.

Hmm, that was wrong.

The compile-time plugin API is, I think, some proc-macro-attributes. The lib user then provides their implementation of some callback-style functions (based on a template crate - a 80/20 rule starting point). Their proc-macro implementation is [patch]ed in via their application crate Cargo.toml, or some .cargo/config.toml

One such function is invoked on matching buyin::MyStruct{f1,f2,f3} - because they have a proc-macro template to start from, they could remove this functionality entirely. Which is a feature - I don't want to tie their hands, nor step on their feet - as you've pointed out above.
All they need to do is respect the proc-macro-attribute syntax used in the template, and some naming conventions and they are free to do as they wish - the audience is developers.

At least, that is the hope, and I'm open to suggestions or pointers to crates that have done this a better way.