How I solved the problem of cfg/cfg_attr
expansion in proc macro attributes
#124
Replies: 1 comment 3 replies
-
I think the That is not what I was talking about: Rather, the idea would be that // user written code:
#[bon::builder]
fn greet(
#[cfg(all())] name: &str,
#[cfg(any())] level: Option<u32>,
) -> String {
format!("Hello {name}!")
}
// expands to
fn greet() -> GreetBuilder {
GreetBuilder {
#[cfg(all())]
name: None,
#[cfg(any())]
level: None,
}
}
#[derive(bon::Builder)]
#[bon(__private_fn_builder)]
struct GreetBuilder<'a> {
#[cfg(all())]
name: Option<&'a str>,
#[cfg(any(())]
level: Option<Option<u32>>,
}
// what the Builder derive sees
#[bon(__private_fn_builder)]
struct GreetBuilder<'a> {
name: Option<&'a str>,
}
// so it can generate an `impl<'a> GreetBuilder<'a>`
// knowing that there is (only) a name field Am I talking nonsense? I applied this pattern successfully before so I know the general idea works, but maybe it's not applicable here. ¹ either always, or only if there are any |
Beta Was this translation helpful? Give feedback.
-
This is in response to the reddit comment by @jplatte.
When developing a hack for
cfg/cfg_attr
handing in proc macro attributes, I tried almost everything possible. I even tried generating a dummy struct with aderive()
(as suggested in @jplatte's Reddit comment). It looked smth like this:Now let's suppose that only
feature = "bar"
cfg evaluates totrue
.In this case, the logic of
derive(bon::__ExpandCfgs)
would expand to the following code:Then
bon::builder
would parse the special__cfgs(...)
syntax in front of its parameters to collect the cfgs expressions that evaluate totrue
. Then there is a bit of code that traverses the function or impl block thatbon::builder
was placed on and evaluates thecfg/cfg_attr
manually (it basically reimplements these attrs inside ofbon::builder
). Yeah... that's unfortunate:bon/bon-macros/src/normalization/cfg/mod.rs
Lines 191 to 243 in 3217b4b
I implemented this logic, tested it and it indeed worked! Yes, but... it didn't work well with Rust Analyzer. The entire function body with the
#[bon::builder]
annotation was completely excluded from RA's analysis. No code completions and on-hover hints worked in it. Why? I'm not entirely sure, but I think what causes this is that function item is passed as an argument to the derive macro like#[expand_cfgs(item(fn greet(/**/) {/* */}))]
. This somehow makes RA lose the span information, I guess.I even tried a crazy hack of passing the function item in a nested expression inside of the enum's discriminant value expression. However, I stumbled with a bug in rustc, in that it invalidly expands such code (see rust-lang/rust#130004). I suppose no one ever tried doing such a crazy hack so no one ever caught such a bug.
So, in the end, I didn't use the
derive
syntax approach to evaluate the cfgs, instead I used themacro_rules!
that can be observed here:bon/bon/src/private/mod.rs
Lines 85 to 188 in 3217b4b
So the
#[bon::builder]
macro generates a call to thismacro_rules
which basically does the similar job of expanding the cfgs, collecting the results and passing them as__cfg(...)
in front of the callback proc macro attribute parameters. This, surprisingly works well with RA. RA can even recursively expand the macro with its "Expand macro recursively" feature.Note that both of these solutions suffer from a very silly problem that you need to generate unique names for the struct under the derive (if you use the derive approach) or the symbols created by
use
statements in themacro_rules!
approach current used inbon
. You probably don't want macros to generate non-deterministic random identifiers on each rerun to avoid any unknown headaches that this may bring, so there is a bit of heuristic solution to this problem implemented here:bon/bon-macros/src/normalization/cfg/mod.rs
Lines 129 to 189 in 3217b4b
Beta Was this translation helpful? Give feedback.
All reactions