diff --git a/bon-macros/src/builder/builder_gen/input_fn/validation.rs b/bon-macros/src/builder/builder_gen/input_fn/validation.rs index e539f531..8effae15 100644 --- a/bon-macros/src/builder/builder_gen/input_fn/validation.rs +++ b/bon-macros/src/builder/builder_gen/input_fn/validation.rs @@ -30,12 +30,14 @@ impl super::FnInputCtx<'_> { } } - if self.config.const_.is_present() && self.fn_item.orig.sig.constness.is_none() { - bail!( - &self.config.const_.span(), - "#[builder(const)] requires the underlying function to be \ - marked as `const fn`" - ); + if let Some(const_) = &self.config.const_ { + if self.fn_item.orig.sig.constness.is_none() { + bail!( + &const_, + "#[builder(const)] requires the underlying function to be \ + marked as `const fn`" + ); + } } Ok(()) diff --git a/bon-macros/src/builder/builder_gen/member/config/mod.rs b/bon-macros/src/builder/builder_gen/member/config/mod.rs index d8181f90..a345f6fd 100644 --- a/bon-macros/src/builder/builder_gen/member/config/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/config/mod.rs @@ -209,7 +209,7 @@ impl MemberConfig { } pub(crate) fn validate(&self, top_config: &TopLevelConfig, origin: MemberOrigin) -> Result { - if top_config.const_.is_present() { + if top_config.const_.is_some() { self.require_const_compat()?; } diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs index fa9684c3..4462b3d2 100644 --- a/bon-macros/src/builder/builder_gen/models.rs +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -181,7 +181,7 @@ pub(super) struct BuilderGenCtxParams<'a> { pub(super) members: Vec, pub(super) allow_attrs: Vec, - pub(super) const_: darling::util::Flag, + pub(super) const_: Option, pub(super) on: Vec, /// This is the visibility of the original item that the builder is generated for. @@ -357,10 +357,6 @@ impl BuilderGenCtx { } }); - let const_ = const_ - .is_present() - .then(|| syn::Token![const](const_.span())); - Ok(Self { bon, state_var, diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index c9507cde..2b3d51c9 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -6,6 +6,7 @@ use crate::parsing::{BonCratePath, ItemSigConfig, ItemSigConfigParsing, SpannedK use crate::util::prelude::*; use darling::ast::NestedMeta; use darling::FromMeta; +use syn::parse::Parser; use syn::punctuated::Punctuated; use syn::ItemFn; @@ -43,9 +44,13 @@ fn parse_start_fn(meta: &syn::Meta) -> Result { #[derive(Debug, FromMeta)] pub(crate) struct TopLevelConfig { - /// Specifies whether the generated functions should be `const` - #[darling(rename = "const")] - pub(crate) const_: darling::util::Flag, + /// Specifies whether the generated functions should be `const`. + /// + /// It is marked as `#[darling(skip)]` because `const` is a keyword, that + /// can't be parsed as a `syn::Ident` and therefore as a `syn::Meta` item. + /// We manually parse it from the beginning `builder(...)`. + #[darling(skip)] + pub(crate) const_: Option, /// Overrides the path to the `bon` crate. This is useful when the macro is /// wrapped in another macro that also reexports `bon`. @@ -124,7 +129,30 @@ impl TopLevelConfig { Self::parse_for_any(configs) } - fn parse_for_any(configs: Vec) -> Result { + fn parse_for_any(mut configs: Vec) -> Result { + fn parse_const_prefix( + parse: syn::parse::ParseStream<'_>, + ) -> syn::Result<(Option, TokenStream)> { + let const_ = parse.parse().ok(); + if const_.is_some() && !parse.is_empty() { + parse.parse::()?; + } + + let rest = parse.parse()?; + Ok((const_, rest)) + } + + // Try to parse the first token of the first config as `const` token. + // We have to do this manually because `syn` doesn't support parsing + // keywords in the `syn::Meta` keys. Yeah, unfortunately it means that + // the users must ensure they place `const` right at the beginning of + // their `#[builder(...)]` attributes. + let mut const_ = None; + + if let Some(config) = configs.first_mut() { + (const_, *config) = parse_const_prefix.parse2(std::mem::take(config))?; + } + let configs = configs .into_iter() .map(NestedMeta::parse_meta_list) @@ -160,7 +188,10 @@ impl TopLevelConfig { } } - let me = Self::from_list(&configs)?; + let me = Self { + const_, + ..Self::from_list(&configs)? + }; if let Some(on) = me.on.iter().skip(1).find(|on| on.required.is_present()) { bail!( diff --git a/bon/tests/integration/builder/attr_const.rs b/bon/tests/integration/builder/attr_const.rs index d2a137b7..260624dc 100644 --- a/bon/tests/integration/builder/attr_const.rs +++ b/bon/tests/integration/builder/attr_const.rs @@ -9,8 +9,7 @@ mod msrv_1_61 { #[test] const fn test_struct() { #[derive(Builder)] - // Make sure `const` is parsed if it's not the first attribute - #[builder(builder_type(vis = ""), const)] + #[builder(const)] struct Sut { #[builder(start_fn)] x1: u32,