diff --git a/Cargo.lock b/Cargo.lock index 8774fd16..ebcf9cc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", + "rustversion", "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 29da93e4..b6e4bd1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,6 +147,12 @@ missing_crate_level_docs = "warn" unescaped_backticks = "warn" [workspace.lints.rust] +# This lint is used only to warn about the changes in drop order during the +# transition from Rust 2021 to Rust 2024. There are too many instances of this +# lint in our code base and some of them may be false positives. So we just allow +# this lint globally. +tail_expr_drop_order = { level = "allow", priority = 2 } + unsafe_code = "warn" unstable_features = "warn" diff --git a/benchmarks/runtime/src/args_10.rs b/benchmarks/runtime/src/args_10.rs index 6d6f268e..d7188771 100644 --- a/benchmarks/runtime/src/args_10.rs +++ b/benchmarks/runtime/src/args_10.rs @@ -46,8 +46,8 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(expose_positional_fn = regular)] -fn builder( +#[builder(start_fn = builder)] +fn regular( arg1: &str, arg2: u32, arg3: bool, diff --git a/benchmarks/runtime/src/args_10_alloc.rs b/benchmarks/runtime/src/args_10_alloc.rs index 06dc976d..c43d91c6 100644 --- a/benchmarks/runtime/src/args_10_alloc.rs +++ b/benchmarks/runtime/src/args_10_alloc.rs @@ -46,8 +46,8 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(expose_positional_fn = regular)] -fn builder( +#[builder(start_fn = builder)] +fn regular( arg1: String, arg2: u32, arg3: bool, diff --git a/benchmarks/runtime/src/args_10_structs.rs b/benchmarks/runtime/src/args_10_structs.rs index 037ccff3..a98b3f64 100644 --- a/benchmarks/runtime/src/args_10_structs.rs +++ b/benchmarks/runtime/src/args_10_structs.rs @@ -87,8 +87,8 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(expose_positional_fn = regular)] -fn builder( +#[builder(start_fn = builder)] +fn regular( arg1: Point3D, arg2: u32, arg3: bool, diff --git a/benchmarks/runtime/src/args_20.rs b/benchmarks/runtime/src/args_20.rs index dc87323f..d87c6d29 100644 --- a/benchmarks/runtime/src/args_20.rs +++ b/benchmarks/runtime/src/args_20.rs @@ -121,8 +121,8 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(expose_positional_fn = regular)] -fn builder( +#[builder(start_fn = builder)] +fn regular( arg1: &str, arg2: u32, arg3: bool, diff --git a/benchmarks/runtime/src/args_3.rs b/benchmarks/runtime/src/args_3.rs index cfee7dac..da24e90e 100644 --- a/benchmarks/runtime/src/args_3.rs +++ b/benchmarks/runtime/src/args_3.rs @@ -13,8 +13,8 @@ pub fn builder_bench() -> u32 { builder().arg1(arg1).arg2(arg2).maybe_arg3(arg3).call() } -#[builder(expose_positional_fn = regular)] -fn builder(arg1: &str, arg2: u32, arg3: Option<&str>) -> u32 { +#[builder(start_fn = builder)] +fn regular(arg1: &str, arg2: u32, arg3: Option<&str>) -> u32 { let x = arg1.parse::().unwrap() + arg2; let x = x + arg3.map(|x| x.parse::().unwrap()).unwrap_or(0); x diff --git a/benchmarks/runtime/src/args_5.rs b/benchmarks/runtime/src/args_5.rs index eda0741d..d7af533d 100644 --- a/benchmarks/runtime/src/args_5.rs +++ b/benchmarks/runtime/src/args_5.rs @@ -19,8 +19,8 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(expose_positional_fn = regular)] -fn builder(arg1: &str, arg2: u32, arg3: bool, arg4: Option<&str>, arg5: Option) -> u32 { +#[builder(start_fn = builder)] +fn regular(arg1: &str, arg2: u32, arg3: bool, arg4: Option<&str>, arg5: Option) -> u32 { let x = arg1.parse::().unwrap() + arg2; let x = x + u32::from(arg3); let x = x + arg4.map(|x| x.parse::().unwrap()).unwrap_or(0); diff --git a/bon-macros/Cargo.toml b/bon-macros/Cargo.toml index 9ea686bd..6f9b6c50 100644 --- a/bon-macros/Cargo.toml +++ b/bon-macros/Cargo.toml @@ -58,6 +58,7 @@ quote = "1" syn = { version = "2.0.56", features = ["full", "visit-mut", "visit"] } prettyplease = "0.2" +rustversion = "1.0" [features] default = [] diff --git a/bon-macros/src/bon.rs b/bon-macros/src/bon.rs index 51b115f7..d24c8678 100644 --- a/bon-macros/src/bon.rs +++ b/bon-macros/src/bon.rs @@ -1,39 +1,35 @@ use crate::builder; -use crate::normalization::{ExpandCfg, ExpansionOutput}; +use crate::normalization::{ExpandCfg, Expansion}; use crate::util::prelude::*; +use darling::ast::NestedMeta; +use darling::FromMeta; pub(crate) fn generate(params: TokenStream, item: TokenStream) -> TokenStream { - crate::error::with_fallback(item.clone(), || try_generate(params, item)) + crate::error::handle_errors(item.clone(), || try_generate(params, item)) .unwrap_or_else(std::convert::identity) } pub(crate) fn try_generate(params: TokenStream, item: TokenStream) -> Result { let item: syn::Item = syn::parse2(item)?; - let macro_path = syn::parse_quote!(::bon::bon); let ctx = ExpandCfg { - macro_path, - params, + current_macro: format_ident!("bon"), + config: params, item, }; - let (params, item) = match ctx.expand_cfg()? { - ExpansionOutput::Expanded { params, item } => (params, item), - ExpansionOutput::Recurse(output) => return Ok(output), + let input = match ctx.expand_cfg()? { + Expansion::Expanded(input) => input, + Expansion::Recurse(output) => return Ok(output), }; - if !params.is_empty() { - bail!( - ¶ms, - "`#[bon]` attribute does not accept any parameters yet, \ - but it will in future releases" - ); - } + let params = NestedMeta::parse_meta_list(input.config)?; + let params = FromMeta::from_list(¶ms)?; - match item { - syn::Item::Impl(item_impl) => builder::item_impl::generate(item_impl), + match input.item { + syn::Item::Impl(item_impl) => builder::item_impl::generate(params, item_impl), _ => bail!( - &item, + &input.item, "`#[bon]` attribute is expected to be placed on an `impl` block \ but it was placed on other syntax instead" ), diff --git a/bon-macros/src/builder/builder_gen/builder_derives.rs b/bon-macros/src/builder/builder_gen/builder_derives.rs index 8d7d334a..faa3651a 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives.rs @@ -1,4 +1,4 @@ -use super::builder_params::{BuilderDerive, BuilderDerives}; +use super::top_level_config::{DeriveConfig, DerivesConfig}; use super::BuilderGenCtx; use crate::builder::builder_gen::Member; use crate::util::prelude::*; @@ -6,7 +6,7 @@ use darling::ast::GenericParamExt; impl BuilderGenCtx { pub(crate) fn builder_derives(&self) -> TokenStream { - let BuilderDerives { clone, debug } = &self.builder_type.derives; + let DerivesConfig { clone, debug } = &self.builder_type.derives; let mut tokens = TokenStream::new(); @@ -31,7 +31,7 @@ impl BuilderGenCtx { fn where_clause_for_derive( &self, target_trait_bounds: &TokenStream, - derive: &BuilderDerive, + derive: &DeriveConfig, ) -> TokenStream { let derive_specific_predicates = derive .bounds @@ -64,7 +64,8 @@ impl BuilderGenCtx { } } - fn derive_clone(&self, derive: &BuilderDerive) -> TokenStream { + fn derive_clone(&self, derive: &DeriveConfig) -> TokenStream { + let bon = &self.bon; let generics_decl = &self.generics.decl_without_defaults; let generic_args = &self.generics.args; let builder_ident = &self.builder_type.ident; @@ -113,7 +114,7 @@ impl BuilderGenCtx { let ty = member.underlying_norm_ty(); quote! { - ::bon::private::derives::clone_member::<#ty>( + #bon::__private::derives::clone_member::<#ty>( &self.#named_members_field.#member_index ) } @@ -153,10 +154,11 @@ impl BuilderGenCtx { } } - fn derive_debug(&self, derive: &BuilderDerive) -> TokenStream { + fn derive_debug(&self, derive: &DeriveConfig) -> TokenStream { let receiver_field = &self.ident_pool.receiver; let start_fn_args_field = &self.ident_pool.start_fn_args; let named_members_field = &self.ident_pool.named_members; + let bon = &self.bon; let format_members = self.members.iter().filter_map(|member| { match member { @@ -168,7 +170,7 @@ impl BuilderGenCtx { if let Some(value) = &self.#named_members_field.#member_index { output.field( #member_ident_str, - ::bon::private::derives::as_dyn_debug::<#member_ty>(value) + #bon::__private::derives::as_dyn_debug::<#member_ty>(value) ); } }) @@ -180,7 +182,7 @@ impl BuilderGenCtx { Some(quote! { output.field( #member_ident_str, - ::bon::private::derives::as_dyn_debug::<#member_ty>( + #bon::__private::derives::as_dyn_debug::<#member_ty>( &self.#start_fn_args_field.#member_index ) ); @@ -199,7 +201,7 @@ impl BuilderGenCtx { quote! { output.field( "self", - ::bon::private::derives::as_dyn_debug::<#ty>( + #bon::__private::derives::as_dyn_debug::<#ty>( &self.#receiver_field ) ); diff --git a/bon-macros/src/builder/builder_gen/builder_params/mod.rs b/bon-macros/src/builder/builder_gen/builder_params/mod.rs deleted file mode 100644 index 67e9413b..00000000 --- a/bon-macros/src/builder/builder_gen/builder_params/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -mod on_params; - -pub(crate) use on_params::OnParams; - -use crate::parsing::{ItemParams, ItemParamsParsing}; -use crate::util::prelude::*; -use darling::FromMeta; -use syn::punctuated::Punctuated; - -fn parse_finish_fn(meta: &syn::Meta) -> Result { - ItemParamsParsing { - meta, - reject_self_mentions: Some("builder struct's impl block"), - } - .parse() -} - -fn parse_builder_type(meta: &syn::Meta) -> Result { - ItemParamsParsing { - meta, - reject_self_mentions: Some("builder struct"), - } - .parse() -} - -fn parse_state_mod(meta: &syn::Meta) -> Result { - ItemParamsParsing { - meta, - reject_self_mentions: Some("builder's state module"), - } - .parse() -} - -#[derive(Debug, FromMeta)] -pub(crate) struct BuilderParams { - #[darling(default, with = parse_finish_fn)] - pub(crate) finish_fn: ItemParams, - - #[darling(default, with = parse_builder_type)] - pub(crate) builder_type: ItemParams, - - #[darling(default, with = parse_state_mod)] - pub(crate) state_mod: ItemParams, - - #[darling(multiple, with = crate::parsing::parse_non_empty_paren_meta_list)] - pub(crate) on: Vec, - - /// Specifies the derives to apply to the builder. - #[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)] - pub(crate) derive: BuilderDerives, -} - -#[derive(Debug, Clone, Default, FromMeta)] -pub(crate) struct BuilderDerives { - #[darling(rename = "Clone")] - pub(crate) clone: Option, - - #[darling(rename = "Debug")] - pub(crate) debug: Option, -} - -#[derive(Debug, Clone, Default)] -pub(crate) struct BuilderDerive { - pub(crate) bounds: Option>, -} - -impl FromMeta for BuilderDerive { - fn from_meta(meta: &syn::Meta) -> Result { - if let syn::Meta::Path(_) = meta { - return Ok(Self { bounds: None }); - } - - meta.require_list()?.require_parens_delim()?; - - #[derive(FromMeta)] - struct Parsed { - #[darling(with = crate::parsing::parse_paren_meta_list_with_terminated)] - bounds: Punctuated, - } - - let Parsed { bounds } = Parsed::from_meta(meta)?; - - Ok(Self { - bounds: Some(bounds), - }) - } -} diff --git a/bon-macros/src/builder/builder_gen/finish_fn.rs b/bon-macros/src/builder/builder_gen/finish_fn.rs index 4a274853..b4c1a69a 100644 --- a/bon-macros/src/builder/builder_gen/finish_fn.rs +++ b/bon-macros/src/builder/builder_gen/finish_fn.rs @@ -31,16 +31,15 @@ impl super::BuilderGenCtx { self.#named_members_field.#index }; - let param_default = member - .params + let default = member + .config .default .as_ref() .map(|default| default.value.as_ref()); - match param_default { + match default { Some(Some(default)) => { - let has_into = member.params.into.is_present(); - let default = if has_into { + let default = if member.config.into.is_present() { quote! { Into::into((|| #default)()) } } else { quote! { #default } diff --git a/bon-macros/src/builder/builder_gen/input_fn.rs b/bon-macros/src/builder/builder_gen/input_fn.rs index 5ec08a71..7820a303 100644 --- a/bon-macros/src/builder/builder_gen/input_fn.rs +++ b/bon-macros/src/builder/builder_gen/input_fn.rs @@ -1,72 +1,34 @@ -use super::builder_params::BuilderParams; use super::models::FinishFnParams; +use super::top_level_config::TopLevelConfig; use super::{ AssocMethodCtx, AssocMethodReceiverCtx, BuilderGenCtx, FinishFnBody, Generics, Member, MemberOrigin, RawMember, }; use crate::builder::builder_gen::models::{BuilderGenCtxParams, BuilderTypeParams, StartFnParams}; use crate::normalization::{GenericsNamespace, NormalizeSelfTy, SyntaxVariant}; -use crate::parsing::{ItemParams, SpannedKey}; +use crate::parsing::{ItemSigConfig, SpannedKey}; use crate::util::prelude::*; -use darling::util::SpannedValue; -use darling::FromMeta; use std::borrow::Cow; use std::rc::Rc; use syn::punctuated::Punctuated; use syn::visit::Visit; use syn::visit_mut::VisitMut; -#[derive(Debug, FromMeta)] -pub(crate) struct FnInputParams { - expose_positional_fn: Option>, - - #[darling(flatten)] - base: BuilderParams, -} - -#[derive(Debug, Default)] -struct ExposePositionalFnParams { - name: Option, - vis: Option, -} - -impl FromMeta for ExposePositionalFnParams { - fn from_meta(meta: &syn::Meta) -> Result { - match meta { - syn::Meta::Path(_) => { - return Ok(Self::default()); - } - syn::Meta::NameValue(meta) => { - let val = &meta.value; - let name = syn::parse2(quote!(#val))?; - - return Ok(Self { name, vis: None }); - } - syn::Meta::List(_) => {} - } - - #[derive(Debug, FromMeta)] - struct Full { - name: Option, - vis: Option, - } - - let full = Full::from_meta(meta)?; - - let me = Self { - name: full.name, - vis: full.vis, - }; +pub(crate) struct FnInputCtx<'a> { + namespace: &'a GenericsNamespace, + fn_item: SyntaxVariant, + impl_ctx: Option>, + config: TopLevelConfig, - Ok(me) - } + start_fn: StartFnParams, + self_ty_prefix: Option, } -pub(crate) struct FnInputCtx<'a> { +pub(crate) struct FnInputCtxParams<'a> { pub(crate) namespace: &'a GenericsNamespace, pub(crate) fn_item: SyntaxVariant, pub(crate) impl_ctx: Option>, - pub(crate) params: FnInputParams, + pub(crate) config: TopLevelConfig, } pub(crate) struct ImplCtx { @@ -79,20 +41,85 @@ pub(crate) struct ImplCtx { pub(crate) allow_attrs: Vec, } -impl FnInputCtx<'_> { - fn self_ty_prefix(&self) -> Option { - let prefix = self - .impl_ctx - .as_deref()? - .self_ty - .as_path()? - .path - .segments - .last()? - .ident - .to_string(); - - Some(prefix) +impl<'a> FnInputCtx<'a> { + pub(crate) fn new(params: FnInputCtxParams<'a>) -> Self { + let start_fn = params.config.start_fn.clone(); + + let start_fn_ident = start_fn + .name + .map(SpannedKey::into_value) + .unwrap_or_else(|| { + let fn_ident = ¶ms.fn_item.norm.sig.ident; + + // Special case for the method named `new`. We rename it to `builder` + // since this is the name that is conventionally used by starting + // function in the builder pattern. We also want to make + // the `#[builder]` attribute on the method `new` fully compatible + // with deriving a builder from a struct. + if params.impl_ctx.is_some() && fn_ident == "new" { + syn::Ident::new("builder", fn_ident.span()) + } else { + fn_ident.clone() + } + }); + + let start_fn = StartFnParams { + ident: start_fn_ident, + + vis: start_fn.vis.map(SpannedKey::into_value), + + docs: start_fn + .docs + .map(SpannedKey::into_value) + .unwrap_or_else(|| { + params + .fn_item + .norm + .attrs + .iter() + .filter(|attr| attr.is_doc_expr()) + .cloned() + .collect() + }), + + // Override on the start fn to use the generics from the + // target function itself. We must not duplicate the generics + // from the impl block here + generics: Some(Generics::new( + params + .fn_item + .norm + .sig + .generics + .params + .iter() + .cloned() + .collect(), + params.fn_item.norm.sig.generics.where_clause.clone(), + )), + }; + + let self_ty_prefix = params.impl_ctx.as_deref().and_then(|impl_ctx| { + let prefix = impl_ctx + .self_ty + .as_path()? + .path + .segments + .last()? + .ident + .to_string(); + + Some(prefix) + }); + + Self { + namespace: params.namespace, + fn_item: params.fn_item, + impl_ctx: params.impl_ctx, + config: params.config, + self_ty_prefix, + start_fn, + } } fn assoc_method_ctx(&self) -> Result> { @@ -163,79 +190,21 @@ impl FnInputCtx<'_> { Generics::new(params, where_clause) } - fn builder_ident(&self) -> syn::Ident { - let user_override = self.params.base.builder_type.name.as_deref(); - - if let Some(user_override) = user_override { - return user_override.clone(); - } - - if self.is_method_new() { - return format_ident!("{}Builder", self.self_ty_prefix().unwrap_or_default()); - } - - let pascal_case_fn = self.fn_item.norm.sig.ident.snake_to_pascal_case(); - - format_ident!( - "{}{pascal_case_fn}Builder", - self.self_ty_prefix().unwrap_or_default(), - ) - } - pub(crate) fn adapted_fn(&self) -> Result { let mut orig = self.fn_item.orig.clone(); - let params = self.params.expose_positional_fn.as_ref(); - - orig.vis = params - .map(|params| { - params - .vis - .clone() - // If exposing of positional fn is enabled without an explicit - // visibility, then just use the visibility of the original function. - .unwrap_or_else(|| self.fn_item.norm.vis.clone()) - }) - // By default we change the positional function's visibility to private - // to avoid exposing it to the surrounding code. The surrounding code is - // supposed to use this function through the builder only. - // - // Not that this doesn't guarantee that adjacent code in this module can't - // access the function, therefore we rename it below. - .unwrap_or(syn::Visibility::Inherited); - - let orig_ident = orig.sig.ident.clone(); - - if let Some(params) = params { - let has_no_value = matches!( - params.as_ref(), - ExposePositionalFnParams { - name: None, - vis: None, - } - ); - - if has_no_value && !self.is_method_new() { + if let Some(name) = self.config.start_fn.name.as_deref() { + if *name == orig.sig.ident { bail!( - ¶ms.span(), - "Positional function identifier is required. It must be \ - specified with `#[builder(expose_positional_fn = function_name_here)]`" + &name, + "the starting function name must be different from the name \ + of the positional function under the #[builder] attribute" ) } - } + } else { + // By default the original positional function becomes hidden. + orig.vis = syn::Visibility::Inherited; - orig.sig.ident = params - .and_then(|params| { - params - .name - .clone() - // We treat `new` method specially. In this case we already know the best - // default name for the positional function, which is `new` itself. - .or_else(|| self.is_method_new().then(|| orig.sig.ident)) - }) - // By default we don't want to expose the positional function, so we - // hide it under a generated name to avoid name conflicts. - // // We don't use a random name here because the name of this function // can be used by other macros that may need a stable identifier. // For example, if `#[tracing::instrument]` is placed on the function, @@ -243,7 +212,22 @@ impl FnInputCtx<'_> { // may be indexed in some logs database (e.g. Grafana Loki). If the name // of the span changes the DB index may grow and also log queries won't // be stable. - .unwrap_or_else(|| format_ident!("__orig_{}", orig_ident.raw_name())); + orig.sig.ident = format_ident!("__orig_{}", orig.sig.ident.raw_name()); + + // Remove all doc comments from the function itself to avoid docs duplication + // which may lead to duplicating doc tests, which in turn implies repeated doc + // tests execution, which means worse tests performance. + // + // We don't do this for the case when the positional function is exposed + // alongside the builder which implies that the docs should be visible + // as the function itself is visible. + orig.attrs.retain(|attr| !attr.is_doc_expr()); + + orig.attrs.extend([syn::parse_quote!(#[doc(hidden)])]); + } + + // Remove any `#[builder]` attributes that were meant for this proc macro. + orig.attrs.retain(|attr| !attr.path().is_ident("builder")); // Remove all doc comments attributes from function arguments, because they are // not valid in that position in regular Rust code. The cool trick is that they @@ -258,31 +242,6 @@ impl FnInputCtx<'_> { .retain(|attr| !attr.is_doc_expr() && !attr.path().is_ident("builder")); } - // Remove all doc comments from the function itself to avoid docs duplication - // which may lead to duplicating doc tests, which in turn implies repeated doc - // tests execution, which means worse tests performance. - // - // Also remove any `#[builder]` attributes that were meant for this proc macro. - orig.attrs - .retain(|attr| !attr.is_doc_expr() && !attr.path().is_ident("builder")); - - let prefix = self - .self_ty_prefix() - .map(|self_ty_prefix| format!("{self_ty_prefix}::")) - .unwrap_or_default(); - - let builder_entry_fn_link = format!("{prefix}{orig_ident}",); - - let doc = format!( - "Positional function equivalent of [`{builder_entry_fn_link}()`].\n\ - See its docs for details.", - ); - - orig.attrs.push(syn::parse_quote!(#[doc = #doc])); - - if self.params.expose_positional_fn.is_none() { - orig.attrs.extend([syn::parse_quote!(#[doc(hidden)])]); - } orig.attrs.push(syn::parse_quote!(#[allow( // It's fine if there are too many positional arguments in the function // because the whole purpose of this macro is to fight with this problem @@ -299,10 +258,6 @@ impl FnInputCtx<'_> { Ok(orig) } - fn is_method_new(&self) -> bool { - self.impl_ctx.is_some() && self.fn_item.norm.sig.ident == "new" - } - pub(crate) fn into_builder_gen_ctx(self) -> Result { let assoc_method_ctx = self.assoc_method_ctx()?; @@ -310,7 +265,7 @@ impl FnInputCtx<'_> { let explanation = "\ but #[bon] attribute is absent on top of the impl block; this \ additional #[bon] attribute on the impl block is required for \ - the macro to see the type of `Self` and properly generate + the macro to see the type of `Self` and properly generate \ the builder struct definition adjacently to the impl block."; if let Some(receiver) = &self.fn_item.orig.sig.receiver() { @@ -330,8 +285,6 @@ impl FnInputCtx<'_> { } } - let builder_ident = self.builder_ident(); - let members = self .fn_item .apply_ref(|fn_item| fn_item.sig.inputs.iter().filter_map(syn::FnArg::as_typed)) @@ -359,7 +312,7 @@ impl FnInputCtx<'_> { }) .collect::>>()?; - let members = Member::from_raw(&self.params.base.on, MemberOrigin::FnArg, members)?; + let members = Member::from_raw(&self.config.on, MemberOrigin::FnArg, members)?; let generics = self.generics(); @@ -368,27 +321,17 @@ impl FnInputCtx<'_> { impl_ctx: self.impl_ctx.clone(), }; - let is_method_new = self.is_method_new(); - - // Special case for `new` methods. We rename them to `builder` - // since this is the name that is used in the builder pattern - let start_fn_ident = if is_method_new { - format_ident!("builder") - } else { - self.fn_item.norm.sig.ident.clone() - }; - - let ItemParams { + let ItemSigConfig { name: finish_fn_ident, vis: finish_fn_vis, docs: finish_fn_docs, - } = self.params.base.finish_fn; + } = self.config.finish_fn; let finish_fn_ident = finish_fn_ident .map(SpannedKey::into_value) .unwrap_or_else(|| { - // For `new` methods the `build` finisher is more conventional - if is_method_new { + // For `builder` methods the `build` finisher is more conventional + if self.impl_ctx.is_some() && self.start_fn.ident == "builder" { format_ident!("build") } else { format_ident!("call") @@ -429,53 +372,56 @@ impl FnInputCtx<'_> { .chain(fn_allows) .collect(); - let start_fn = StartFnParams { - ident: start_fn_ident, + let builder_ident = || { + let user_override = self.config.builder_type.name.map(SpannedKey::into_value); - // No override for visibility for the start fn is provided here. - // It's supposed to be the same as the original function's visibility. - vis: None, - - attrs: self - .fn_item - .norm - .attrs - .into_iter() - .filter(<_>::is_doc_expr) - .collect(), - - // Override on the start fn to use the the generics from the - // target function itself. We don't need to duplicate the generics - // from the impl block here. - generics: Some(Generics::new( - Vec::from_iter(self.fn_item.norm.sig.generics.params), - self.fn_item.norm.sig.generics.where_clause, - )), + if let Some(user_override) = user_override { + return user_override; + } + + let ty_prefix = self.self_ty_prefix.unwrap_or_default(); + + // A special case for the starting function named `builder`. + // We don't insert the `Builder` suffix in this case because + // this special case should be compatible with deriving + // a builder from a struct. + // + // We can arrive inside of this branch only if the function under + // the macro is called `new` or `builder` without `start_fn` + // name override, or if the `start_fn = builder/start_fn(name = builder)` + // is specified in the macro invocation explicitly. + if self.impl_ctx.is_some() && self.start_fn.ident == "builder" { + return format_ident!("{ty_prefix}Builder"); + } + + let pascal_case_fn = self.fn_item.norm.sig.ident.snake_to_pascal_case(); + + format_ident!("{ty_prefix}{pascal_case_fn}Builder") }; - let builder_type = self.params.base.builder_type; let builder_type = BuilderTypeParams { - ident: builder_ident, - derives: self.params.base.derive, - docs: builder_type.docs.map(SpannedKey::into_value), - vis: builder_type.vis.map(SpannedKey::into_value), + ident: builder_ident(), + derives: self.config.derive, + docs: self.config.builder_type.docs.map(SpannedKey::into_value), + vis: self.config.builder_type.vis.map(SpannedKey::into_value), }; BuilderGenCtx::new(BuilderGenCtxParams { + bon: self.config.bon, namespace: Cow::Borrowed(self.namespace), members, allow_attrs, - on_params: self.params.base.on, + on: self.config.on, assoc_method_ctx, generics, orig_item_vis: self.fn_item.norm.vis, builder_type, - state_mod: self.params.base.state_mod, - start_fn, + state_mod: self.config.state_mod, + start_fn: self.start_fn, finish_fn, }) } diff --git a/bon-macros/src/builder/builder_gen/input_struct.rs b/bon-macros/src/builder/builder_gen/input_struct.rs index 45aef731..de458114 100644 --- a/bon-macros/src/builder/builder_gen/input_struct.rs +++ b/bon-macros/src/builder/builder_gen/input_struct.rs @@ -1,76 +1,57 @@ -use super::builder_params::BuilderParams; use super::models::FinishFnParams; +use super::top_level_config::TopLevelConfig; use super::{ AssocMethodCtx, BuilderGenCtx, FinishFnBody, Generics, Member, MemberOrigin, RawMember, }; use crate::builder::builder_gen::models::{BuilderGenCtxParams, BuilderTypeParams, StartFnParams}; use crate::normalization::{GenericsNamespace, SyntaxVariant}; -use crate::parsing::{ItemParams, ItemParamsParsing, SpannedKey}; +use crate::parsing::{ItemSigConfig, SpannedKey}; use crate::util::prelude::*; use darling::FromMeta; use std::borrow::Cow; use syn::visit::Visit; use syn::visit_mut::VisitMut; -#[derive(Debug, FromMeta)] -pub(crate) struct StructInputParams { - #[darling(flatten)] - base: BuilderParams, - - #[darling(default, with = parse_start_fn)] - start_fn: ItemParams, -} - -fn parse_start_fn(meta: &syn::Meta) -> Result { - ItemParamsParsing { - meta, - reject_self_mentions: None, - } - .parse() -} - -impl StructInputParams { - fn parse(item_struct: &syn::ItemStruct) -> Result { - let meta = item_struct - .attrs - .iter() - .filter(|attr| attr.path().is_ident("builder")) - .map(|attr| { - let meta = match &attr.meta { - syn::Meta::List(meta) => meta, - syn::Meta::Path(_) => bail!( - &attr.meta, - "this empty `#[builder]` attribute is redundant; remove it" - ), - syn::Meta::NameValue(_) => bail!( - &attr.meta, - "`#[builder = ...]` syntax is unsupported; use `#[builder(...)]` instead" - ), - }; - - crate::parsing::require_non_empty_paren_meta_list_or_name_value(&attr.meta)?; - - let meta = darling::ast::NestedMeta::parse_meta_list(meta.tokens.clone())?; - - Ok(meta) - }) - .collect::>>()? - .into_iter() - .concat(); +fn parse_top_level_config(item_struct: &syn::ItemStruct) -> Result { + let meta = item_struct + .attrs + .iter() + .filter(|attr| attr.path().is_ident("builder")) + .map(|attr| { + let meta = match &attr.meta { + syn::Meta::List(meta) => meta, + syn::Meta::Path(_) => bail!( + &attr.meta, + "this empty `#[builder]` attribute is redundant; remove it" + ), + syn::Meta::NameValue(_) => bail!( + &attr.meta, + "`#[builder = ...]` syntax is unsupported; use `#[builder(...)]` instead" + ), + }; + + crate::parsing::require_non_empty_paren_meta_list_or_name_value(&attr.meta)?; + + let meta = darling::ast::NestedMeta::parse_meta_list(meta.tokens.clone())?; + + Ok(meta) + }) + .collect::>>()? + .into_iter() + .concat(); - Self::from_list(&meta) - } + TopLevelConfig::from_list(&meta) } pub(crate) struct StructInputCtx { struct_item: SyntaxVariant, - params: StructInputParams, + config: TopLevelConfig, struct_ty: syn::Type, } impl StructInputCtx { pub(crate) fn new(orig_struct: syn::ItemStruct) -> Result { - let params = StructInputParams::parse(&orig_struct)?; + let params = parse_top_level_config(&orig_struct)?; let generic_args = orig_struct .generics @@ -98,7 +79,7 @@ impl StructInputCtx { Ok(Self { struct_item, - params, + config: params, struct_ty, }) } @@ -138,7 +119,7 @@ impl StructInputCtx { }) .collect::>>()?; - let members = Member::from_raw(&self.params.base.on, MemberOrigin::StructField, members)?; + let members = Member::from_raw(&self.config.on, MemberOrigin::StructField, members)?; let generics = Generics::new( self.struct_item @@ -155,21 +136,21 @@ impl StructInputCtx { struct_ident: self.struct_item.norm.ident.clone(), }; - let ItemParams { + let ItemSigConfig { name: start_fn_ident, vis: start_fn_vis, docs: start_fn_docs, - } = self.params.start_fn; + } = self.config.start_fn; let start_fn_ident = start_fn_ident .map(SpannedKey::into_value) .unwrap_or_else(|| syn::Ident::new("builder", self.struct_item.norm.ident.span())); - let ItemParams { + let ItemSigConfig { name: finish_fn_ident, vis: finish_fn_vis, docs: finish_fn_docs, - } = self.params.base.finish_fn; + } = self.config.finish_fn; let finish_fn_ident = finish_fn_ident .map(SpannedKey::into_value) @@ -209,7 +190,7 @@ impl StructInputCtx { let start_fn = StartFnParams { ident: start_fn_ident, vis: start_fn_vis.map(SpannedKey::into_value), - attrs: start_fn_docs, + docs: start_fn_docs, generics: None, }; @@ -227,14 +208,14 @@ impl StructInputCtx { .collect(); let builder_type = { - let ItemParams { name, vis, docs } = self.params.base.builder_type; + let ItemSigConfig { name, vis, docs } = self.config.builder_type; let builder_ident = name.map(SpannedKey::into_value).unwrap_or_else(|| { format_ident!("{}Builder", self.struct_item.norm.ident.raw_name()) }); BuilderTypeParams { - derives: self.params.base.derive, + derives: self.config.derive, ident: builder_ident, docs: docs.map(SpannedKey::into_value), vis: vis.map(SpannedKey::into_value), @@ -245,19 +226,20 @@ impl StructInputCtx { namespace.visit_item_struct(&self.struct_item.orig); BuilderGenCtx::new(BuilderGenCtxParams { + bon: self.config.bon, namespace: Cow::Owned(namespace), members, allow_attrs, - on_params: self.params.base.on, + on: self.config.on, assoc_method_ctx, generics, orig_item_vis: self.struct_item.norm.vis, builder_type, - state_mod: self.params.base.state_mod, + state_mod: self.config.state_mod, start_fn, finish_fn, }) diff --git a/bon-macros/src/builder/builder_gen/member/params/blanket.rs b/bon-macros/src/builder/builder_gen/member/config/blanket.rs similarity index 68% rename from bon-macros/src/builder/builder_gen/member/params/blanket.rs rename to bon-macros/src/builder/builder_gen/member/config/blanket.rs index e0124483..3d1e4b4d 100644 --- a/bon-macros/src/builder/builder_gen/member/params/blanket.rs +++ b/bon-macros/src/builder/builder_gen/member/config/blanket.rs @@ -1,6 +1,6 @@ -use super::MemberParams; -use crate::builder::builder_gen::builder_params::OnParams; +use super::MemberConfig; use crate::builder::builder_gen::member::MemberOrigin; +use crate::builder::builder_gen::top_level_config::OnConfig; use crate::util::prelude::*; use std::fmt; @@ -19,25 +19,25 @@ impl fmt::Display for BlanketParamName { } impl BlanketParamName { - fn value_in_on_params(&self, on_params: &OnParams) -> darling::util::Flag { + fn value_in_on_config(&self, cfg: &OnConfig) -> darling::util::Flag { match self { - Self::Into => on_params.into, - Self::Overwritable => on_params.overwritable, + Self::Into => cfg.into, + Self::Overwritable => cfg.overwritable, } } - fn value_in_member_params(&self, member_params: &MemberParams) -> darling::util::Flag { + fn value_in_member_config(&self, cfg: &MemberConfig) -> darling::util::Flag { match self { - Self::Into => member_params.into, - Self::Overwritable => member_params.overwritable, + Self::Into => cfg.into, + Self::Overwritable => cfg.overwritable, } } } pub(crate) struct EvalBlanketFlagParam<'a> { - pub(crate) on_params: &'a [OnParams], + pub(crate) on: &'a [OnConfig], pub(crate) param_name: BlanketParamName, - pub(crate) member_params: &'a MemberParams, + pub(crate) member_config: &'a MemberConfig, pub(crate) scrutinee: &'a syn::Type, pub(crate) origin: MemberOrigin, } @@ -45,24 +45,24 @@ pub(crate) struct EvalBlanketFlagParam<'a> { impl EvalBlanketFlagParam<'_> { pub(crate) fn eval(self) -> Result { let Self { - on_params, + on, param_name, - member_params, + member_config, scrutinee, origin, } = self; - let verdict_from_on_params = on_params + let verdict_from_on = on .iter() .map(|params| Ok((params, scrutinee.matches(¶ms.type_pattern)?))) .collect::>>()? .into_iter() .filter(|(_, matched)| *matched) - .map(|(params, _)| param_name.value_in_on_params(params)) + .map(|(params, _)| param_name.value_in_on_config(params)) .find(darling::util::Flag::is_present); - let value_in_member = param_name.value_in_member_params(member_params); - let flag = match (verdict_from_on_params, value_in_member.is_present()) { + let value_in_member = param_name.value_in_member_config(member_config); + let flag = match (verdict_from_on, value_in_member.is_present()) { (Some(_), true) => { bail!( &value_in_member.span(), diff --git a/bon-macros/src/builder/builder_gen/member/params/mod.rs b/bon-macros/src/builder/builder_gen/member/config/mod.rs similarity index 98% rename from bon-macros/src/builder/builder_gen/member/params/mod.rs rename to bon-macros/src/builder/builder_gen/member/config/mod.rs index 3beb71c2..e2c9a552 100644 --- a/bon-macros/src/builder/builder_gen/member/params/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/config/mod.rs @@ -13,7 +13,7 @@ use std::fmt; #[derive(Debug, darling::FromAttributes)] #[darling(attributes(builder))] -pub(crate) struct MemberParams { +pub(crate) struct MemberConfig { /// Enables an `Into` conversion for the setter method. pub(crate) into: darling::util::Flag, @@ -36,7 +36,7 @@ pub(crate) struct MemberParams { /// Configurations for the setter methods. #[darling(with = crate::parsing::parse_non_empty_paren_meta_list)] - pub(crate) setters: Option, + pub(crate) setters: Option, /// Where to place the member in the generated builder methods API. /// By default the member is treated like a named parameter that @@ -97,7 +97,7 @@ impl fmt::Display for ParamName { } } -impl MemberParams { +impl MemberConfig { fn validate_mutually_exclusive( &self, attr_name: ParamName, diff --git a/bon-macros/src/builder/builder_gen/member/params/setter_closure.rs b/bon-macros/src/builder/builder_gen/member/config/setter_closure.rs similarity index 100% rename from bon-macros/src/builder/builder_gen/member/params/setter_closure.rs rename to bon-macros/src/builder/builder_gen/member/config/setter_closure.rs diff --git a/bon-macros/src/builder/builder_gen/member/params/setters.rs b/bon-macros/src/builder/builder_gen/member/config/setters.rs similarity index 75% rename from bon-macros/src/builder/builder_gen/member/params/setters.rs rename to bon-macros/src/builder/builder_gen/member/config/setters.rs index ea9a51b2..72a9c1c2 100644 --- a/bon-macros/src/builder/builder_gen/member/params/setters.rs +++ b/bon-macros/src/builder/builder_gen/member/config/setters.rs @@ -1,11 +1,11 @@ -use crate::parsing::{ItemParams, ItemParamsParsing, SpannedKey}; +use crate::parsing::{ItemSigConfig, ItemSigConfigParsing, SpannedKey}; use crate::util::prelude::*; use darling::FromMeta; const DOCS_CONTEXT: &str = "builder struct's impl block"; -fn parse_setter_fn(meta: &syn::Meta) -> Result> { - let params = ItemParamsParsing { +fn parse_setter_fn(meta: &syn::Meta) -> Result> { + let params = ItemSigConfigParsing { meta, reject_self_mentions: Some(DOCS_CONTEXT), } @@ -19,7 +19,7 @@ fn parse_docs(meta: &syn::Meta) -> Result>> { } #[derive(Debug, FromMeta)] -pub(crate) struct SettersParams { +pub(crate) struct SettersConfig { pub(crate) name: Option>, pub(crate) vis: Option>, @@ -27,22 +27,22 @@ pub(crate) struct SettersParams { pub(crate) docs: Option>>, #[darling(flatten)] - pub(crate) fns: SettersFnParams, + pub(crate) fns: SettersFnsConfig, } #[derive(Debug, FromMeta)] -pub(crate) struct SettersFnParams { +pub(crate) struct SettersFnsConfig { /// Config for the setter that accepts the value of type T for a member of /// type `Option` or with `#[builder(default)]`. /// /// By default, it's named `{member}` without any prefix or suffix. #[darling(default, with = parse_setter_fn, map = Some)] - pub(crate) some_fn: Option>, + pub(crate) some_fn: Option>, /// The setter that accepts the value of type `Option` for a member of /// type `Option` or with `#[builder(default)]`. /// /// By default, it's named `maybe_{member}`. #[darling(default, with = parse_setter_fn, map = Some)] - pub(crate) option_fn: Option>, + pub(crate) option_fn: Option>, } diff --git a/bon-macros/src/builder/builder_gen/member/into_conversion.rs b/bon-macros/src/builder/builder_gen/member/into_conversion.rs index bffb259f..6da745bc 100644 --- a/bon-macros/src/builder/builder_gen/member/into_conversion.rs +++ b/bon-macros/src/builder/builder_gen/member/into_conversion.rs @@ -1,13 +1,13 @@ -use super::params::{BlanketParamName, EvalBlanketFlagParam}; +use super::config::{BlanketParamName, EvalBlanketFlagParam}; use super::{NamedMember, PositionalFnArgMember}; -use crate::builder::builder_gen::builder_params::OnParams; +use crate::builder::builder_gen::top_level_config::OnConfig; use crate::util::prelude::*; impl NamedMember { - pub(super) fn merge_param_into(&mut self, on_params: &[OnParams]) -> Result { + pub(super) fn merge_config_into(&mut self, on: &[OnConfig]) -> Result { // `with` is mutually exclusive with `into`. So there is nothing to merge here // if `with` is present. - if self.params.with.is_some() { + if self.config.with.is_some() { return Ok(()); } @@ -17,10 +17,10 @@ impl NamedMember { // if `Option` is used or the member of type `T` has `#[builder(default)]` on it. let scrutinee = self.underlying_orig_ty(); - self.params.into = EvalBlanketFlagParam { - on_params, + self.config.into = EvalBlanketFlagParam { + on, param_name: BlanketParamName::Into, - member_params: &self.params, + member_config: &self.config, scrutinee, origin: self.origin, } @@ -31,16 +31,16 @@ impl NamedMember { } impl PositionalFnArgMember { - pub(crate) fn merge_param_into(&mut self, on_params: &[OnParams]) -> Result { + pub(crate) fn merge_config_into(&mut self, on: &[OnConfig]) -> Result { // Positional members are never optional. Users must always specify them, so there // is no need for us to look into the `Option` generic parameter, because the // `Option` itself is the target of the into conversion, not the `T` inside it. let scrutinee = self.ty.orig.as_ref(); - self.params.into = EvalBlanketFlagParam { - on_params, + self.config.into = EvalBlanketFlagParam { + on, param_name: BlanketParamName::Into, - member_params: &self.params, + member_config: &self.config, scrutinee, origin: self.origin, } @@ -53,7 +53,7 @@ impl PositionalFnArgMember { let ty = &self.ty.norm; let ident = &self.ident; - if self.params.into.is_present() { + if self.config.into.is_present() { quote! { #ident: impl Into<#ty> } } else { quote! { #ident: #ty } @@ -63,7 +63,7 @@ impl PositionalFnArgMember { pub(crate) fn init_expr(&self) -> TokenStream { let ident = &self.ident; - if self.params.into.is_present() { + if self.config.into.is_present() { quote! { Into::into(#ident) } diff --git a/bon-macros/src/builder/builder_gen/member/mod.rs b/bon-macros/src/builder/builder_gen/member/mod.rs index 976fc41f..1ad23689 100644 --- a/bon-macros/src/builder/builder_gen/member/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/mod.rs @@ -1,16 +1,16 @@ +mod config; mod into_conversion; mod named; -mod params; +pub(crate) use config::*; pub(crate) use named::*; -pub(crate) use params::*; -use super::builder_params::OnParams; +use super::top_level_config::OnConfig; use crate::normalization::SyntaxVariant; use crate::parsing::SpannedKey; use crate::util::prelude::*; +use config::MemberConfig; use darling::FromAttributes; -use params::MemberParams; use std::fmt; #[derive(Debug, Clone, Copy)] @@ -71,7 +71,7 @@ pub(crate) struct PositionalFnArgMember { pub(crate) ty: SyntaxVariant>, /// Parameters configured by the user explicitly via attributes - pub(crate) params: MemberParams, + pub(crate) config: MemberConfig, } /// Member that was skipped by the user with `#[builder(skip)]` @@ -99,7 +99,7 @@ impl Member { // (there is an other lint that checks for this). #[allow(single_use_lifetimes)] pub(crate) fn from_raw<'a>( - on_params: &[OnParams], + on: &[OnConfig], origin: MemberOrigin, members: impl IntoIterator>, ) -> Result> { @@ -114,9 +114,9 @@ impl Member { } } - let params = MemberParams::from_attributes(member.attrs)?; - params.validate(origin)?; - Ok((member, params)) + let config = MemberConfig::from_attributes(member.attrs)?; + config.validate(origin)?; + Ok((member, config)) }) .collect::>>()? .into_iter() @@ -125,31 +125,31 @@ impl Member { let mut output = vec![]; for index in 0.. { - let next = members.next_if(|(_, params)| params.start_fn.is_present()); - let (member, params) = match next { + let next = members.next_if(|(_, meta)| meta.start_fn.is_present()); + let (member, config) = match next { Some(item) => item, None => break, }; - let base = PositionalFnArgMember::new(origin, member, on_params, params)?; + let base = PositionalFnArgMember::new(origin, member, on, config)?; output.push(Self::StartFnArg(StartFnArgMember { base, index: index.into(), })); } - while let Some((member, params)) = - members.next_if(|(_, params)| params.finish_fn.is_present()) + while let Some((member, config)) = + members.next_if(|(_, config)| config.finish_fn.is_present()) { - let member = PositionalFnArgMember::new(origin, member, on_params, params)?; + let member = PositionalFnArgMember::new(origin, member, on, config)?; output.push(Self::FinishFnArg(member)); } let mut named_count = 0; - for (member, params) in members { + for (member, config) in members { let RawMember { attrs, ident, ty } = member; - if let Some(value) = params.skip { + if let Some(value) = config.skip { output.push(Self::Skipped(SkippedMember { ident, norm_ty: ty.norm, @@ -161,7 +161,7 @@ impl Member { let active_flag = |flag: darling::util::Flag| flag.is_present().then(|| flag); let incorrect_order = - active_flag(params.finish_fn).or_else(|| active_flag(params.start_fn)); + active_flag(config.finish_fn).or_else(|| active_flag(config.start_fn)); if let Some(attr) = incorrect_order { bail!( @@ -189,13 +189,13 @@ impl Member { let mut member = NamedMember { index: named_count.into(), origin, - name: MemberName::new(ident, ¶ms), + name: MemberName::new(ident, &config), ty, - params, + config, docs, }; - member.merge_on_params(on_params)?; + member.merge_on_config(on)?; member.validate()?; output.push(Self::Named(member)); @@ -251,8 +251,8 @@ impl PositionalFnArgMember { fn new( origin: MemberOrigin, member: RawMember<'_>, - on_params: &[OnParams], - params: MemberParams, + on: &[OnConfig], + config: MemberConfig, ) -> Result { let RawMember { attrs: _, @@ -264,10 +264,10 @@ impl PositionalFnArgMember { origin, ident, ty, - params, + config, }; - me.merge_param_into(on_params)?; + me.merge_config_into(on)?; Ok(me) } diff --git a/bon-macros/src/builder/builder_gen/member/named.rs b/bon-macros/src/builder/builder_gen/member/named.rs index 04e4a3c4..6da9e658 100644 --- a/bon-macros/src/builder/builder_gen/member/named.rs +++ b/bon-macros/src/builder/builder_gen/member/named.rs @@ -1,9 +1,9 @@ -use super::params::MemberParams; -use super::{params, MemberOrigin}; -use crate::builder::builder_gen::builder_params::OnParams; -use crate::builder::builder_gen::member::params::SettersFnParams; +use super::config::MemberConfig; +use super::{config, MemberOrigin}; +use crate::builder::builder_gen::member::config::SettersFnsConfig; +use crate::builder::builder_gen::top_level_config::OnConfig; use crate::normalization::SyntaxVariant; -use crate::parsing::{ItemParams, SpannedKey}; +use crate::parsing::{ItemSigConfig, SpannedKey}; use crate::util::prelude::*; #[derive(Debug)] @@ -40,8 +40,8 @@ pub(crate) struct MemberName { } impl MemberName { - pub(crate) fn new(orig: syn::Ident, params: &MemberParams) -> Self { - let snake = params.name.clone().unwrap_or_else(|| { + pub(crate) fn new(orig: syn::Ident, config: &MemberConfig) -> Self { + let snake = config.name.clone().unwrap_or_else(|| { let orig_str = orig.to_string(); let norm = orig_str // Remove the leading underscore from the member name since it's used @@ -89,12 +89,12 @@ pub(crate) struct NamedMember { pub(crate) ty: SyntaxVariant>, /// Parameters configured by the user explicitly via attributes - pub(crate) params: MemberParams, + pub(crate) config: MemberConfig, } impl NamedMember { pub(super) fn validate(&self) -> Result { - if let Some(default) = &self.params.default { + if let Some(default) = &self.config.default { if self.is_special_option_ty() { bail!( &default.key, @@ -105,7 +105,7 @@ impl NamedMember { } let member_docs_not_copied = self - .params + .config .setters .as_ref() .map(|setters| { @@ -113,12 +113,12 @@ impl NamedMember { return true; } - let SettersFnParams { some_fn, option_fn } = &setters.fns; + let SettersFnsConfig { some_fn, option_fn } = &setters.fns; matches!( (some_fn.as_deref(), option_fn.as_deref()), ( - Some(ItemParams { docs: Some(_), .. }), - Some(ItemParams { docs: Some(_), .. }) + Some(ItemSigConfig { docs: Some(_), .. }), + Some(ItemSigConfig { docs: Some(_), .. }) ) ) }) @@ -131,11 +131,11 @@ impl NamedMember { )?; } - self.validate_setters_params()?; + self.validate_setters_config()?; - if self.params.transparent.is_present() && !self.ty.norm.is_option() { + if self.config.transparent.is_present() && !self.ty.norm.is_option() { bail!( - &self.params.transparent.span(), + &self.config.transparent.span(), "`#[builder(transparent)]` can only be applied to members of \ type `Option` to disable their special handling", ); @@ -144,14 +144,14 @@ impl NamedMember { Ok(()) } - fn validate_setters_params(&self) -> Result { - let setters = match &self.params.setters { + fn validate_setters_config(&self) -> Result { + let setters = match &self.config.setters { Some(setters) => setters, None => return Ok(()), }; if self.is_required() { - let SettersFnParams { some_fn, option_fn } = &setters.fns; + let SettersFnsConfig { some_fn, option_fn } = &setters.fns; let unexpected_setter = option_fn.as_ref().or(some_fn.as_ref()); @@ -165,16 +165,16 @@ impl NamedMember { } } - if let SettersFnParams { + if let SettersFnsConfig { some_fn: Some(some_fn), option_fn: Some(option_fn), } = &setters.fns { let setter_fns = &[some_fn, option_fn]; - Self::validate_unused_setters_cfg(setter_fns, &setters.name, |params| ¶ms.name)?; - Self::validate_unused_setters_cfg(setter_fns, &setters.vis, |params| ¶ms.vis)?; - Self::validate_unused_setters_cfg(setter_fns, &setters.docs, |params| ¶ms.docs)?; + Self::validate_unused_setters_cfg(setter_fns, &setters.name, |config| &config.name)?; + Self::validate_unused_setters_cfg(setter_fns, &setters.vis, |config| &config.vis)?; + Self::validate_unused_setters_cfg(setter_fns, &setters.docs, |config| &config.docs)?; } Ok(()) @@ -183,9 +183,9 @@ impl NamedMember { // Lint from nightly. `&Option` is used to reduce syntax at the call site #[allow(unknown_lints, clippy::ref_option)] fn validate_unused_setters_cfg( - overrides: &[&SpannedKey], + overrides: &[&SpannedKey], config: &Option>, - get_val: impl Fn(&ItemParams) -> &Option>, + get_val: impl Fn(&ItemSigConfig) -> &Option>, ) -> Result { let config = match config { Some(config) => config, @@ -217,13 +217,13 @@ impl NamedMember { /// Returns `true` if this member is of `Option<_>` type, but returns `false` /// if `#[builder(transparent)]` is set. pub(crate) fn is_special_option_ty(&self) -> bool { - !self.params.transparent.is_present() && self.ty.norm.is_option() + !self.config.transparent.is_present() && self.ty.norm.is_option() } /// Returns `false` if the member has a default value. It means this member /// is required to be set before building can be finished. pub(crate) fn is_required(&self) -> bool { - self.params.default.is_none() && !self.is_special_option_ty() + self.config.default.is_none() && !self.is_special_option_ty() } /// A stateful member is the one that has a corresponding associated type in @@ -231,7 +231,7 @@ impl NamedMember { /// member was set or not. This is necessary to make sure all members without /// default values are set before building can be finished. pub(crate) fn is_stateful(&self) -> bool { - self.is_required() || !self.params.overwritable.is_present() + self.is_required() || !self.config.overwritable.is_present() } /// Returns the normalized type of the member stripping the `Option<_>` @@ -247,7 +247,7 @@ impl NamedMember { } fn underlying_ty<'m>(&'m self, ty: &'m syn::Type) -> &'m syn::Type { - if self.params.transparent.is_present() || self.params.default.is_some() { + if self.config.transparent.is_present() || self.config.default.is_some() { ty } else { ty.option_type_param().unwrap_or(ty) @@ -258,16 +258,16 @@ impl NamedMember { self.index == other.index } - pub(crate) fn merge_on_params(&mut self, on_params: &[OnParams]) -> Result { - self.merge_param_into(on_params)?; + pub(crate) fn merge_on_config(&mut self, on: &[OnConfig]) -> Result { + self.merge_config_into(on)?; // FIXME: refactor this to make it more consistent with `into` - // and allow for non-boolean flags in `OnParams`. E.g. add support + // and allow for non-boolean flags in `OnConfig`. E.g. add support // for `with = closure` to `on` as well. - self.params.overwritable = params::EvalBlanketFlagParam { - on_params, - param_name: params::BlanketParamName::Overwritable, - member_params: &self.params, + self.config.overwritable = config::EvalBlanketFlagParam { + on, + param_name: config::BlanketParamName::Overwritable, + member_config: &self.config, scrutinee: self.underlying_norm_ty(), origin: self.origin, } diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index 040d2bad..8fe45b57 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -1,14 +1,16 @@ mod builder_derives; -mod builder_params; mod finish_fn; mod member; mod models; mod setters; mod state_mod; +mod top_level_config; pub(crate) mod input_fn; pub(crate) mod input_struct; +pub(crate) use top_level_config::TopLevelConfig; + use crate::util::prelude::*; use member::{Member, MemberOrigin, NamedMember, RawMember, StartFnArgMember}; use models::{AssocMethodCtx, AssocMethodReceiverCtx, BuilderGenCtx, FinishFnBody, Generics}; @@ -44,7 +46,7 @@ impl BuilderGenCtx { let builder_derives = self.builder_derives(); let default_allows = syn::parse_quote!(#[allow( - // We have a `deprecated` lint on all `bon::private` items which we + // We have a `deprecated` lint on all `bon::__private` items which we // use in the generated code extensively deprecated )]); @@ -131,7 +133,7 @@ impl BuilderGenCtx { /// hints. fn ide_hints(&self) -> TokenStream { let type_patterns = self - .on_params + .on .iter() .map(|params| ¶ms.type_pattern) .collect::>(); @@ -163,7 +165,7 @@ impl BuilderGenCtx { fn start_fn(&self) -> syn::ItemFn { let builder_ident = &self.builder_type.ident; - let attrs = &self.start_fn.attrs; + let docs = &self.start_fn.docs; let vis = &self.start_fn.vis; let start_fn_ident = &self.start_fn.ident; @@ -231,7 +233,7 @@ impl BuilderGenCtx { }; syn::parse_quote! { - #(#attrs)* + #(#docs)* #[inline(always)] #[allow( // This is intentional. We want the builder syntax to compile away diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs index 83f54c29..0626a7a9 100644 --- a/bon-macros/src/builder/builder_gen/models.rs +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -1,7 +1,7 @@ -use super::builder_params::{BuilderDerives, OnParams}; use super::member::Member; +use super::top_level_config::{DerivesConfig, OnConfig}; use crate::normalization::GenericsNamespace; -use crate::parsing::{ItemParams, SpannedKey}; +use crate::parsing::{ItemSigConfig, SpannedKey}; use crate::util::prelude::*; use std::borrow::Cow; @@ -62,8 +62,7 @@ pub(super) struct StartFn { pub(super) ident: syn::Ident, pub(super) vis: syn::Visibility, - /// Additional attributes to apply to the item - pub(super) attrs: Vec, + pub(super) docs: Vec, /// Overrides the default generics pub(super) generics: Option, @@ -75,8 +74,7 @@ pub(super) struct StartFnParams { /// If present overrides the default visibility derived from the builder's type. pub(super) vis: Option, - /// Additional attributes to apply to the item - pub(super) attrs: Vec, + pub(super) docs: Vec, /// Overrides the default generics pub(super) generics: Option, @@ -88,14 +86,14 @@ pub(super) struct BuilderType { /// Visibility of the builder module itself. pub(super) vis: syn::Visibility, - pub(super) derives: BuilderDerives, + pub(super) derives: DerivesConfig, pub(super) docs: Vec, } pub(super) struct BuilderTypeParams { pub(super) ident: syn::Ident, pub(super) vis: Option, - pub(super) derives: BuilderDerives, + pub(super) derives: DerivesConfig, pub(super) docs: Option>, } @@ -134,6 +132,9 @@ pub(super) struct Generics { } pub(crate) struct BuilderGenCtx { + /// Path to the `bon` crate. + pub(super) bon: syn::Path, + /// Private identifiers that are used in the builder implementation. /// They are intentionally randomized to prevent users from accessing them. pub(super) ident_pool: PrivateIdentsPool, @@ -147,7 +148,7 @@ pub(crate) struct BuilderGenCtx { /// generated by the macro. If the original syntax used `#[expect(...)]`, /// then it must be represented as `#[allow(...)]` here. pub(super) allow_attrs: Vec, - pub(super) on_params: Vec, + pub(super) on: Vec, pub(super) generics: Generics, @@ -176,11 +177,12 @@ pub(super) struct PrivateIdentsPool { } pub(super) struct BuilderGenCtxParams<'a> { + pub(crate) bon: Option, pub(super) namespace: Cow<'a, GenericsNamespace>, pub(super) members: Vec, pub(super) allow_attrs: Vec, - pub(super) on_params: Vec, + pub(super) on: Vec, /// This is the visibility of the original item that the builder is generated for. /// For example, the `struct` or `fn` item visibility that the `#[builder]` or @@ -196,7 +198,7 @@ pub(super) struct BuilderGenCtxParams<'a> { pub(super) assoc_method_ctx: Option, pub(super) builder_type: BuilderTypeParams, - pub(super) state_mod: ItemParams, + pub(super) state_mod: ItemSigConfig, pub(super) start_fn: StartFnParams, pub(super) finish_fn: FinishFnParams, } @@ -204,10 +206,11 @@ pub(super) struct BuilderGenCtxParams<'a> { impl BuilderGenCtx { pub(super) fn new(params: BuilderGenCtxParams<'_>) -> Result { let BuilderGenCtxParams { + bon, namespace, members, allow_attrs, - on_params, + on, generics, orig_item_vis, assoc_method_ctx, @@ -300,7 +303,7 @@ impl BuilderGenCtx { let start_fn = StartFn { ident: start_fn.ident, vis: start_fn.vis.unwrap_or_else(|| builder_type.vis.clone()), - attrs: start_fn.attrs, + docs: start_fn.docs, generics: start_fn.generics, }; @@ -325,11 +328,12 @@ impl BuilderGenCtx { }; Ok(Self { + bon: bon.unwrap_or_else(|| syn::parse_quote!(::bon)), state_var, ident_pool: PrivateIdentsPool::new(), members, allow_attrs, - on_params, + on, generics, assoc_method_ctx, builder_type, diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs index dab66e3d..48bf088b 100644 --- a/bon-macros/src/builder/builder_gen/setters/mod.rs +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -1,6 +1,6 @@ use super::member::SetterClosure; use super::{BuilderGenCtx, NamedMember}; -use crate::parsing::ItemParams; +use crate::parsing::ItemSigConfig; use crate::util::prelude::*; use std::iter; @@ -27,10 +27,10 @@ impl<'a> SettersCtx<'a> { let member_type = self.member.ty.norm.as_ref(); - if let Some(closure) = &self.member.params.with { + if let Some(closure) = &self.member.config.with { input = Self::underlying_input_from_closure(closure); expr = self.member_expr_from_closure(closure); - } else if self.member.params.into.is_present() { + } else if self.member.config.into.is_present() { input = quote!(value: impl Into<#member_type>); expr = quote!(Into::into(value)); } else { @@ -49,12 +49,12 @@ impl<'a> SettersCtx<'a> { } fn setters_for_optional_member(&self, items: OptionalSettersItems) -> TokenStream { - if let Some(closure) = &self.member.params.with { + if let Some(closure) = &self.member.config.with { return self.setters_for_optional_member_with_closure(closure, items); } let underlying_ty = self.member.underlying_norm_ty(); - let underlying_ty = if self.member.params.into.is_present() { + let underlying_ty = if self.member.config.into.is_present() { quote!(impl Into<#underlying_ty>) } else { quote!(#underlying_ty) @@ -80,7 +80,7 @@ impl<'a> SettersCtx<'a> { imp: SetterImpl { input: quote!(value: Option<#underlying_ty>), body: SetterBody::SetMember { - expr: if self.member.params.into.is_present() { + expr: if self.member.config.into.is_present() { quote! { Option::map(value, Into::into) } @@ -257,7 +257,7 @@ impl<'a> SettersCtx<'a> { let result_output = self .member - .params + .config .with .as_ref() .and_then(|closure| closure.output.as_ref()); @@ -290,11 +290,11 @@ impl<'a> SettersCtx<'a> { } }; - if let Some(closure) = &self.member.params.with { + if let Some(closure) = &self.member.config.with { return_type = Self::maybe_wrap_in_result(closure, return_type); } - let where_clause = (!self.member.params.overwritable.is_present()).then(|| { + let where_clause = (!self.member.config.overwritable.is_present()).then(|| { let state_var = &self.base.state_var; let member_pascal = &self.member.name.pascal; quote! { @@ -366,11 +366,11 @@ impl SettersItems { let SettersCtx { member, base } = ctx; let builder_type = &base.builder_type; - let params = member.params.setters.as_ref(); + let config = member.config.setters.as_ref(); - let common_name = params.and_then(|params| params.name.as_deref()); - let common_vis = params.and_then(|params| params.vis.as_deref()); - let common_docs = params.and_then(|params| params.docs.as_deref().map(Vec::as_slice)); + let common_name = config.and_then(|config| config.name.as_deref()); + let common_vis = config.and_then(|config| config.vis.as_deref()); + let common_docs = config.and_then(|config| config.docs.as_deref().map(Vec::as_slice)); let doc = |docs: &str| iter::once(syn::parse_quote!(#[doc = #docs])); @@ -390,16 +390,16 @@ impl SettersItems { }); } - let some_fn = params.and_then(|params| params.fns.some_fn.as_deref()); + let some_fn = config.and_then(|config| config.fns.some_fn.as_deref()); let some_fn_name = some_fn - .and_then(ItemParams::name) + .and_then(ItemSigConfig::name) .or(common_name) .unwrap_or(&member.name.snake) .clone(); - let option_fn = params.and_then(|params| params.fns.option_fn.as_deref()); + let option_fn = config.and_then(|config| config.fns.option_fn.as_deref()); let option_fn_name = option_fn - .and_then(ItemParams::name) + .and_then(ItemSigConfig::name) .cloned() .unwrap_or_else(|| { let base_name = common_name.unwrap_or(&member.name.snake); @@ -410,7 +410,7 @@ impl SettersItems { syn::Ident::new(&format!("maybe_{base_name}"), base_name.span()) }); - let default = member.params.default.as_deref().and_then(|default| { + let default = member.config.default.as_deref().and_then(|default| { let default = default .clone() .or_else(|| well_known_default(&member.ty.norm)) @@ -434,7 +434,7 @@ impl SettersItems { // FIXME: the docs shouldn't reference the companion setter if that // setter has a lower visibility. let some_fn_docs = some_fn - .and_then(ItemParams::docs) + .and_then(ItemSigConfig::docs) .or(common_docs) .unwrap_or(&member.docs); @@ -445,7 +445,7 @@ impl SettersItems { }; let option_fn_docs = option_fn - .and_then(ItemParams::docs) + .and_then(ItemSigConfig::docs) .or(common_docs) .unwrap_or(&member.docs); @@ -462,7 +462,7 @@ impl SettersItems { let some_fn = SetterItem { name: some_fn_name, vis: some_fn - .and_then(ItemParams::vis) + .and_then(ItemSigConfig::vis) .or(common_vis) .unwrap_or(&builder_type.vis) .clone(), @@ -470,12 +470,12 @@ impl SettersItems { docs: some_fn_docs, }; - let option_fn = params.and_then(|params| params.fns.option_fn.as_deref()); + let option_fn = config.and_then(|config| config.fns.option_fn.as_deref()); let option_fn = SetterItem { name: option_fn_name, vis: option_fn - .and_then(ItemParams::vis) + .and_then(ItemSigConfig::vis) .or(common_vis) .unwrap_or(&builder_type.vis) .clone(), diff --git a/bon-macros/src/builder/builder_gen/state_mod.rs b/bon-macros/src/builder/builder_gen/state_mod.rs index ca093d26..5208903e 100644 --- a/bon-macros/src/builder/builder_gen/state_mod.rs +++ b/bon-macros/src/builder/builder_gen/state_mod.rs @@ -38,6 +38,7 @@ impl<'a> StateModGenCtx<'a> { } pub(super) fn state_mod(&self) -> TokenStream { + let bon = &self.base.bon; let vis = &self.base.state_mod.vis; let vis_child = &self.base.state_mod.vis_child; let vis_child_child = &self.base.state_mod.vis_child_child; @@ -68,8 +69,8 @@ impl<'a> StateModGenCtx<'a> { #( #state_mod_docs )* #vis mod #state_mod_ident { #[doc(inline)] - #vis_child use ::bon::private::{IsSet, IsUnset}; - use ::bon::private::{Set, Unset}; + #vis_child use #bon::__private::{IsSet, IsUnset}; + use #bon::__private::{Set, Unset}; mod sealed { #vis_child_child struct Sealed; 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 new file mode 100644 index 00000000..17c612e1 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -0,0 +1,134 @@ +mod on; + +pub(crate) use on::OnConfig; + +use crate::parsing::{ItemSigConfig, ItemSigConfigParsing, SpannedKey}; +use crate::util::prelude::*; +use darling::FromMeta; +use syn::punctuated::Punctuated; + +fn parse_finish_fn(meta: &syn::Meta) -> Result { + ItemSigConfigParsing { + meta, + reject_self_mentions: Some("builder struct's impl block"), + } + .parse() +} + +fn parse_builder_type(meta: &syn::Meta) -> Result { + ItemSigConfigParsing { + meta, + reject_self_mentions: Some("builder struct"), + } + .parse() +} + +fn parse_state_mod(meta: &syn::Meta) -> Result { + ItemSigConfigParsing { + meta, + reject_self_mentions: Some("builder's state module"), + } + .parse() +} + +fn parse_start_fn(meta: &syn::Meta) -> Result { + ItemSigConfigParsing { + meta, + reject_self_mentions: None, + } + .parse() +} + +#[derive(Debug, FromMeta)] +pub(crate) struct TopLevelConfig { + /// Overrides the path to the `bon` crate. This is usedfule when the macro is + /// wrapped in another macro that also reexports `bon`. + #[darling(rename = "crate", default, map = Some, with = crate::parsing::parse_bon_crate_path)] + pub(crate) bon: Option, + + #[darling(default, with = parse_start_fn)] + pub(crate) start_fn: ItemSigConfig, + + #[darling(default, with = parse_finish_fn)] + pub(crate) finish_fn: ItemSigConfig, + + #[darling(default, with = parse_builder_type)] + pub(crate) builder_type: ItemSigConfig, + + #[darling(default, with = parse_state_mod)] + pub(crate) state_mod: ItemSigConfig, + + #[darling(multiple, with = crate::parsing::parse_non_empty_paren_meta_list)] + pub(crate) on: Vec, + + /// Specifies the derives to apply to the builder. + #[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)] + pub(crate) derive: DerivesConfig, +} + +impl TopLevelConfig { + pub(crate) fn parse_for_fn(meta_list: &[darling::ast::NestedMeta]) -> Result { + let me = Self::from_list(meta_list)?; + + if me.start_fn.name.is_none() { + let ItemSigConfig { name: _, vis, docs } = &me.start_fn; + + let unexpected_param = None + .or_else(|| vis.as_ref().map(SpannedKey::key)) + .or_else(|| docs.as_ref().map(SpannedKey::key)); + + if let Some(unexpected_param) = unexpected_param { + bail!( + unexpected_param, + "#[builder(start_fn({unexpected_param}))] requires that you \ + also specify #[builder(start_fn(name))] which makes the starting \ + function not to replace the positional function under the #[builder] \ + attribute; by default (without the explicit #[builder(start_fn(name))]) \ + the name, visibility and documentation of the positional \ + function are all copied to the starting function, and the positional \ + function under the #[builder] attribute becomes private with \ + #[doc(hidden)] and it's renamed (the name is not guaranteed \ + to be stable) to make it inaccessible even within the current module", + ); + } + } + + Ok(me) + } +} + +#[derive(Debug, Clone, Default, FromMeta)] +pub(crate) struct DerivesConfig { + #[darling(rename = "Clone")] + pub(crate) clone: Option, + + #[darling(rename = "Debug")] + pub(crate) debug: Option, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct DeriveConfig { + pub(crate) bounds: Option>, +} + +impl FromMeta for DeriveConfig { + fn from_meta(meta: &syn::Meta) -> Result { + if let syn::Meta::Path(_) = meta { + return Ok(Self { bounds: None }); + } + + meta.require_list()?.require_parens_delim()?; + + #[derive(FromMeta)] + struct Parsed { + #[darling(with = crate::parsing::parse_paren_meta_list_with_terminated)] + bounds: Punctuated, + } + + let Parsed { bounds } = Parsed::from_meta(meta)?; + + Ok(Self { + bounds: Some(bounds), + }) + } +} diff --git a/bon-macros/src/builder/builder_gen/builder_params/on_params.rs b/bon-macros/src/builder/builder_gen/top_level_config/on.rs similarity index 97% rename from bon-macros/src/builder/builder_gen/builder_params/on_params.rs rename to bon-macros/src/builder/builder_gen/top_level_config/on.rs index 1acf1d23..6a105dd1 100644 --- a/bon-macros/src/builder/builder_gen/builder_params/on_params.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/on.rs @@ -5,13 +5,13 @@ use syn::spanned::Spanned; use syn::visit::Visit; #[derive(Debug)] -pub(crate) struct OnParams { +pub(crate) struct OnConfig { pub(crate) type_pattern: syn::Type, pub(crate) into: darling::util::Flag, pub(crate) overwritable: darling::util::Flag, } -impl Parse for OnParams { +impl Parse for OnConfig { fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { let type_pattern = input.parse()?; @@ -86,7 +86,7 @@ impl Parse for OnParams { } } -impl FromMeta for OnParams { +impl FromMeta for OnConfig { fn from_meta(meta: &syn::Meta) -> Result { let meta = match meta { syn::Meta::List(meta) => meta, diff --git a/bon-macros/src/builder/item_fn.rs b/bon-macros/src/builder/item_fn.rs index 7f6735aa..dc49b557 100644 --- a/bon-macros/src/builder/item_fn.rs +++ b/bon-macros/src/builder/item_fn.rs @@ -1,11 +1,11 @@ -use super::builder_gen::input_fn::{FnInputCtx, FnInputParams}; -use super::builder_gen::MacroOutput; +use super::builder_gen::input_fn::{FnInputCtx, FnInputCtxParams}; +use super::builder_gen::{MacroOutput, TopLevelConfig}; use crate::normalization::SyntaxVariant; use crate::util::prelude::*; use syn::visit_mut::VisitMut; pub(crate) fn generate( - params: FnInputParams, + config: TopLevelConfig, orig_fn: syn::ItemFn, namespace: &crate::normalization::GenericsNamespace, ) -> Result { @@ -19,12 +19,12 @@ pub(crate) fn generate( norm: norm_fn, }; - let ctx = FnInputCtx { + let ctx = FnInputCtx::new(FnInputCtxParams { namespace, fn_item, impl_ctx: None, - params, - }; + config, + }); let adapted_fn = ctx.adapted_fn()?; diff --git a/bon-macros/src/builder/item_impl.rs b/bon-macros/src/builder/item_impl.rs index 09dd3da5..4831db4c 100644 --- a/bon-macros/src/builder/item_impl.rs +++ b/bon-macros/src/builder/item_impl.rs @@ -1,4 +1,5 @@ -use super::builder_gen::input_fn::{FnInputCtx, FnInputParams, ImplCtx}; +use super::builder_gen::input_fn::{FnInputCtx, FnInputCtxParams, ImplCtx}; +use super::builder_gen::TopLevelConfig; use crate::normalization::{GenericsNamespace, SyntaxVariant}; use crate::util::prelude::*; use darling::ast::NestedMeta; @@ -7,7 +8,20 @@ use std::rc::Rc; use syn::visit::Visit; use syn::visit_mut::VisitMut; -pub(crate) fn generate(mut orig_impl_block: syn::ItemImpl) -> Result { +#[derive(FromMeta)] +pub(crate) struct ImplInputParams { + /// Overrides the path to the `bon` crate. This is usedfule when the macro is + /// wrapped in another macro that also reexports `bon`. + #[darling(rename = "crate", default, map = Some, with = crate::parsing::parse_bon_crate_path)] + bon: Option, +} + +// ImplInputParams will evolve in the future where we'll probably want to move from it +#[allow(clippy::needless_pass_by_value)] +pub(crate) fn generate( + impl_params: ImplInputParams, + mut orig_impl_block: syn::ItemImpl, +) -> Result { let mut namespace = GenericsNamespace::default(); namespace.visit_item_impl(&orig_impl_block); @@ -31,8 +45,8 @@ pub(crate) fn generate(mut orig_impl_block: syn::ItemImpl) -> Result Result>(); - let params = FnInputParams::from_list(&meta)?; + let mut config = TopLevelConfig::parse_for_fn(&meta)?; + + if let Some(bon) = config.bon { + bail!( + &bon, + "`crate` parameter should be specified via `#[bon(crate = path::to::bon)]` \ + when impl block syntax is used; no need to specify it in the method's \ + `#[builder]` attribute" + ); + } + + config.bon.clone_from(&impl_params.bon); let fn_item = SyntaxVariant { orig: orig_fn, norm: norm_fn, }; - let ctx = FnInputCtx { + let ctx = FnInputCtx::new(FnInputCtxParams { namespace: &namespace, fn_item, impl_ctx: Some(impl_ctx.clone()), - params, - }; + config, + }); Result::<_>::Ok((ctx.adapted_fn()?, ctx.into_builder_gen_ctx()?.output()?)) }) diff --git a/bon-macros/src/builder/mod.rs b/bon-macros/src/builder/mod.rs index 1108fde6..2afbede5 100644 --- a/bon-macros/src/builder/mod.rs +++ b/bon-macros/src/builder/mod.rs @@ -5,10 +5,10 @@ pub(crate) mod item_impl; mod item_fn; mod item_struct; -use crate::normalization::{ExpandCfg, ExpansionOutput, GenericsNamespace}; +use crate::normalization::{ExpandCfg, Expansion, GenericsNamespace}; use crate::util; use crate::util::prelude::*; -use darling::FromMeta; +use builder_gen::TopLevelConfig; use syn::parse::Parser; use syn::visit::Visit; @@ -27,7 +27,7 @@ fn try_generate_from_derive(item: TokenStream) -> Result { } pub(crate) fn generate_from_attr(params: TokenStream, item: TokenStream) -> TokenStream { - crate::error::with_fallback(item.clone(), || { + crate::error::handle_errors(item.clone(), || { try_generate_from_attr(params.clone(), item) }) .unwrap_or_else(|fallback| [generate_completion_triggers(params), fallback].concat()) @@ -36,29 +36,28 @@ pub(crate) fn generate_from_attr(params: TokenStream, item: TokenStream) -> Toke fn try_generate_from_attr(params: TokenStream, item: TokenStream) -> Result { let item: syn::Item = syn::parse2(item)?; - let macro_path = syn::parse_quote!(::bon::builder); - let ctx = ExpandCfg { - macro_path, - params, + current_macro: format_ident!("builder"), + config: params, item, }; - let (params, item) = match ctx.expand_cfg()? { - ExpansionOutput::Expanded { params, item } => (params, item), - ExpansionOutput::Recurse(output) => return Ok(output), + let input = match ctx.expand_cfg()? { + Expansion::Expanded(input) => input, + Expansion::Recurse(output) => return Ok(output), }; - let nested_meta = &darling::ast::NestedMeta::parse_meta_list(params.clone())?; - - let main_output = match item { + let main_output = match input.item { syn::Item::Fn(item_fn) => { let mut namespace = GenericsNamespace::default(); - namespace.visit_token_stream(params.clone()); + namespace.visit_token_stream(input.config.clone()); namespace.visit_item_fn(&item_fn); - item_fn::generate(FromMeta::from_list(nested_meta)?, item_fn, &namespace)? + let meta_list = darling::ast::NestedMeta::parse_meta_list(input.config.clone())?; + let config = TopLevelConfig::parse_for_fn(&meta_list)?; + + item_fn::generate(config, item_fn, &namespace)? } syn::Item::Struct(struct_item) => { bail!( @@ -73,7 +72,7 @@ fn try_generate_from_attr(params: TokenStream, item: TokenStream) -> Result -pub(crate) fn with_fallback( +pub(crate) fn handle_errors( item: TokenStream, imp: impl FnOnce() -> Result, ) -> Result { + let panic_listener = panic_context::PanicListener::register(); + std::panic::catch_unwind(AssertUnwindSafe(imp)) .unwrap_or_else(|err| { - let msg = err - .downcast::<&str>() - .map(|msg| msg.to_string()) - .or_else(|err| err.downcast::().map(|msg| *msg)) - .unwrap_or_else(|_| "".to_owned()); + let msg = panic_context::message_from_panic_payload(err.as_ref()) + .unwrap_or_else(|| "".to_owned()); let msg = if msg.contains("unsupported proc macro punctuation character") { format!( @@ -33,10 +34,15 @@ pub(crate) fn with_fallback( Github issue: https://github.com/rust-lang/rust-analyzer/issues/18244" ) } else { + let context = panic_listener + .get_last_panic() + .map(|ctx| format!("\n\n{ctx}")) + .unwrap_or_default(); + format!( - "bug in the crate `bon` (proc-macro panicked): `{msg}`;\n\ + "proc-macro panicked (may be a bug in the crate `bon`): `{msg}`;\n\ please report this issue at our Github repository: \ - https://github.com/elastio/bon" + https://github.com/elastio/bon{context}" ) }; diff --git a/bon-macros/src/error/panic_context.rs b/bon-macros/src/error/panic_context.rs new file mode 100644 index 00000000..9eebb3ea --- /dev/null +++ b/bon-macros/src/error/panic_context.rs @@ -0,0 +1,222 @@ +// The new name is used on newer rust versions +#[rustversion::since(1.81.0)] +use std::panic::PanicHookInfo as StdPanicHookInfo; + +// The deprecated name for is used on older rust versions +#[rustversion::before(1.81.0)] +use std::panic::PanicInfo as StdPanicHookInfo; + +use std::any::Any; +use std::cell::RefCell; +use std::fmt; +use std::rc::Rc; + +fn with_global_panic_context(f: impl FnOnce(&mut GlobalPanicContext) -> T) -> T { + thread_local! { + /// A lazily initialized global panic context. It aggregates the panics from the + /// current thread. This is used to capture info about the panic after the + /// `catch_unwind` call and observe the context of the panic that happened. + /// + /// Unfortunately, we can't use a global static variable that would be + /// accessible by all threads because `std::sync::Mutex::new` became + /// `const` only in Rust 1.63.0, which is above our MSRV 1.59.0. However, + /// a thread-local works perfectly fine for our use case because we don't + /// spawn threads in proc macros. + static GLOBAL: RefCell = const { + RefCell::new(GlobalPanicContext { + last_panic: None, + initialized: false, + }) + }; + } + + GLOBAL.with(|global| f(&mut global.borrow_mut())) +} + +struct GlobalPanicContext { + last_panic: Option, + initialized: bool, +} + +/// This struct without any fields exists to make sure that [`PanicListener::register()`] +/// is called first before the code even attempts to get the last panic information. +#[derive(Default)] +pub(super) struct PanicListener { + /// Required to make sure struct is not constructable via a struct literal + /// in the code outside of this module. + _private: (), +} + +impl PanicListener { + pub(super) fn register() -> Self { + with_global_panic_context(Self::register_with_global) + } + + fn register_with_global(global: &mut GlobalPanicContext) -> Self { + if global.initialized { + return Self { _private: () }; + } + + let prev_panic_hook = std::panic::take_hook(); + + std::panic::set_hook(Box::new(move |panic_info| { + with_global_panic_context(|global| { + let panics_count = global.last_panic.as_ref().map(|p| p.0.panics_count); + let panics_count = panics_count.unwrap_or(0) + 1; + + global.last_panic = Some(PanicContext::from_std(panic_info, panics_count)); + }); + + prev_panic_hook(panic_info); + })); + + global.initialized = true; + + Self { _private: () } + } + + /// Returns the last panic that happened since the [`PanicListener::register()`] call. + // `self` is required to make sure this code runs only after we initialized + // the global panic listener in the `register` method. + #[allow(clippy::unused_self)] + pub(super) fn get_last_panic(&self) -> Option { + with_global_panic_context(|global| global.last_panic.clone()) + } +} + +/// Contains all the necessary bits of information about the occurred panic. +#[derive(Clone)] +pub(super) struct PanicContext(Rc); + +struct PanicContextShared { + backtrace: backtrace::Backtrace, + + location: Option, + thread: String, + + /// Defines the number of panics that happened before this one. Each panic + /// increments this counter. This is useful to know how many panics happened + /// before the current one. + panics_count: usize, +} + +impl PanicContext { + fn from_std(std_panic_info: &StdPanicHookInfo<'_>, panics_count: usize) -> Self { + let location = std_panic_info.location(); + let current_thread = std::thread::current(); + let thread_ = current_thread + .name() + .map(String::from) + .unwrap_or_else(|| format!("{:?}", current_thread.id())); + + Self(Rc::new(PanicContextShared { + backtrace: backtrace::Backtrace::capture(), + location: location.map(PanicLocation::from_std), + thread: thread_, + panics_count, + })) + } +} + +impl fmt::Debug for PanicContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for PanicContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let PanicContextShared { + location, + backtrace, + thread, + panics_count, + } = &*self.0; + + write!(f, "panic occurred")?; + + if let Some(location) = location { + write!(f, " at {location}")?; + } + + write!(f, " in thread '{thread}'")?; + + if *panics_count > 1 { + write!(f, " (total panics observed: {panics_count})")?; + } + + // #[rustversion::attr(before(1.65.0), allow(clippy::irrefutable_let_patterns))] + #[allow(clippy::incompatible_msrv)] + if backtrace.status() == backtrace::BacktraceStatus::Captured { + write!(f, "\nbacktrace:\n{backtrace}")?; + } + + Ok(()) + } +} + +/// Extract the message of a panic. +pub(super) fn message_from_panic_payload(payload: &dyn Any) -> Option { + if let Some(str_slice) = payload.downcast_ref::<&str>() { + return Some((*str_slice).to_owned()); + } + if let Some(owned_string) = payload.downcast_ref::() { + return Some(owned_string.clone()); + } + + None +} + +/// Location of the panic call site. +#[derive(Clone)] +struct PanicLocation { + file: String, + line: u32, + col: u32, +} + +impl PanicLocation { + fn from_std(loc: &std::panic::Location<'_>) -> Self { + Self { + file: loc.file().to_owned(), + line: loc.line(), + col: loc.column(), + } + } +} + +impl fmt::Display for PanicLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}:{}", self.file, self.line, self.col) + } +} + +#[rustversion::since(1.65.0)] +mod backtrace { + pub(super) use std::backtrace::{Backtrace, BacktraceStatus}; +} + +#[rustversion::before(1.65.0)] +mod backtrace { + #[derive(PartialEq)] + pub(super) enum BacktraceStatus { + Captured, + } + + pub(super) struct Backtrace; + + impl Backtrace { + pub(super) fn capture() -> Self { + Self + } + pub(super) fn status(&self) -> BacktraceStatus { + BacktraceStatus::Captured + } + } + + impl std::fmt::Display for Backtrace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("{update your Rust compiler to >=1.65.0 to see the backtrace}") + } + } +} diff --git a/bon-macros/src/normalization/cfg/mod.rs b/bon-macros/src/normalization/cfg/mod.rs index bf0e5a1b..020fb47f 100644 --- a/bon-macros/src/normalization/cfg/mod.rs +++ b/bon-macros/src/normalization/cfg/mod.rs @@ -2,41 +2,45 @@ mod parse; mod visit; use crate::util::prelude::*; +use darling::ast::NestedMeta; use parse::CfgSyntax; use std::collections::BTreeSet; +use syn::parse::Parser; -pub(crate) enum ExpansionOutput { - Expanded { - params: TokenStream, - item: syn::Item, - }, +pub(crate) enum Expansion { + Expanded(Expanded), Recurse(TokenStream), } +pub(crate) struct Expanded { + pub(crate) config: TokenStream, + pub(crate) item: syn::Item, +} + pub(crate) struct ExpandCfg { - pub(crate) macro_path: syn::Path, - pub(crate) params: TokenStream, + pub(crate) current_macro: syn::Ident, + pub(crate) config: TokenStream, pub(crate) item: syn::Item, } impl ExpandCfg { - pub(crate) fn expand_cfg(mut self) -> Result { + pub(crate) fn expand_cfg(mut self) -> Result { let predicates = self.collect_predicates()?; if predicates.is_empty() { - return Ok(ExpansionOutput::Expanded { - params: self.params, + return Ok(Expansion::Expanded(Expanded { + config: self.config, item: self.item, - }); + })); } - let predicate_results = match parse::parse_predicate_results(self.params.clone())? { + let predicate_results = match parse::parse_predicate_results(self.config.clone())? { Some(predicate_results) => predicate_results, None => return self.into_recursion(0, &predicates), }; - // Update the parameters to remove the `@cfgs(...)` prefix from them - self.params = predicate_results.rest; + // Update the config to remove the `@cfgs(...)` prefix from them + self.config = predicate_results.rest; let true_predicates: BTreeSet<_> = predicates .iter() @@ -53,10 +57,10 @@ impl ExpandCfg { let predicates = self.collect_predicates()?; if predicates.is_empty() { - return Ok(ExpansionOutput::Expanded { - params: self.params, + return Ok(Expansion::Expanded(Expanded { + config: self.config, item: self.item, - }); + })); } self.into_recursion(predicate_results.recursion_counter + 1, &predicates) @@ -95,14 +99,28 @@ impl ExpandCfg { self, recursion_counter: usize, predicates: &[TokenStream], - ) -> Result { + ) -> Result { let Self { - params, + config, item, - macro_path, - } = &self; + current_macro, + } = self; + + let bon = NestedMeta::parse_meta_list(config.clone())? + .iter() + .find_map(|meta| match meta { + NestedMeta::Meta(syn::Meta::NameValue(meta)) if meta.path.is_ident("crate") => { + let path = &meta.value; + Some(syn::Path::parse_mod_style.parse2(quote!(#path))) + } + _ => None, + }) + .transpose()? + .unwrap_or_else(|| syn::parse_quote!(::bon)); + + let current_macro = syn::parse_quote!(#bon::#current_macro); - let invocation_name = self.unique_invocation_name()?; + let invocation_name = Self::unique_invocation_name(&item, ¤t_macro)?; let predicates = predicates.iter().enumerate().map(|(i, predicate)| { // We need to insert the recursion counter into the name so that @@ -112,17 +130,17 @@ impl ExpandCfg { }); let expansion = quote! { - ::bon::__eval_cfg_callback! { + #bon::__eval_cfg_callback! { {} #((#predicates))* - #macro_path, + #current_macro, #recursion_counter, - ( #params ) + ( #config ) #item } }; - Ok(ExpansionOutput::Recurse(expansion)) + Ok(Expansion::Recurse(expansion)) } /// The macro `__eval_cfg_callback` needs to generate a use statement for @@ -151,16 +169,16 @@ impl ExpandCfg { /// /// However, in most of the cases it will be a simple path, so its combination /// with the name of the first function in the `impl` block should be unique enough. - fn unique_invocation_name(&self) -> Result { + fn unique_invocation_name(item: &syn::Item, current_macro: &syn::Path) -> Result { let path_to_ident = |path: &syn::Path| path.segments.iter().map(|segment| &segment.ident).join("_"); // Include the name of the proc macro in the unique name to avoid // collisions when different proc macros are placed on the same item // and they use this code to generate unique names. - let macro_path_str = path_to_ident(&self.macro_path); + let macro_path_str = path_to_ident(current_macro); - let item_name = match &self.item { + let item_name = match item { syn::Item::Fn(item) => item.sig.ident.to_string(), syn::Item::Impl(item) => { let self_ty = item diff --git a/bon-macros/src/parsing/item_params.rs b/bon-macros/src/parsing/item_sig.rs similarity index 78% rename from bon-macros/src/parsing/item_params.rs rename to bon-macros/src/parsing/item_sig.rs index 441a5bc4..7288ebde 100644 --- a/bon-macros/src/parsing/item_params.rs +++ b/bon-macros/src/parsing/item_sig.rs @@ -2,14 +2,17 @@ use super::SpannedKey; use crate::util::prelude::*; use darling::FromMeta; +/// "Item signature" is a set of parameters that configures some aspects of +/// an item like a function, struct, struct field, module, trait. All of them +/// have configurable properties that are specified here. #[derive(Debug, Clone, Default)] -pub(crate) struct ItemParams { +pub(crate) struct ItemSigConfig { pub(crate) name: Option>, pub(crate) vis: Option>, pub(crate) docs: Option>>, } -impl ItemParams { +impl ItemSigConfig { pub(crate) fn name(&self) -> Option<&syn::Ident> { self.name.as_ref().map(|name| &name.value) } @@ -23,20 +26,20 @@ impl ItemParams { } } -pub(crate) struct ItemParamsParsing<'a> { +pub(crate) struct ItemSigConfigParsing<'a> { pub(crate) meta: &'a syn::Meta, pub(crate) reject_self_mentions: Option<&'static str>, } -impl ItemParamsParsing<'_> { - pub(crate) fn parse(self) -> Result { +impl ItemSigConfigParsing<'_> { + pub(crate) fn parse(self) -> Result { let meta = self.meta; if let syn::Meta::NameValue(meta) = meta { let val = &meta.value; let name = syn::parse2(val.to_token_stream())?; - return Ok(ItemParams { + return Ok(ItemSigConfig { name: Some(SpannedKey::new(&meta.path, name)?), vis: None, docs: None, @@ -60,12 +63,12 @@ impl ItemParamsParsing<'_> { } } - let params = ItemParams { + let config = ItemSigConfig { name: full.name, vis: full.vis, docs: full.doc, }; - Ok(params) + Ok(config) } } diff --git a/bon-macros/src/parsing/mod.rs b/bon-macros/src/parsing/mod.rs index 6b8477e1..15788d18 100644 --- a/bon-macros/src/parsing/mod.rs +++ b/bon-macros/src/parsing/mod.rs @@ -1,10 +1,10 @@ mod docs; -mod item_params; +mod item_sig; mod simple_closure; mod spanned_key; pub(crate) use docs::*; -pub(crate) use item_params::*; +pub(crate) use item_sig::*; pub(crate) use simple_closure::*; pub(crate) use spanned_key::*; @@ -12,6 +12,7 @@ use crate::util::prelude::*; use darling::FromMeta; use syn::parse::Parser; use syn::punctuated::Punctuated; +use syn::spanned::Spanned; pub(crate) fn parse_non_empty_paren_meta_list(meta: &syn::Meta) -> Result { require_non_empty_paren_meta_list_or_name_value(meta)?; @@ -32,9 +33,9 @@ pub(crate) fn require_non_empty_paren_meta_list_or_name_value(meta: &syn::Meta) } syn::Meta::Path(path) => bail!( &meta, - "this empty `#[{0}]` attribute is unexpected; \ + "this empty `{0}` attribute is unexpected; \ remove it or pass parameters in parentheses: \ - `#[{0}(...)]`", + `{0}(...)`", darling::util::path_to_string(path) ), syn::Meta::NameValue(_) => {} @@ -81,3 +82,57 @@ where Ok(punctuated) } + +fn parse_path_mod_style(meta: &syn::Meta) -> Result { + let expr = match meta { + syn::Meta::NameValue(meta) => &meta.value, + _ => bail!(meta, "expected a simple path, like `foo::bar`"), + }; + + Ok(expr.require_path_mod_style()?.clone()) +} + +pub(crate) fn parse_bon_crate_path(meta: &syn::Meta) -> Result { + let path = parse_path_mod_style(meta)?; + + let prefix = &path + .segments + .first() + .ok_or_else(|| err!(&path, "path must have at least one segment"))? + .ident; + + let is_absolute = path.leading_colon.is_some() || prefix == "crate" || prefix == "$crate"; + + if is_absolute { + return Ok(path); + } + + if prefix == "super" || prefix == "self" { + bail!( + &path, + "path must not be relative; specify the path that starts with `crate::` \ + instead; if you want to refer to a reexport from an external crate then \ + use a leading colon like `::crate_name::reexport::path::bon`" + ) + } + + let path_str = darling::util::path_to_string(&path); + + bail!( + &path, + "path must be absolute; if you want to refer to a reexport from an external \ + crate then add a leading colon like `::{path_str}`; if the path leads to a module \ + in the current crate, then specify the absolute path with `crate` like \ + `crate::reexport::path::bon` or `$crate::reexport::path::bon` (if within a macro)" + ) +} + +// Lint from nightly. `&Option` is used to reduce syntax at the callsite +#[allow(unknown_lints, clippy::ref_option)] +pub(crate) fn reject_syntax(name: &'static str, syntax: &Option) -> Result { + if let Some(syntax) = syntax { + bail!(syntax, "{name} is not allowed here") + } + + Ok(()) +} diff --git a/bon-macros/src/parsing/simple_closure.rs b/bon-macros/src/parsing/simple_closure.rs index 69b21252..732419f3 100644 --- a/bon-macros/src/parsing/simple_closure.rs +++ b/bon-macros/src/parsing/simple_closure.rs @@ -1,6 +1,6 @@ +use super::reject_syntax; use crate::util::prelude::*; use darling::FromMeta; -use syn::spanned::Spanned; /// Utility type for parsing simple closure syntax that only allows [`syn::PatIdent`] /// inputs and rejects any attributes and prefix keywords like `async`, `move`, `for` @@ -64,16 +64,6 @@ impl FromMeta for SimpleClosure { } } -// Lint from nightly. `&Option` is used to reduce syntax at the callsite -#[allow(unknown_lints, clippy::ref_option)] -fn reject_syntax(name: &'static str, syntax: &Option) -> Result { - if let Some(syntax) = syntax { - bail!(syntax, "{name} is not allowed here") - } - - Ok(()) -} - impl SimpleClosureInput { fn from_pat_ident(pat: syn::PatIdent) -> Result { reject_syntax("attribute", &pat.attrs.first())?; diff --git a/bon-macros/src/parsing/spanned_key.rs b/bon-macros/src/parsing/spanned_key.rs index f90fc822..dbec4311 100644 --- a/bon-macros/src/parsing/spanned_key.rs +++ b/bon-macros/src/parsing/spanned_key.rs @@ -23,6 +23,10 @@ impl SpannedKey { pub(crate) fn into_value(self) -> T { self.value } + + pub(crate) fn key(&self) -> &syn::Ident { + &self.key + } } impl FromMeta for SpannedKey { diff --git a/bon-macros/src/util/attrs.rs b/bon-macros/src/util/attrs.rs index 92503c4c..9475a6ef 100644 --- a/bon-macros/src/util/attrs.rs +++ b/bon-macros/src/util/attrs.rs @@ -1,3 +1,5 @@ +use syn::spanned::Spanned; + pub(crate) trait AttributeExt { fn is_doc_expr(&self) -> bool; fn as_doc_expr(&self) -> Option<&syn::Expr>; @@ -43,7 +45,7 @@ impl AttributeExt for syn::Attribute { syn::Meta::NameValue(meta) => &mut meta.path, }; - *path = syn::parse_quote!(allow); + *path = syn::parse_quote_spanned!(path.span()=> allow); Some(attr) } diff --git a/bon-macros/src/util/expr.rs b/bon-macros/src/util/expr.rs new file mode 100644 index 00000000..36cc2bd4 --- /dev/null +++ b/bon-macros/src/util/expr.rs @@ -0,0 +1,21 @@ +use crate::util::prelude::*; + +pub(crate) trait ExprExt { + fn require_path_mod_style(&self) -> Result<&syn::Path>; +} + +impl ExprExt for syn::Expr { + fn require_path_mod_style(&self) -> Result<&syn::Path> { + let expr = match self { + Self::Path(expr) => expr, + _ => bail!(self, "expected a simple path, like `foo::bar`"), + }; + + crate::parsing::reject_syntax("attribute", &expr.attrs.first())?; + crate::parsing::reject_syntax(" syntax", &expr.qself)?; + + expr.path.require_mod_style()?; + + Ok(&expr.path) + } +} diff --git a/bon-macros/src/util/ide.rs b/bon-macros/src/util/ide.rs index 8465ac51..3a2bcc38 100644 --- a/bon-macros/src/util/ide.rs +++ b/bon-macros/src/util/ide.rs @@ -135,11 +135,23 @@ fn paths_from_meta(meta: Vec) -> Vec { /// we can hint the IDEs to provide completions for the attributes based on what's /// available in the module the use statement references. pub(crate) fn generate_completion_triggers(meta: Vec) -> TokenStream { + let bon = meta + .iter() + .find_map(|meta| match meta { + Meta::NameValue(meta) if meta.path.is_ident("crate") => Some(meta.value.as_ref()), + _ => None, + }) + .flatten() + .and_then(|expr| Some(expr.require_path_mod_style().ok()?.clone())) + .unwrap_or_else(|| syn::parse_quote!(::bon)); + let completions = CompletionsSchema::with_children( "builder_top_level", vec![ - CompletionsSchema::leaf("expose_positional_fn"), + CompletionsSchema::leaf("builder_type"), CompletionsSchema::leaf("start_fn"), + CompletionsSchema::leaf("finish_fn"), + CompletionsSchema::leaf("state_mod"), CompletionsSchema::leaf("on").set_custom_filter(|meta| { if !meta.is_empty() { meta.remove(0); @@ -149,7 +161,7 @@ pub(crate) fn generate_completion_triggers(meta: Vec) -> TokenStream { ], ); - let completion_triggers = completions.generate_completion_triggers(meta, &[]); + let completion_triggers = completions.generate_completion_triggers(&bon, meta, &[]); quote! { // The special `rust_analyzer` CFG is enabled only when Rust Analyzer is @@ -192,6 +204,7 @@ impl CompletionsSchema { fn generate_completion_triggers( &self, + bon: &syn::Path, mut meta: Vec, module_prefix: &[&syn::Ident], ) -> TokenStream { @@ -226,7 +239,7 @@ impl CompletionsSchema { }) .concat(); - child.generate_completion_triggers(child_metas, &module_name) + child.generate_completion_triggers(bon, child_metas, &module_name) }) .collect::>(); @@ -241,7 +254,7 @@ impl CompletionsSchema { // to avoid Rust Analyzer from providing completions for the // `self` keyword in the `use` statement. It works because // `use self::self` is not a valid syntax. - use ::bon::private::ide #(::#module_name)* ::*; + use #bon::__private::ide #(::#module_name)* ::*; use self::{ #( #paths as _, )* }; } diff --git a/bon-macros/src/util/mod.rs b/bon-macros/src/util/mod.rs index e80aaec6..5c2e8ae0 100644 --- a/bon-macros/src/util/mod.rs +++ b/bon-macros/src/util/mod.rs @@ -1,4 +1,5 @@ mod attrs; +mod expr; mod fn_arg; mod generic_param; mod ident; @@ -30,6 +31,7 @@ pub(crate) mod prelude { pub(crate) type Result = std::result::Result; pub(crate) use super::attrs::AttributeExt; + pub(crate) use super::expr::ExprExt; pub(crate) use super::fn_arg::FnArgExt; pub(crate) use super::generic_param::GenericParamExt; pub(crate) use super::ident::IdentExt; diff --git a/bon-macros/src/util/path.rs b/bon-macros/src/util/path.rs index db4dc282..002a633f 100644 --- a/bon-macros/src/util/path.rs +++ b/bon-macros/src/util/path.rs @@ -1,6 +1,11 @@ +use crate::util::prelude::*; + pub(crate) trait PathExt { /// Check if the path starts with the given segment. fn starts_with_segment(&self, desired_segment: &str) -> bool; + + /// Returns an error if this path has some generic arguments. + fn require_mod_style(&self) -> Result; } impl PathExt for syn::Path { @@ -10,4 +15,16 @@ impl PathExt for syn::Path { .map(|first| first.ident == desired_segment) .unwrap_or(false) } + + fn require_mod_style(&self) -> Result { + if self + .segments + .iter() + .any(|seg| seg.arguments != syn::PathArguments::None) + { + bail!(self, "expected a simple path e.g. `foo::bar`"); + } + + Ok(()) + } } diff --git a/bon/src/private/cfg_eval.rs b/bon/src/__private/cfg_eval.rs similarity index 100% rename from bon/src/private/cfg_eval.rs rename to bon/src/__private/cfg_eval.rs diff --git a/bon/src/private/derives.rs b/bon/src/__private/derives.rs similarity index 100% rename from bon/src/private/derives.rs rename to bon/src/__private/derives.rs diff --git a/bon/src/private/ide.rs b/bon/src/__private/ide.rs similarity index 53% rename from bon/src/private/ide.rs rename to bon/src/__private/ide.rs index 05977ba8..7d06a6f1 100644 --- a/bon/src/private/ide.rs +++ b/bon/src/__private/ide.rs @@ -8,6 +8,39 @@ pub mod builder_top_level { use super::*; + /// See the docs at + pub const builder_type: Option = None; + + pub mod builder_type { + use super::*; + + /// See the docs at + pub const name: Identifier = Identifier; + + /// See the docs at + pub const vis: VisibilityString = VisibilityString; + + /// See the docs at + pub const doc: DocComments = DocComments; + } + + /// See the docs at + pub const finish_fn: Option = None; + + /// See the docs at + pub mod finish_fn { + use super::*; + + /// See the docs at + pub const name: Identifier = Identifier; + + /// See the docs at + pub const vis: VisibilityString = VisibilityString; + + /// See the docs at + pub const doc: DocComments = DocComments; + } + /// See the docs at pub const start_fn: Option = None; @@ -20,26 +53,26 @@ pub mod builder_top_level { /// See the docs at pub const vis: VisibilityString = VisibilityString; - } - /// See the docs at - pub const finish_fn: Option = None; - - /// See the docs at - pub const builder_type: Option = None; + /// See the docs at + pub const doc: DocComments = DocComments; + } - /// See the docs at - pub const expose_positional_fn: Option = None; + /// See the docs at + pub const state_mod: Option = None; - /// See the docs at - pub mod expose_positional_fn { + /// See the docs at + pub mod state_mod { use super::*; - /// See the docs at + /// See the docs at pub const name: Identifier = Identifier; - /// See the docs at - pub const vis: Option = None; + /// See the docs at + pub const vis: VisibilityString = VisibilityString; + + /// See the docs at + pub const doc: DocComments = DocComments; } /// See the docs at @@ -58,6 +91,13 @@ pub mod builder_top_level { /// See the docs at pub use core::clone::Clone; } + + /// The real name of this parameter is `crate` (without the underscore). + /// It's hinted with an underscore due to the limitations of the current + /// completions limitation. This will be fixed in the future. + /// + /// See the docs at + pub const crate_: Option = None; } /// Visibility inside of a string literal. Empty string means private visibility. @@ -76,3 +116,13 @@ pub struct Identifier; /// The presence of this attribute enables the behavior. The attribute has no value. pub struct Flag; + +/// Documentation comments using the syntax `/// doc comment here`. +/// +/// [Rust reference](https://doc.rust-lang.org/reference/comments.html#doc-comments) +pub struct DocComments; + +/// Simple path that is valid in a `use` statement. Example: `foo::bar::baz`. +/// +/// [Rust reference](https://doc.rust-lang.org/reference/paths.html?highlight=path#simple-paths) +pub struct Path; diff --git a/bon/src/private/mod.rs b/bon/src/__private/mod.rs similarity index 100% rename from bon/src/private/mod.rs rename to bon/src/__private/mod.rs diff --git a/bon/src/builder_state.rs b/bon/src/builder_state.rs index 4336eebb..14dfca27 100644 --- a/bon/src/builder_state.rs +++ b/bon/src/builder_state.rs @@ -1,5 +1,5 @@ //! The items here are intentionally defined in a private module not inside of the -//! [`crate::private`] module. This is because that module is marked with `#[deprecated]` +//! [`crate::__private`] module. This is because that module is marked with `#[deprecated]` //! which makes all items defined in that module also deprecated. //! //! This is not the desired behavior for the items defined here. They are not deprecated, @@ -7,7 +7,7 @@ //! them through the `bon` crate. Instead, they should use the re-exports from the state //! module generated for the builder. -use crate::private::{Sealed, Set, Unset}; +use crate::__private::{Sealed, Set, Unset}; /// Marker trait that indicates that the member is set, i.e. at least /// one of its setters was called. diff --git a/bon/src/collections.rs b/bon/src/collections.rs index b5b0b186..3cbc2d85 100644 --- a/bon/src/collections.rs +++ b/bon/src/collections.rs @@ -29,8 +29,8 @@ #[cfg_attr(docsrs, doc(cfg(any(feature = "std", feature = "alloc"))))] #[allow(edition_2024_expr_fragment_specifier)] macro_rules! vec { - () => ($crate::private::alloc::vec::Vec::new()); - ($($item:expr),+ $(,)?) => ($crate::private::alloc::vec![$(::core::convert::Into::into($item)),+ ]); + () => ($crate::__private::alloc::vec::Vec::new()); + ($($item:expr),+ $(,)?) => ($crate::__private::alloc::vec![$(::core::convert::Into::into($item)),+ ]); } /// Creates a fixed-size array literal with each element converted with [`Into`]. @@ -74,7 +74,7 @@ macro_rules! arr { #[cfg(test)] mod tests { #[cfg(feature = "alloc")] - use crate::private::alloc::{string::String, vec::Vec}; + use crate::__private::alloc::{string::String, vec::Vec}; use core::num::NonZeroU8; #[cfg(feature = "alloc")] diff --git a/bon/src/lib.rs b/bon/src/lib.rs index cd7349dd..6718fdde 100644 --- a/bon/src/lib.rs +++ b/bon/src/lib.rs @@ -18,11 +18,11 @@ pub use bon_macros::{bon, builder, map, set, Builder}; mod collections; #[doc(hidden)] -#[deprecated = "the items from the `bon::private` module are an implementation detail; \ +#[deprecated = "the items from the `bon::__private` module are an implementation detail; \ they should not be used directly; if you found a need for this, then you are probably \ doing something wrong; feel free to open an issue/discussion in our GitHub repository \ (https://github.com/elastio/bon) or ask for help in our Discord server \ (https://discord.gg/QcBYSamw4c)"] -pub mod private; +pub mod __private; mod builder_state; diff --git a/bon/tests/integration/builder/attr_bon.rs b/bon/tests/integration/builder/attr_bon.rs new file mode 100644 index 00000000..77651467 --- /dev/null +++ b/bon/tests/integration/builder/attr_bon.rs @@ -0,0 +1,35 @@ +use crate::prelude::*; + +#[test] +fn builder_method_special_case() { + struct Sut; + + #[bon] + impl Sut { + #[builder] + fn builder() {} + } + + let _: SutBuilder = Sut::builder(); + let builder: SutBuilder = Sut::builder(); + + builder.build(); +} + +#[test] +fn builder_start_fn_special_case() { + struct Sut; + + #[bon] + impl Sut { + #[builder(start_fn = builder)] + fn some_other_name() {} + } + + let _: SutBuilder = Sut::builder(); + let builder: SutBuilder = Sut::builder(); + + builder.build(); + + Sut::some_other_name(); +} diff --git a/bon/tests/integration/builder/attr_crate.rs b/bon/tests/integration/builder/attr_crate.rs new file mode 100644 index 00000000..740391c4 --- /dev/null +++ b/bon/tests/integration/builder/attr_crate.rs @@ -0,0 +1,122 @@ +use crate::prelude::*; +use bon as lyra; + +#[test] +fn test_struct() { + { + #[derive(Builder)] + #[builder( + crate = crate::builder::attr_crate::lyra, + derive(Debug, Clone) + )] + struct Sut { + _a: u32, + _b: u32, + } + + let _ = Sut::builder().a(1).b(2).build(); + } + { + macro_rules! in_macro { + () => { + #[derive(Builder)] + #[builder(crate = $crate::builder::attr_crate::lyra, derive(Debug, Clone))] + struct Sut { + _a: u32, + _b: u32, + } + }; + } + in_macro!(); + + let _ = Sut::builder().a(1).b(2).build(); + } + { + #[derive(Builder)] + #[builder( + crate = ::bon, + derive(Debug, Clone) + )] + struct Sut { + _a: u32, + _b: u32, + } + + let _ = Sut::builder().a(1).b(2).build(); + } +} + +#[test] +fn test_free_fn() { + { + #[builder( + crate = crate::builder::attr_crate::lyra, + derive(Debug, Clone) + )] + fn sut(_a: u32, _b: u32) {} + + sut().a(1).b(2).call(); + } + { + macro_rules! in_macro { + () => { + #[builder(crate = $crate::builder::attr_crate::lyra, derive(Debug, Clone))] + fn sut(_a: u32, _b: u32) {} + }; + } + in_macro!(); + + sut().a(1).b(2).call(); + } + { + #[builder( + crate = ::bon, + derive(Debug, Clone) + )] + fn sut(_a: u32, _b: u32) {} + + sut().a(1).b(2).call(); + } +} + +#[test] +fn test_assoc_method() { + { + struct Sut; + + #[bon(crate = crate::builder::attr_crate::lyra)] + impl Sut { + #[builder(derive(Debug, Clone))] + fn sut(_a: u32, _b: u32) {} + } + + Sut::sut().a(1).b(2).call(); + } + { + macro_rules! in_macro { + () => { + struct Sut; + + #[bon(crate = $crate::builder::attr_crate::lyra)] + impl Sut { + #[builder(derive(Debug, Clone))] + fn sut(_a: u32, _b: u32) {} + } + }; + } + in_macro!(); + + Sut::sut().a(1).b(2).call(); + } + { + struct Sut; + + #[bon(crate = ::bon)] + impl Sut { + #[builder(derive(Debug, Clone))] + fn sut(_a: u32, _b: u32) {} + } + + Sut::sut().a(1).b(2).call(); + } +} diff --git a/bon/tests/integration/builder/attr_expose_positional_fn.rs b/bon/tests/integration/builder/attr_expose_positional_fn.rs deleted file mode 100644 index 0a1e892e..00000000 --- a/bon/tests/integration/builder/attr_expose_positional_fn.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::prelude::*; -use core::fmt; - -#[test] -fn method_new_doesnt_require_a_value_for_name() { - struct Sut; - - #[bon] - impl Sut { - #[builder(expose_positional_fn)] - fn new() -> Self { - Self - } - } - - let _: Sut = Sut::builder().build(); - let _: Sut = Sut::new(); - - #[allow(clippy::items_after_statements)] - struct Sut2; - - #[bon] - impl Sut2 { - #[builder(expose_positional_fn(vis = "pub(crate)"))] - fn new() -> Self { - Self - } - } - - let _: Sut2 = Sut2::builder().build(); - let _: Sut2 = Sut2::new(); -} - -#[test] -fn with_nested_params() { - #[builder(expose_positional_fn(name = positional))] - fn sut(arg1: bool, arg2: u32) -> impl fmt::Debug { - (arg1, arg2) - } - - assert_debug_eq(positional(true, 42), expect!["(true, 42)"]); -} - -#[test] -fn simple() { - #[builder(expose_positional_fn = positional)] - fn sut(arg1: u32) -> u32 { - arg1 - } - - assert_debug_eq(positional(42), expect!["42"]); -} diff --git a/bon/tests/integration/builder/attr_top_level_start_fn.rs b/bon/tests/integration/builder/attr_top_level_start_fn.rs new file mode 100644 index 00000000..7e972bcc --- /dev/null +++ b/bon/tests/integration/builder/attr_top_level_start_fn.rs @@ -0,0 +1,92 @@ +use crate::prelude::*; +use core::fmt; + +#[test] +fn test_assoc_method() { + { + struct Sut; + + #[bon] + impl Sut { + #[builder(start_fn = builder)] + fn new() -> Self { + Self + } + + #[builder(start_fn = regular_builder)] + fn regular() {} + } + + let builder: SutBuilder = Sut::builder(); + let _: Sut = builder.build(); + let _: Sut = Sut::new(); + + Sut::regular_builder().call(); + Sut::regular(); + } + + { + pub(crate) struct Sut; + + #[bon] + impl Sut { + #[builder(start_fn(name = builder, vis = ""))] + pub(crate) fn new() -> Self { + Self + } + + #[builder(start_fn(name = regular_builder, vis = ""))] + pub(crate) fn regular() {} + } + + let builder: SutBuilder = Sut::builder(); + let _: Sut = builder.build(); + let _: Sut = Sut::new(); + + Sut::regular_builder().call(); + Sut::regular(); + } +} + +#[test] +fn test_free_fn() { + { + #[builder(start_fn(name = sut_builder))] + fn sut(arg1: bool, arg2: u32) -> impl fmt::Debug { + (arg1, arg2) + } + + assert_debug_eq(sut(true, 42), expect!["(true, 42)"]); + } + + { + #[builder(start_fn = sut_builder)] + fn sut(arg1: u32) -> u32 { + arg1 + } + + assert_debug_eq(sut(42), expect!["42"]); + } + + { + #[builder(start_fn(name = sut_builder, vis = ""))] + fn sut(arg1: u32) -> u32 { + arg1 + } + + assert_debug_eq(sut(42), expect!["42"]); + } + + { + /// Docs on `sut` + #[builder(start_fn(name = sut_builder, doc { + /// Docs on `sut_builder` + }))] + fn sut(arg1: u32) -> u32 { + arg1 + } + + assert_debug_eq(sut_builder().arg1(42).call(), expect!["42"]); + assert_debug_eq(sut(42), expect!["42"]); + } +} diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index bdf83a19..c0663186 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -1,11 +1,13 @@ +mod attr_bon; +mod attr_crate; mod attr_default; mod attr_derive; -mod attr_expose_positional_fn; mod attr_into; mod attr_on; mod attr_overwritable; mod attr_setters; mod attr_skip; +mod attr_top_level_start_fn; mod attr_transparent; mod attr_with; mod cfgs; @@ -109,7 +111,7 @@ fn constructor() { #[bon] impl Counter { - #[builder(expose_positional_fn = new)] + #[builder(start_fn = builder)] fn new(initial: Option) -> Self { Self { val: initial.unwrap_or_default(), diff --git a/bon/tests/integration/ui/compile_fail/attr_bon.rs b/bon/tests/integration/ui/compile_fail/attr_bon.rs index c84acc9e..e810dc8d 100644 --- a/bon/tests/integration/ui/compile_fail/attr_bon.rs +++ b/bon/tests/integration/ui/compile_fail/attr_bon.rs @@ -16,5 +16,15 @@ impl BuilderAttrOnReceiver { fn sut(#[builder] &self) {} } +struct NoBuilderMethods; + +#[bon] +impl NoBuilderMethods { + fn not_builder1() {} + fn not_builder2(&self) {} + + const NOT_BUILDER: () = (); +} + fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_bon.stderr b/bon/tests/integration/ui/compile_fail/attr_bon.stderr index 28613d73..2d9962db 100644 --- a/bon/tests/integration/ui/compile_fail/attr_bon.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_bon.stderr @@ -1,4 +1,4 @@ -error: `#[bon]` attribute does not accept any parameters yet, but it will in future releases +error: Unknown field: `attrs` --> tests/integration/ui/compile_fail/attr_bon.rs:5:7 | 5 | #[bon(attrs)] @@ -9,3 +9,11 @@ error: #[builder] attributes on the receiver are not supported | 16 | fn sut(#[builder] &self) {} | ^ + +error: there are no #[builder] functions in the impl block, so there is no need for a #[bon] attribute here + --> tests/integration/ui/compile_fail/attr_bon.rs:21:1 + | +21 | #[bon] + | ^^^^^^ + | + = note: this error originates in the attribute macro `bon` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/attr_builder.stderr b/bon/tests/integration/ui/compile_fail/attr_builder.stderr index 6996c6e6..da7ad31c 100644 --- a/bon/tests/integration/ui/compile_fail/attr_builder.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_builder.stderr @@ -10,7 +10,7 @@ error: expected parameters in parentheses 8 | #[builder()] | ^^ -error: this empty `#[builder]` attribute is unexpected; remove it or pass parameters in parentheses: `#[builder(...)]` +error: this empty `builder` attribute is unexpected; remove it or pass parameters in parentheses: `builder(...)` --> tests/integration/ui/compile_fail/attr_builder.rs:13:7 | 13 | #[builder] @@ -22,7 +22,7 @@ error: expected parameters in parentheses 19 | #[builder()] | ^^ -error: this empty `#[builder]` attribute is unexpected; remove it or pass parameters in parentheses: `#[builder(...)]` +error: this empty `builder` attribute is unexpected; remove it or pass parameters in parentheses: `builder(...)` --> tests/integration/ui/compile_fail/attr_builder.rs:24:41 | 24 | fn fn_empty_member_level_builder_attr(#[builder] _x: u32) {} @@ -34,7 +34,7 @@ error: expected parameters in parentheses 27 | fn fn_empty_member_level_builder_attr_with_parens(#[builder()] _x: u32) {} | ^^ -error: this empty `#[builder]` attribute is unexpected; remove it or pass parameters in parentheses: `#[builder(...)]` +error: this empty `builder` attribute is unexpected; remove it or pass parameters in parentheses: `builder(...)` --> tests/integration/ui/compile_fail/attr_builder.rs:34:42 | 34 | fn empty_member_level_builder_attr(#[builder] _x: u32) {} diff --git a/bon/tests/integration/ui/compile_fail/attr_crate.rs b/bon/tests/integration/ui/compile_fail/attr_crate.rs new file mode 100644 index 00000000..3560cef6 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_crate.rs @@ -0,0 +1,52 @@ +use bon::{bon, builder, Builder}; + +#[derive(Builder)] +#[builder(crate = self::bon)] +struct Relative1 {} + +#[derive(Builder)] +#[builder(crate = super::bon)] +struct Relative2 {} + +#[derive(Builder)] +#[builder(crate = bon)] +struct Relative3 {} + +#[builder(crate = self::bon)] +fn relative_1() {} + +#[builder(crate = super::bon)] +fn relative_2() {} + +#[builder(crate = bon)] +fn relative_3() {} + +struct CrateAttrInMethod; + +#[bon] +impl CrateAttrInMethod { + #[builder(crate = ::bon)] + fn method() {} +} + +struct Relative; + +#[bon(crate = self::bon)] +impl Relative { + #[builder] + fn method1() {} +} + +#[bon(crate = super::bon)] +impl Relative { + #[builder] + fn method2() {} +} + +#[bon(crate = bon)] +impl Relative { + #[builder] + fn method3() {} +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_crate.stderr b/bon/tests/integration/ui/compile_fail/attr_crate.stderr new file mode 100644 index 00000000..9225dbbb --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_crate.stderr @@ -0,0 +1,59 @@ +error: path must not be relative; specify the path that starts with `crate::` instead; if you want to refer to a reexport from an external crate then use a leading colon like `::crate_name::reexport::path::bon` + --> tests/integration/ui/compile_fail/attr_crate.rs:4:19 + | +4 | #[builder(crate = self::bon)] + | ^^^^ + +error: path must not be relative; specify the path that starts with `crate::` instead; if you want to refer to a reexport from an external crate then use a leading colon like `::crate_name::reexport::path::bon` + --> tests/integration/ui/compile_fail/attr_crate.rs:8:19 + | +8 | #[builder(crate = super::bon)] + | ^^^^^ + +error: path must be absolute; if you want to refer to a reexport from an external crate then add a leading colon like `::bon`; if the path leads to a module in the current crate, then specify the absolute path with `crate` like `crate::reexport::path::bon` or `$crate::reexport::path::bon` (if within a macro) + --> tests/integration/ui/compile_fail/attr_crate.rs:12:19 + | +12 | #[builder(crate = bon)] + | ^^^ + +error: path must not be relative; specify the path that starts with `crate::` instead; if you want to refer to a reexport from an external crate then use a leading colon like `::crate_name::reexport::path::bon` + --> tests/integration/ui/compile_fail/attr_crate.rs:15:19 + | +15 | #[builder(crate = self::bon)] + | ^^^^ + +error: path must not be relative; specify the path that starts with `crate::` instead; if you want to refer to a reexport from an external crate then use a leading colon like `::crate_name::reexport::path::bon` + --> tests/integration/ui/compile_fail/attr_crate.rs:18:19 + | +18 | #[builder(crate = super::bon)] + | ^^^^^ + +error: path must be absolute; if you want to refer to a reexport from an external crate then add a leading colon like `::bon`; if the path leads to a module in the current crate, then specify the absolute path with `crate` like `crate::reexport::path::bon` or `$crate::reexport::path::bon` (if within a macro) + --> tests/integration/ui/compile_fail/attr_crate.rs:21:19 + | +21 | #[builder(crate = bon)] + | ^^^ + +error: `crate` parameter should be specified via `#[bon(crate = path::to::bon)]` when impl block syntax is used; no need to specify it in the method's `#[builder]` attribute + --> tests/integration/ui/compile_fail/attr_crate.rs:28:23 + | +28 | #[builder(crate = ::bon)] + | ^ + +error: path must not be relative; specify the path that starts with `crate::` instead; if you want to refer to a reexport from an external crate then use a leading colon like `::crate_name::reexport::path::bon` + --> tests/integration/ui/compile_fail/attr_crate.rs:34:15 + | +34 | #[bon(crate = self::bon)] + | ^^^^ + +error: path must not be relative; specify the path that starts with `crate::` instead; if you want to refer to a reexport from an external crate then use a leading colon like `::crate_name::reexport::path::bon` + --> tests/integration/ui/compile_fail/attr_crate.rs:40:15 + | +40 | #[bon(crate = super::bon)] + | ^^^^^ + +error: path must be absolute; if you want to refer to a reexport from an external crate then add a leading colon like `::bon`; if the path leads to a module in the current crate, then specify the absolute path with `crate` like `crate::reexport::path::bon` or `$crate::reexport::path::bon` (if within a macro) + --> tests/integration/ui/compile_fail/attr_crate.rs:46:15 + | +46 | #[bon(crate = bon)] + | ^^^ diff --git a/bon/tests/integration/ui/compile_fail/attr_derive.stderr b/bon/tests/integration/ui/compile_fail/attr_derive.stderr index fc60a841..99d8dc89 100644 --- a/bon/tests/integration/ui/compile_fail/attr_derive.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_derive.stderr @@ -35,7 +35,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -52,7 +52,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -69,7 +69,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -88,7 +88,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -107,7 +107,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -126,7 +126,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -145,7 +145,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -174,7 +174,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -191,7 +191,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -208,7 +208,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -227,7 +227,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -246,7 +246,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -265,7 +265,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -284,7 +284,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -319,7 +319,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -336,7 +336,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -353,7 +353,7 @@ error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` | note: required by a bound in `clone_member` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn clone_member(member: &Option) -> Option { | ^^^^^ required by this bound in `clone_member` @@ -372,7 +372,7 @@ error[E0277]: `StructContainsNonTrait` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `StructContainsNonTrait` = note: add `#[derive(Debug)]` to `StructContainsNonTrait` or manually `impl Debug for StructContainsNonTrait` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -386,7 +386,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -405,7 +405,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -424,7 +424,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` @@ -443,7 +443,7 @@ error[E0277]: `NoTraitImpls` doesn't implement `Debug` = help: the trait `Debug` is not implemented for `NoTraitImpls` = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` note: required by a bound in `as_dyn_debug` - --> src/private/derives.rs + --> src/__private/derives.rs | | pub fn as_dyn_debug(member: &T) -> &dyn Debug { | ^^^^^ required by this bound in `as_dyn_debug` diff --git a/bon/tests/integration/ui/compile_fail/attr_on.stderr b/bon/tests/integration/ui/compile_fail/attr_on.stderr index e7ccc8c3..500ecc73 100644 --- a/bon/tests/integration/ui/compile_fail/attr_on.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_on.stderr @@ -22,7 +22,7 @@ error: nested attributes are not allowed in the type pattern of #[builder(on(typ 12 | #[builder(on(fn(#[attr] a: u32), into))] | ^ -error: this empty `#[on]` attribute is unexpected; remove it or pass parameters in parentheses: `#[on(...)]` +error: this empty `on` attribute is unexpected; remove it or pass parameters in parentheses: `on(...)` --> tests/integration/ui/compile_fail/attr_on.rs:15:11 | 15 | #[builder(on)] diff --git a/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.rs b/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.rs deleted file mode 100644 index 7d643da1..00000000 --- a/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.rs +++ /dev/null @@ -1,11 +0,0 @@ -use bon::Builder; - -#[derive(Builder)] -#[builder(start_fn())] -struct EmptyStartFn {} - -#[derive(Builder)] -#[builder(finish_fn())] -struct EmptyFinisFn {} - -fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.stderr b/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.stderr deleted file mode 100644 index e8a17698..00000000 --- a/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: expected parameters in parentheses - --> tests/integration/ui/compile_fail/attr_start_finish_fn.rs:4:19 - | -4 | #[builder(start_fn())] - | ^^ - -error: expected parameters in parentheses - --> tests/integration/ui/compile_fail/attr_start_finish_fn.rs:8:20 - | -8 | #[builder(finish_fn())] - | ^^ diff --git a/bon/tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs b/bon/tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs new file mode 100644 index 00000000..605c0732 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs @@ -0,0 +1,74 @@ +use bon::{bon, builder, Builder}; + +#[derive(Builder)] +#[builder(start_fn())] +struct EmptyStartFn {} + +#[derive(Builder)] +#[builder(finish_fn())] +struct EmptyFinisFn {} + +#[derive(Builder)] +#[builder(start_fn)] +struct BareStartFnAttrOnStruct {} + +#[builder(start_fn)] +fn bare_start_fn_on_free_function() {} + +#[builder(start_fn())] +fn empty_paren_start_fn_on_free_function() {} + +#[builder(start_fn(vis = ""))] +fn missing_name_for_start_fn_on_free_function1() {} + +#[builder(start_fn(doc {}))] +fn missing_name_for_start_fn_on_free_function2() {} + +struct AssocCtx; +struct AssocCtx2; +struct AssocCtx3; +struct AssocCtx4; + +#[bon] +impl AssocCtx { + #[builder(start_fn)] + fn new() {} +} + +#[bon] +impl AssocCtx2 { + #[builder(start_fn())] + fn new() {} +} + +#[bon] +impl AssocCtx3 { + #[builder(start_fn(vis = ""))] + fn new() {} +} + +#[bon] +impl AssocCtx4 { + #[builder(start_fn(doc {}))] + fn new() {} +} + +#[bon] +impl AssocCtx { + #[builder(start_fn)] + fn bare_start_fn_on_method() {} +} + +#[bon] +impl AssocCtx { + #[builder(start_fn(vis = ""))] + fn missing_name_for_start_fn_on_method1() {} +} + +#[bon] +impl AssocCtx { + #[builder(start_fn(doc {}))] + fn missing_name_for_start_fn_on_method2() {} +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.stderr b/bon/tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.stderr new file mode 100644 index 00000000..ed04a4b6 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.stderr @@ -0,0 +1,83 @@ +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:4:19 + | +4 | #[builder(start_fn())] + | ^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:8:20 + | +8 | #[builder(finish_fn())] + | ^^ + +error: this empty `start_fn` attribute is unexpected; remove it or pass parameters in parentheses: `start_fn(...)` + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:12:11 + | +12 | #[builder(start_fn)] + | ^^^^^^^^ + +error: this empty `start_fn` attribute is unexpected; remove it or pass parameters in parentheses: `start_fn(...)` + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:15:11 + | +15 | #[builder(start_fn)] + | ^^^^^^^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:18:19 + | +18 | #[builder(start_fn())] + | ^^ + +error: #[builder(start_fn(vis))] requires that you also specify #[builder(start_fn(name))] which makes the starting function not to replace the positional function under the #[builder] attribute; by default (without the explicit #[builder(start_fn(name))]) the name, visibility and documentation of the positional function are all copied to the starting function, and the positional function under the #[builder] attribute becomes private with #[doc(hidden)] and it's renamed (the name is not guaranteed to be stable) to make it inaccessible even within the current module + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:21:20 + | +21 | #[builder(start_fn(vis = ""))] + | ^^^ + +error: #[builder(start_fn(doc))] requires that you also specify #[builder(start_fn(name))] which makes the starting function not to replace the positional function under the #[builder] attribute; by default (without the explicit #[builder(start_fn(name))]) the name, visibility and documentation of the positional function are all copied to the starting function, and the positional function under the #[builder] attribute becomes private with #[doc(hidden)] and it's renamed (the name is not guaranteed to be stable) to make it inaccessible even within the current module + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:24:20 + | +24 | #[builder(start_fn(doc {}))] + | ^^^ + +error: this empty `start_fn` attribute is unexpected; remove it or pass parameters in parentheses: `start_fn(...)` + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:34:15 + | +34 | #[builder(start_fn)] + | ^^^^^^^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:40:23 + | +40 | #[builder(start_fn())] + | ^^ + +error: #[builder(start_fn(vis))] requires that you also specify #[builder(start_fn(name))] which makes the starting function not to replace the positional function under the #[builder] attribute; by default (without the explicit #[builder(start_fn(name))]) the name, visibility and documentation of the positional function are all copied to the starting function, and the positional function under the #[builder] attribute becomes private with #[doc(hidden)] and it's renamed (the name is not guaranteed to be stable) to make it inaccessible even within the current module + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:46:24 + | +46 | #[builder(start_fn(vis = ""))] + | ^^^ + +error: #[builder(start_fn(doc))] requires that you also specify #[builder(start_fn(name))] which makes the starting function not to replace the positional function under the #[builder] attribute; by default (without the explicit #[builder(start_fn(name))]) the name, visibility and documentation of the positional function are all copied to the starting function, and the positional function under the #[builder] attribute becomes private with #[doc(hidden)] and it's renamed (the name is not guaranteed to be stable) to make it inaccessible even within the current module + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:52:24 + | +52 | #[builder(start_fn(doc {}))] + | ^^^ + +error: this empty `start_fn` attribute is unexpected; remove it or pass parameters in parentheses: `start_fn(...)` + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:58:15 + | +58 | #[builder(start_fn)] + | ^^^^^^^^ + +error: #[builder(start_fn(vis))] requires that you also specify #[builder(start_fn(name))] which makes the starting function not to replace the positional function under the #[builder] attribute; by default (without the explicit #[builder(start_fn(name))]) the name, visibility and documentation of the positional function are all copied to the starting function, and the positional function under the #[builder] attribute becomes private with #[doc(hidden)] and it's renamed (the name is not guaranteed to be stable) to make it inaccessible even within the current module + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:64:24 + | +64 | #[builder(start_fn(vis = ""))] + | ^^^ + +error: #[builder(start_fn(doc))] requires that you also specify #[builder(start_fn(name))] which makes the starting function not to replace the positional function under the #[builder] attribute; by default (without the explicit #[builder(start_fn(name))]) the name, visibility and documentation of the positional function are all copied to the starting function, and the positional function under the #[builder] attribute becomes private with #[doc(hidden)] and it's renamed (the name is not guaranteed to be stable) to make it inaccessible even within the current module + --> tests/integration/ui/compile_fail/attr_top_level_start_finish_fn.rs:70:24 + | +70 | #[builder(start_fn(doc {}))] + | ^^^ diff --git a/bon/tests/integration/ui/compile_fail/wrong_delimiters.rs b/bon/tests/integration/ui/compile_fail/wrong_delimiters.rs index 1ba55d70..f421702d 100644 --- a/bon/tests/integration/ui/compile_fail/wrong_delimiters.rs +++ b/bon/tests/integration/ui/compile_fail/wrong_delimiters.rs @@ -33,10 +33,10 @@ struct SquareBracketsInFieldSetters { #[derive(bon::Builder)] #[builder( - builder_type(docs[]), - state_mod(docs[]), - start_fn(docs[]), - finish_fn(docs[]), + builder_type(doc[]), + state_mod(doc[]), + start_fn(doc[]), + finish_fn(doc[]), )] struct SquareBracketsInFieldDoc { #[builder(setters(doc[]))] diff --git a/bon/tests/integration/ui/compile_fail/wrong_delimiters.stderr b/bon/tests/integration/ui/compile_fail/wrong_delimiters.stderr index ddf1778b..e3c8effe 100644 --- a/bon/tests/integration/ui/compile_fail/wrong_delimiters.stderr +++ b/bon/tests/integration/ui/compile_fail/wrong_delimiters.stderr @@ -1,9 +1,3 @@ -error: wrong delimiter, expected parentheses e.g. `start_fn(...)`, but got curly braces: `start_fn{...}` - --> tests/integration/ui/compile_fail/wrong_delimiters.rs:5:5 - | -5 | start_fn{}, - | ^^^^^^^^ - error: wrong delimiter, expected parentheses e.g. `builder_type(...)`, but got curly braces: `builder_type{...}` --> tests/integration/ui/compile_fail/wrong_delimiters.rs:3:5 | @@ -16,6 +10,12 @@ error: wrong delimiter, expected parentheses e.g. `state_mod(...)`, but got curl 4 | state_mod{}, | ^^^^^^^^^ +error: wrong delimiter, expected parentheses e.g. `start_fn(...)`, but got curly braces: `start_fn{...}` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:5:5 + | +5 | start_fn{}, + | ^^^^^^^^ + error: wrong delimiter, expected parentheses e.g. `finish_fn(...)`, but got curly braces: `finish_fn{...}` --> tests/integration/ui/compile_fail/wrong_delimiters.rs:6:5 | @@ -28,12 +28,6 @@ error: wrong delimiter, expected parentheses e.g. `setters(...)`, but got curly 12 | #[builder(setters{})] | ^^^^^^^ -error: wrong delimiter, expected parentheses e.g. `start_fn(...)`, but got square brackets: `start_fn[...]` - --> tests/integration/ui/compile_fail/wrong_delimiters.rs:20:5 - | -20 | start_fn[doc[]], - | ^^^^^^^^ - error: wrong delimiter, expected parentheses e.g. `builder_type(...)`, but got square brackets: `builder_type[...]` --> tests/integration/ui/compile_fail/wrong_delimiters.rs:18:5 | @@ -46,6 +40,12 @@ error: wrong delimiter, expected parentheses e.g. `state_mod(...)`, but got squa 19 | state_mod[doc[]], | ^^^^^^^^^ +error: wrong delimiter, expected parentheses e.g. `start_fn(...)`, but got square brackets: `start_fn[...]` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:20:5 + | +20 | start_fn[doc[]], + | ^^^^^^^^ + error: wrong delimiter, expected parentheses e.g. `finish_fn(...)`, but got square brackets: `finish_fn[...]` --> tests/integration/ui/compile_fail/wrong_delimiters.rs:21:5 | @@ -58,35 +58,29 @@ error: wrong delimiter, expected parentheses e.g. `setters(...)`, but got square 30 | #[builder(setters[])] | ^^^^^^^ -error: Unknown field: `docs`. Did you mean `doc`? - --> tests/integration/ui/compile_fail/wrong_delimiters.rs:38:14 - | -38 | start_fn(docs[]), - | ^^^^ - -error: Unknown field: `docs`. Did you mean `doc`? +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got square brackets: `doc[...]` --> tests/integration/ui/compile_fail/wrong_delimiters.rs:36:18 | -36 | builder_type(docs[]), - | ^^^^ +36 | builder_type(doc[]), + | ^^^ -error: Unknown field: `docs`. Did you mean `doc`? +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got square brackets: `doc[...]` --> tests/integration/ui/compile_fail/wrong_delimiters.rs:37:15 | -37 | state_mod(docs[]), - | ^^^^ +37 | state_mod(doc[]), + | ^^^ -error: Unknown field: `docs`. Did you mean `doc`? - --> tests/integration/ui/compile_fail/wrong_delimiters.rs:39:15 +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got square brackets: `doc[...]` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:38:14 | -39 | finish_fn(docs[]), - | ^^^^ +38 | start_fn(doc[]), + | ^^^ -error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parentheses: `doc(...)` - --> tests/integration/ui/compile_fail/wrong_delimiters.rs:50:14 +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got square brackets: `doc[...]` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:39:15 | -50 | start_fn(doc()), - | ^^^ +39 | finish_fn(doc[]), + | ^^^ error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parentheses: `doc(...)` --> tests/integration/ui/compile_fail/wrong_delimiters.rs:48:18 @@ -100,6 +94,12 @@ error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parenthes 49 | state_mod(doc()), | ^^^ +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parentheses: `doc(...)` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:50:14 + | +50 | start_fn(doc()), + | ^^^ + error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parentheses: `doc(...)` --> tests/integration/ui/compile_fail/wrong_delimiters.rs:51:15 | diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index da2446c1..21790290 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -156,8 +156,8 @@ export default defineConfig({ link: "/reference/builder", items: [ { - text: "Item attributes", - link: "/reference/builder#item-attributes", + text: "Top-level attributes", + link: "/reference/builder#top-level-attributes", items: [ { text: "builder_type", @@ -175,14 +175,14 @@ export default defineConfig({ text: "finish_fn", link: "/reference/builder#finish-fn", }, - { - text: "start_fn", - link: "/reference/builder#start-fn", - }, { text: "on", link: "/reference/builder#on", }, + { + text: "start_fn", + link: "/reference/builder#start-fn", + }, ], }, { @@ -193,6 +193,10 @@ export default defineConfig({ text: "default", link: "/reference/builder#default", }, + { + text: "finish_fn", + link: "/reference/builder#finish-fn-1", + }, { text: "into", link: "/reference/builder#into", @@ -209,11 +213,6 @@ export default defineConfig({ text: "start_fn", link: "/reference/builder#start-fn-1", }, - { - text: "finish_fn", - link: "/reference/builder#finish-fn-1", - }, - ], }, ], diff --git a/website/reference/builder.md b/website/reference/builder.md index 6784f223..e5ff9ba3 100644 --- a/website/reference/builder.md +++ b/website/reference/builder.md @@ -6,8 +6,8 @@ outline: [2, 3] You can generate a builder using three different kinds of syntax (struct, free function, associated method). They all share two common groups of attributes. -- [Item attributes](#item-attributes) - apply to a `struct` or `fn` declaration itself. -- [Member attributes](#member-attributes) - apply to a `struct` field or `fn` argument. +- [Top-level attributes](#top-level-attributes) - placed on a `struct` or `fn` declaration. +- [Member attributes](#member-attributes) - placed on a `struct` field or `fn` argument. See examples. Make sure to click through the tabs: @@ -17,7 +17,7 @@ See examples. Make sure to click through the tabs: use bon::Builder; #[derive(Builder)] -#[builder(finish_fn = finish)] // <-- this is an item attribute // [!code highlight] +#[builder(finish_fn = finish)] // <-- this is a top-level attribute // [!code highlight] struct Example { #[builder(default)] // <-- this is a member attribute // [!code highlight] field: u32 @@ -27,7 +27,7 @@ struct Example { ```rust [Free function] use bon::builder; -#[builder(finish_fn = finish)] // <-- this is an item attribute // [!code highlight] +#[builder(finish_fn = finish)] // <-- this is a top-level attribute // [!code highlight] fn example( #[builder(default)] // <-- this is a member attribute // [!code highlight] arg: u32 @@ -41,7 +41,7 @@ struct Example; #[bon] impl Example { - #[builder(finish_fn = finish)] // <-- this is an item attribute // [!code highlight] + #[builder(finish_fn = finish)] // <-- this is a top-level attribute // [!code highlight] fn example( #[builder(default)] // <-- this is a member attribute // [!code highlight] arg: u32 @@ -59,7 +59,7 @@ Most of the attributes apply to all kinds of syntax. However, some of them are o ::: -## Item attributes +## Top-level attributes ### `builder_type` @@ -315,7 +315,7 @@ If `vis` parameter is not specified, then the visibility of the exposed position ::: code-group -```rust [Free function] +```rust ignore [Free function] use bon::builder; #[builder(expose_positional_fn = example_positional)] // [!code highlight] @@ -331,7 +331,7 @@ example() .call(); ``` -```rust [Associated method] +```rust ignore [Associated method] use bon::bon; struct Example; @@ -365,7 +365,7 @@ So when `#[builder]` is placed on a method called `new`, it'll generate a method **Example:** -```rust +```rust ignore use bon::bon; struct Example { @@ -457,57 +457,6 @@ assert_eq!(response, "Some article with id 42"); ::: -### `start_fn` - -**Applies to:** - -Overrides the name and visibility of the associated method that starts the building process, i.e. returns the builder for the struct. - -The default name for this method is `builder`, and the default visibility is the same as the visibility of the struct itself. - -This attribute can take several forms. - -- Simple: `#[builder(start_fn = identifier)]`. Overrides only the name of the "start" method. -- Verbose: `#[builder(start_fn(name = identifier, vis = "visibility"))]`. - Allows overriding both the name and the visibility of the "start" method. - Each key is optional. The `vis` must be specified as a string literal e.g. `"pub(crate)"`, `"pub"` or `""` (empty string means private visibility). - -**Example:** - -::: code-group - -```rust [Simple form] -use bon::Builder; - -#[derive(Builder)] -#[builder(start_fn = init)] // [!code highlight] -struct User { - id: u32 -} - -User::init() // [!code highlight] - .id(42) - .build(); -``` - -```rust [Verbose form] -use bon::Builder; - -// `User::init()` method will have `pub(crate)` visibility // [!code highlight] -// Use `vis = ""` to make it fully private instead // [!code highlight] -#[derive(Builder)] -#[builder(start_fn(name = init, vis = "pub(crate)"))] // [!code highlight] -pub struct User { - id: u32 -} - -User::init() // [!code highlight] - .id(42) - .build(); -``` - -::: - ### `on` **Applies to:** @@ -646,6 +595,57 @@ Example::builder() .build(); ``` +### `start_fn` + +**Applies to:** + +Overrides the name and visibility of the associated method that starts the building process, i.e. returns the builder for the struct. + +The default name for this method is `builder`, and the default visibility is the same as the visibility of the struct itself. + +This attribute can take several forms. + +- Simple: `#[builder(start_fn = identifier)]`. Overrides only the name of the "start" method. +- Verbose: `#[builder(start_fn(name = identifier, vis = "visibility"))]`. + Allows overriding both the name and the visibility of the "start" method. + Each key is optional. The `vis` must be specified as a string literal e.g. `"pub(crate)"`, `"pub"` or `""` (empty string means private visibility). + +**Example:** + +::: code-group + +```rust [Simple form] +use bon::Builder; + +#[derive(Builder)] +#[builder(start_fn = init)] // [!code highlight] +struct User { + id: u32 +} + +User::init() // [!code highlight] + .id(42) + .build(); +``` + +```rust [Verbose form] +use bon::Builder; + +// `User::init()` method will have `pub(crate)` visibility // [!code highlight] +// Use `vis = ""` to make it fully private instead // [!code highlight] +#[derive(Builder)] +#[builder(start_fn(name = init, vis = "pub(crate)"))] // [!code highlight] +pub struct User { + id: u32 +} + +User::init() // [!code highlight] + .id(42) + .build(); +``` + +::: + ## Member attributes ### `default` @@ -859,6 +859,115 @@ The `self` parameter in associated methods is not available to the `default` exp This attribute is incompatible with members of `Option` type, since `Option` already implies the default value of `None`. +### `finish_fn` + +**Applies to:** + +Makes the member a positional argument on the finishing function that consumes the builder and returns the resulting object (for struct syntax) or performs the requested action (for function/method syntax). + +The ordering of members annotated with `#[builder(finish_fn)]` matters! They will appear in the same order relative to each other in the finishing function signature. They must also be declared at the top of the members list strictly after members annotated with [`#[builder(start_fn)]`](#start-fn-1) (if any). + +This ensures a consistent initialization order, and it makes these members available for expressions in `#[builder(default/skip = ...)]` for regular members that follow them. + +::: tip + +Don't confuse this with the top-level [`#[builder(finish_fn = ...)]`](#finish-fn) attribute that can be used to configure the name and visibility of the finishing function. You'll likely want to use it in combination with this member-level attribute to define a better name for the finishing function. + +::: + +**Example:** + +::: code-group + +```rust [Struct field] +use bon::Builder; + +#[derive(Builder)] +// Top-level attribute to give a better name for the finishing function // [!code highlight] +#[builder(finish_fn = sign)] // [!code highlight] +struct Message { + // Member-level attribute to mark the member as a parameter of `sign()` // [!code highlight] + #[builder(finish_fn)] // [!code highlight] + author_first_name: String, + + #[builder(finish_fn)] // [!code highlight] + author_last_name: String, + + payload: String, +} + +let message = Message::builder() + .payload("Bon is great! Give it a ⭐".to_owned()) + .sign("Sweetie".to_owned(), "Drops".to_owned()); + +assert_eq!(message.payload, "Bon is great! Give it a ⭐"); +assert_eq!(message.author_first_name, "Sweetie"); +assert_eq!(message.author_last_name, "Drops"); +``` + +```rust [Free function] +use bon::builder; + +// Top-level attribute to give a better name for the finishing function // [!code highlight] +#[builder(finish_fn = send)] // [!code highlight] +fn message( + // Member-level attribute to mark the member as a parameter of `sign()` // [!code highlight] + #[builder(finish_fn)] // [!code highlight] + receiver_first_name: String, + + #[builder(finish_fn)] // [!code highlight] + receiver_last_name: String, + + payload: String, +) {} + +message() + .payload("Bon is great! Give it a ⭐".to_owned()) + .send("Sweetie".to_owned(), "Drops".to_owned()); +``` + +```rust [Associated method] +use bon::bon; + +struct Chat {} + +#[bon] +impl Chat { + // Top-level attribute to give a better name for the finishing function // [!code highlight] + #[builder(finish_fn = send)] // [!code highlight] + fn message( + &self, + + // Member-level attribute to mark the member as a parameter of `sign()` // [!code highlight] + #[builder(finish_fn)] // [!code highlight] + receiver_first_name: String, + + #[builder(finish_fn)] // [!code highlight] + receiver_last_name: String, + + payload: String, + ) {} +} + +let chat = Chat {}; + +chat.message() + .payload("Bon is great! Give it a ⭐".to_owned()) + .send("Sweetie".to_owned(), "Drops".to_owned()); +``` + +::: + +You can also combine this attribute with [`#[builder(into)]`](#into) or [`#[builder(on(..., into))]`](#on) to add an into conversion for the parameter. + +Importantly, `Into` conversions for such members work slightly differently from the regular (named) members in regard to the `Option` type. The `Option` type gives no additional meaning to the member annotated with `#[builder(finish_fn)]`. Thus, it is matched by the type pattern of `on(..., into)` and wrapped with `impl Into>` as any other type. + +::: tip + +In general, it's not recommended to annotate optional members with `#[builder(finish_fn)]` because you can't omit setting them using the positional function call syntax. + +::: + ### `into` **Applies to:** @@ -1199,115 +1308,6 @@ In general, it's not recommended to annotate optional members with `#[builder(st ::: -### `finish_fn` - -**Applies to:** - -Makes the member a positional argument on the finishing function that consumes the builder and returns the resulting object (for struct syntax) or performs the requested action (for function/method syntax). - -The ordering of members annotated with `#[builder(finish_fn)]` matters! They will appear in the same order relative to each other in the finishing function signature. They must also be declared at the top of the members list strictly after members annotated with [`#[builder(start_fn)]`](#start-fn-1) (if any). - -This ensures a consistent initialization order, and it makes these members available for expressions in `#[builder(default/skip = ...)]` for regular members that follow them. - -::: tip - -Don't confuse this with the top-level [`#[builder(finish_fn = ...)]`](#finish-fn) attribute that can be used to configure the name and visibility of the finishing function. You'll likely want to use it in combination with this member-level attribute to define a better name for the finishing function. - -::: - -**Example:** - -::: code-group - -```rust [Struct field] -use bon::Builder; - -#[derive(Builder)] -// Top-level attribute to give a better name for the finishing function // [!code highlight] -#[builder(finish_fn = sign)] // [!code highlight] -struct Message { - // Member-level attribute to mark the member as a parameter of `sign()` // [!code highlight] - #[builder(finish_fn)] // [!code highlight] - author_first_name: String, - - #[builder(finish_fn)] // [!code highlight] - author_last_name: String, - - payload: String, -} - -let message = Message::builder() - .payload("Bon is great! Give it a ⭐".to_owned()) - .sign("Sweetie".to_owned(), "Drops".to_owned()); - -assert_eq!(message.payload, "Bon is great! Give it a ⭐"); -assert_eq!(message.author_first_name, "Sweetie"); -assert_eq!(message.author_last_name, "Drops"); -``` - -```rust [Free function] -use bon::builder; - -// Top-level attribute to give a better name for the finishing function // [!code highlight] -#[builder(finish_fn = send)] // [!code highlight] -fn message( - // Member-level attribute to mark the member as a parameter of `sign()` // [!code highlight] - #[builder(finish_fn)] // [!code highlight] - receiver_first_name: String, - - #[builder(finish_fn)] // [!code highlight] - receiver_last_name: String, - - payload: String, -) {} - -message() - .payload("Bon is great! Give it a ⭐".to_owned()) - .send("Sweetie".to_owned(), "Drops".to_owned()); -``` - -```rust [Associated method] -use bon::bon; - -struct Chat {} - -#[bon] -impl Chat { - // Top-level attribute to give a better name for the finishing function // [!code highlight] - #[builder(finish_fn = send)] // [!code highlight] - fn message( - &self, - - // Member-level attribute to mark the member as a parameter of `sign()` // [!code highlight] - #[builder(finish_fn)] // [!code highlight] - receiver_first_name: String, - - #[builder(finish_fn)] // [!code highlight] - receiver_last_name: String, - - payload: String, - ) {} -} - -let chat = Chat {}; - -chat.message() - .payload("Bon is great! Give it a ⭐".to_owned()) - .send("Sweetie".to_owned(), "Drops".to_owned()); -``` - -::: - -You can also combine this attribute with [`#[builder(into)]`](#into) or [`#[builder(on(..., into))]`](#on) to add an into conversion for the parameter. - -Importantly, `Into` conversions for such members work slightly differently from the regular (named) members in regard to the `Option` type. The `Option` type gives no additional meaning to the member annotated with `#[builder(finish_fn)]`. Thus, it is matched by the type pattern of `on(..., into)` and wrapped with `impl Into>` as any other type. - -::: tip - -In general, it's not recommended to annotate optional members with `#[builder(finish_fn)]` because you can't omit setting them using the positional function call syntax. - -::: - *[Member]: Struct field or a function argument *[member]: Struct field or a function argument *[members]: Struct fields or function arguments