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 8effae15..e539f531 100644 --- a/bon-macros/src/builder/builder_gen/input_fn/validation.rs +++ b/bon-macros/src/builder/builder_gen/input_fn/validation.rs @@ -30,14 +30,12 @@ impl super::FnInputCtx<'_> { } } - 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`" - ); - } + 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`" + ); } 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 a345f6fd..d8181f90 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_some() { + if top_config.const_.is_present() { 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 4462b3d2..fa9684c3 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_: Option, + pub(super) const_: darling::util::Flag, pub(super) on: Vec, /// This is the visibility of the original item that the builder is generated for. @@ -357,6 +357,10 @@ 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 a917f522..24230bb3 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,7 +6,6 @@ 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; @@ -44,13 +43,9 @@ fn parse_start_fn(meta: &syn::Meta) -> Result { #[derive(Debug, FromMeta)] pub(crate) struct TopLevelConfig { - /// 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, + /// Specifies whether the generated functions should be `const` + #[darling(rename = "const")] + pub(crate) const_: darling::util::Flag, /// Overrides the path to the `bon` crate. This is useful when the macro is /// wrapped in another macro that also reexports `bon`. @@ -129,30 +124,7 @@ impl TopLevelConfig { Self::parse_for_any(configs) } - 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))?; - } - + fn parse_for_any(configs: Vec) -> Result { let configs = configs .into_iter() .map(NestedMeta::parse_meta_list) @@ -188,10 +160,7 @@ impl TopLevelConfig { } } - let me = Self { - const_, - ..Self::from_list(&configs)? - }; + let me = 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 260624dc..d2a137b7 100644 --- a/bon/tests/integration/builder/attr_const.rs +++ b/bon/tests/integration/builder/attr_const.rs @@ -9,7 +9,8 @@ mod msrv_1_61 { #[test] const fn test_struct() { #[derive(Builder)] - #[builder(const)] + // Make sure `const` is parsed if it's not the first attribute + #[builder(builder_type(vis = ""), const)] struct Sut { #[builder(start_fn)] x1: u32,