diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 592f5f31..d62b3c2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,6 +145,26 @@ jobs: cd bon && cargo test ${{ matrix.locked }} --no-default-features \ --features=experimental-overwritable,alloc,implied-bounds + - run: | + cd bon && cargo test ${{ matrix.locked }} \ + --features=experimental-generics-setters + + - run: | + cd bon && cargo test ${{ matrix.locked }} --no-default-features \ + --features=experimental-generics-setters + + - run: | + cd bon && cargo test ${{ matrix.locked }} --no-default-features \ + --features=experimental-generics-setters,alloc + + - run: | + cd bon && cargo test ${{ matrix.locked }} --no-default-features \ + --features=experimental-generics-setters,implied-bounds + + - run: | + cd bon && cargo test ${{ matrix.locked }} --no-default-features \ + --features=experimental-generics-setters,alloc,implied-bounds + test-msrv: runs-on: ${{ matrix.os }}-latest diff --git a/benchmarks/compilation/Cargo.toml b/benchmarks/compilation/Cargo.toml index 1a864b93..3ed1111e 100644 --- a/benchmarks/compilation/Cargo.toml +++ b/benchmarks/compilation/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" version = "0.1.0" [dependencies] -bon = { path = "../../bon", optional = true, features = ["experimental-overwritable"] } +bon = { path = "../../bon", optional = true, features = ["experimental-overwritable", "experimental-generics-setters"] } cfg-if = "1.0" derive_builder = { version = "0.20", optional = true } typed-builder = { version = "0.23", optional = true } diff --git a/bon-macros/Cargo.toml b/bon-macros/Cargo.toml index 7c02e38b..2f3d13b4 100644 --- a/bon-macros/Cargo.toml +++ b/bon-macros/Cargo.toml @@ -71,6 +71,9 @@ std = [] # See the docs on this feature in the `bon`'s crate `Cargo.toml` experimental-overwritable = [] +# See the docs on this feature in the `bon`'s crate `Cargo.toml` +experimental-generics-setters = [] + # See the docs on this feature in the `bon`'s crate `Cargo.toml` implied-bounds = [] diff --git a/bon-macros/src/builder/builder_gen/generic_setters.rs b/bon-macros/src/builder/builder_gen/generic_setters.rs new file mode 100644 index 00000000..c1e21613 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/generic_setters.rs @@ -0,0 +1,454 @@ +use super::models::BuilderGenCtx; +use crate::parsing::ItemSigConfig; +use crate::util::prelude::*; +use syn::punctuated::Punctuated; +use syn::token::Where; + +pub(super) struct GenericSettersCtx<'a> { + base: &'a BuilderGenCtx, + config: &'a ItemSigConfig, +} + +impl<'a> GenericSettersCtx<'a> { + pub(super) fn new(base: &'a BuilderGenCtx, config: &'a ItemSigConfig) -> Self { + Self { base, config } + } + + pub(super) fn generic_setter_methods(&self) -> Result { + let generics = &self.base.generics.decl_without_defaults; + + let type_param_idents: Vec<&syn::Ident> = generics + .iter() + .filter_map(|param| match param { + syn::GenericParam::Type(type_param) => Some(&type_param.ident), + _ => None, + }) + .collect(); + + // Check for interdependent type parameters in generic bounds + for param in generics { + if let syn::GenericParam::Type(type_param) = param { + let params_in_bounds = + find_type_params_in_bounds(&type_param.bounds, &type_param_idents); + if params_in_bounds.len() > 1 + || (params_in_bounds.len() == 1 + && params_in_bounds.first() != Some(&&type_param.ident)) + { + let params_str = params_in_bounds + .iter() + .map(|p| format!("`{p}`")) + .collect::>() + .join(", "); + bail!( + &type_param.bounds, + "generic conversion methods cannot be generated for interdependent type parameters; \ + the bounds on generic parameter `{}` reference other type parameters: {}\n\ + \n\ + Consider removing `generics(setters(...))` or restructuring your types to avoid interdependencies", + type_param.ident, + params_str + ); + } + } + } + + // Check for interdependent type parameters in where clauses + if let Some(where_clause) = &self.base.generics.where_clause { + for predicate in &where_clause.predicates { + let params_in_predicate = + find_type_params_in_predicate(predicate, &type_param_idents); + if params_in_predicate.len() > 1 { + let params_str = params_in_predicate + .iter() + .map(|p| format!("`{p}`")) + .collect::>() + .join(", "); + bail!( + predicate, + "generic conversion methods cannot be generated for interdependent type parameters; \ + the where clause predicate references multiple type parameters: {}\n\ + \n\ + Consider removing `generics(setters(...))` or restructuring your types to avoid interdependencies", + params_str + ); + } + } + } + + let mut methods = Vec::with_capacity(generics.len()); + + for (index, param) in generics.iter().enumerate() { + match param { + syn::GenericParam::Type(type_param) => { + methods.push(self.generic_setter_method(index, type_param)); + } + syn::GenericParam::Const(const_param) => { + bail!( + &const_param.ident, + "const generic parameters are not supported in `generics(setters(...))`; \ + only type parameters can be converted" + ); + } + syn::GenericParam::Lifetime(_) => { + // Skip lifetimes, they don't get setters + } + } + } + + Ok(quote! { + #(#methods)* + }) + } + + fn generic_setter_method( + &self, + param_index: usize, + type_param: &syn::TypeParam, + ) -> TokenStream { + let builder_ident = &self.base.builder_type.ident; + let state_var = &self.base.state_var; + let where_clause = &self.base.generics.where_clause; + + let param_ident = &type_param.ident; + let method_name = self.method_name(param_ident); + + let vis = self + .config + .vis + .as_ref() + .map(|v| &v.value) + .unwrap_or(&self.base.builder_type.vis); + + let docs = self.method_docs(param_ident); + + // Build the generic arguments for the output type, where the current parameter + // is replaced with a new type variable + let new_type_var = self.base.namespace.unique_ident(param_ident.to_string()); + + // Copy the bounds from the original type parameter to the new one + let bounds = &type_param.bounds; + let new_type_param = if bounds.is_empty() { + quote!(#new_type_var) + } else { + quote!(#new_type_var: #bounds) + }; + + let output_generic_args = self + .base + .generics + .args + .iter() + .enumerate() + .map(|(i, arg)| { + if i == param_index { + quote!(#new_type_var) + } else { + quote!(#arg) + } + }) + .collect::>(); + + // Check which named members use this generic parameter + let mut runtime_asserts = Vec::new(); + let mut type_state_bounds = Vec::new(); + let named_member_conversions = self + .base + .named_members() + .enumerate() + .map(|(idx, member)| { + let uses_param = member_uses_generic_param(member, param_ident); + let index = syn::Index::from(idx); + if uses_param { + // Add compile-time type state constraint + let state_mod = &self.base.state_mod.ident; + let field_pascal = &member.name.pascal; + type_state_bounds.push(quote! { + #state_var::#field_pascal: #state_mod::IsUnset + }); + + // Add runtime assert that this field is None + let field_ident = &member.name.orig; + let message = format!("BUG: field `{field_ident}` should be None when converting generic parameter `{param_ident}`"); + runtime_asserts.push(quote! { + ::core::assert!(named.#index.is_none(), #message); + }); + // Field uses the generic parameter, so create a new None + quote!(::core::option::Option::None) + } else { + // Field doesn't use the generic parameter, so move it from the tuple + quote!(named.#index) + } + }) + .collect::>(); + + let receiver_field = self.base.receiver().map(|receiver| { + let ident = &receiver.field_ident; + quote!(#ident: self.#ident,) + }); + + let start_fn_fields = self.base.start_fn_args().map(|member| { + let ident = &member.ident; + quote!(#ident: self.#ident,) + }); + + let custom_fields = self.base.custom_fields().map(|field| { + let ident = &field.ident; + quote!(#ident: self.#ident,) + }); + + // Extend where clause with type state bounds and update type parameter references + let extended_where_clause = { + let mut clause = where_clause.clone().unwrap_or_else(|| syn::WhereClause { + where_token: Where::default(), + predicates: Punctuated::default(), + }); + + for predicate in &mut clause.predicates { + replace_type_param_in_predicate(predicate, param_ident, &new_type_var); + } + + for bound in type_state_bounds { + clause.predicates.push(syn::parse_quote!(#bound)); + } + + if clause.predicates.is_empty() { + None + } else { + Some(clause) + } + }; + + quote! { + #(#docs)* + #[inline(always)] + #vis fn #method_name<#new_type_param>( + self + ) -> #builder_ident<#(#output_generic_args,)* #state_var> + #extended_where_clause + { + let named = self.__unsafe_private_named; + + // Runtime safety asserts to ensure fields using the converted + // generic parameter are None + #(#runtime_asserts)* + + #builder_ident { + __unsafe_private_phantom: ::core::marker::PhantomData, + #receiver_field + #(#start_fn_fields)* + #(#custom_fields)* + __unsafe_private_named: ( + #(#named_member_conversions,)* + ), + } + } + } + } + + fn method_name(&self, param_ident: &syn::Ident) -> syn::Ident { + let param_name_snake = param_ident.pascal_to_snake_case(); + + // Name is guaranteed to be present due to validation in parse_setters_config + let name_pattern = &self + .config + .name + .as_ref() + .expect("name should be validated") + .value; + + let method_name = name_pattern.replace("{}", ¶m_name_snake.to_string()); + + syn::Ident::new(&method_name, param_ident.span()) + } + + fn method_docs(&self, param_ident: &syn::Ident) -> Vec { + // If custom docs are provided, use them + if let Some(ref docs) = self.config.docs { + return docs.value.clone(); + } + + // Otherwise, generate default documentation + let doc = format!( + "Convert the `{param_ident}` generic parameter to a different type.\n\ + \n\ + This method allows changing the type of the `{param_ident}` parameter on the builder, \ + which is useful when you need to build up values with different types at \ + different stages of construction." + ); + + vec![syn::parse_quote!(#[doc = #doc])] + } +} + +fn find_type_params_in_bounds<'b>( + bounds: &Punctuated, + type_params: &'b [&'b syn::Ident], +) -> Vec<&'b syn::Ident> { + use syn::visit::Visit; + + struct TypeParamFinder<'a> { + type_params: &'a [&'a syn::Ident], + found: std::collections::HashSet<&'a syn::Ident>, + } + + impl<'ast> Visit<'ast> for TypeParamFinder<'_> { + fn visit_path(&mut self, path: &'ast syn::Path) { + // Check if this path is one of our type parameters + for ¶m in self.type_params { + if path.is_ident(param) { + self.found.insert(param); + } + } + // Continue visiting nested paths + syn::visit::visit_path(self, path); + } + } + + let mut finder = TypeParamFinder { + type_params, + found: std::collections::HashSet::new(), + }; + + for bound in bounds { + finder.visit_type_param_bound(bound); + } + + // Preserve the original order of type parameters for deterministic output + type_params + .iter() + .filter(|param| finder.found.contains(*param)) + .copied() + .collect() +} + +fn find_type_params_in_predicate<'b>( + predicate: &syn::WherePredicate, + type_params: &'b [&'b syn::Ident], +) -> Vec<&'b syn::Ident> { + use syn::visit::Visit; + + struct TypeParamFinder<'a> { + type_params: &'a [&'a syn::Ident], + found: std::collections::HashSet<&'a syn::Ident>, + } + + impl<'ast> Visit<'ast> for TypeParamFinder<'_> { + fn visit_path(&mut self, path: &'ast syn::Path) { + // Check if this path is one of our type parameters + for ¶m in self.type_params { + if path.is_ident(param) { + self.found.insert(param); + } + } + // Continue visiting nested paths + syn::visit::visit_path(self, path); + } + } + + let mut finder = TypeParamFinder { + type_params, + found: std::collections::HashSet::new(), + }; + finder.visit_where_predicate(predicate); + // Preserve the original order of type parameters for deterministic output + type_params + .iter() + .filter(|param| finder.found.contains(*param)) + .copied() + .collect() +} + +fn replace_type_param_in_predicate( + predicate: &mut syn::WherePredicate, + old_param: &syn::Ident, + new_param: &syn::Ident, +) { + use syn::visit_mut::VisitMut; + + struct TypeParamReplacer<'a> { + old_param: &'a syn::Ident, + new_param: &'a syn::Ident, + } + + impl VisitMut for TypeParamReplacer<'_> { + fn visit_path_mut(&mut self, path: &mut syn::Path) { + // Replace simple paths like `T` + if path.is_ident(self.old_param) { + if let Some(segment) = path.segments.first_mut() { + segment.ident = self.new_param.clone(); + } + } + // Continue visiting nested paths + syn::visit_mut::visit_path_mut(self, path); + } + + fn visit_type_path_mut(&mut self, type_path: &mut syn::TypePath) { + // Handle qualified paths like T::Assoc + if let Some(qself) = &mut type_path.qself { + self.visit_type_mut(&mut qself.ty); + } + self.visit_path_mut(&mut type_path.path); + } + } + + let mut replacer = TypeParamReplacer { + old_param, + new_param, + }; + replacer.visit_where_predicate_mut(predicate); +} + +/// Check if a member's type uses a specific generic parameter +fn member_uses_generic_param(member: &super::NamedMember, param_ident: &syn::Ident) -> bool { + let member_ty = member.underlying_norm_ty(); + type_uses_generic_param(member_ty, param_ident) +} + +/// Recursively check if a type uses a specific generic parameter +fn type_uses_generic_param(ty: &syn::Type, param_ident: &syn::Ident) -> bool { + use syn::visit::Visit; + + struct GenericParamVisitor<'a> { + param_ident: &'a syn::Ident, + found: bool, + } + + impl<'ast> Visit<'ast> for GenericParamVisitor<'_> { + fn visit_type_path(&mut self, type_path: &'ast syn::TypePath) { + // Early return if already found to avoid unnecessary recursion + if self.found { + return; + } + + // Check if the path is the generic parameter we're looking for + if type_path.path.is_ident(self.param_ident) { + self.found = true; + return; + } + + // For qualified paths like T::Assoc or ::Assoc, + // check if the first segment (or qself) uses the generic parameter + + if let Some(qself) = &type_path.qself { + // For ::Assoc syntax + self.visit_type(&qself.ty); + } else if let Some(segment) = type_path.path.segments.first() { + // For T::Assoc syntax + if segment.ident == *self.param_ident { + self.found = true; + return; + } + } + + // Continue visiting the rest of the type path + syn::visit::visit_type_path(self, type_path); + } + } + + let mut visitor = GenericParamVisitor { + param_ident, + found: false, + }; + visitor.visit_type(ty); + visitor.found +} diff --git a/bon-macros/src/builder/builder_gen/input_fn/mod.rs b/bon-macros/src/builder/builder_gen/input_fn/mod.rs index 3f712985..bac8e598 100644 --- a/bon-macros/src/builder/builder_gen/input_fn/mod.rs +++ b/bon-macros/src/builder/builder_gen/input_fn/mod.rs @@ -401,6 +401,8 @@ impl<'a> FnInputCtx<'a> { vis: self.config.builder_type.vis.map(SpannedKey::into_value), }; + let generics_config = self.config.generics.map(SpannedKey::into_value); + BuilderGenCtx::new(BuilderGenCtxParams { bon: self.config.bon, namespace: Cow::Borrowed(self.namespace), @@ -413,6 +415,7 @@ impl<'a> FnInputCtx<'a> { assoc_method_ctx, generics, + generics_config, orig_item_vis: self.fn_item.norm.vis, builder_type, diff --git a/bon-macros/src/builder/builder_gen/input_struct.rs b/bon-macros/src/builder/builder_gen/input_struct.rs index 3e86e504..69b2d1f8 100644 --- a/bon-macros/src/builder/builder_gen/input_struct.rs +++ b/bon-macros/src/builder/builder_gen/input_struct.rs @@ -223,6 +223,8 @@ impl StructInputCtx { let mut namespace = GenericsNamespace::default(); namespace.visit_item_struct(&self.struct_item.orig); + let generics_config = self.config.generics.map(SpannedKey::into_value); + BuilderGenCtx::new(BuilderGenCtxParams { bon: self.config.bon, namespace: Cow::Owned(namespace), @@ -235,6 +237,7 @@ impl StructInputCtx { assoc_method_ctx, generics, + generics_config, orig_item_vis: self.struct_item.norm.vis, builder_type, diff --git a/bon-macros/src/builder/builder_gen/member/config/setters.rs b/bon-macros/src/builder/builder_gen/member/config/setters.rs index 7891a0d1..d03efaca 100644 --- a/bon-macros/src/builder/builder_gen/member/config/setters.rs +++ b/bon-macros/src/builder/builder_gen/member/config/setters.rs @@ -7,12 +7,7 @@ use syn::punctuated::Punctuated; const DOCS_CONTEXT: &str = "builder struct's impl block"; fn parse_setter_fn(meta: &syn::Meta) -> Result> { - let params = ItemSigConfigParsing { - meta, - reject_self_mentions: Some(DOCS_CONTEXT), - } - .parse()?; - + let params = ItemSigConfigParsing::new(meta, Some(DOCS_CONTEXT)).parse()?; SpannedKey::new(meta.path(), params) } diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index 082cd175..016c41f7 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -1,6 +1,7 @@ mod builder_decl; mod builder_derives; mod finish_fn; +mod generic_setters; mod getters; mod member; mod models; @@ -14,6 +15,7 @@ pub(crate) mod input_struct; pub(crate) use top_level_config::TopLevelConfig; use crate::util::prelude::*; +use generic_setters::GenericSettersCtx; use getters::GettersCtx; use member::{CustomField, Member, MemberOrigin, NamedMember, PosFnMember, RawMember}; use models::{AssocMethodCtxParams, AssocMethodReceiverCtx, BuilderGenCtx, FinishFnBody, Generics}; @@ -125,6 +127,14 @@ impl BuilderGenCtx { .into_iter() .flatten(); + let generic_setter_methods = self + .generics_config + .as_ref() + .and_then(|config| config.setters.as_ref()) + .map(|config| GenericSettersCtx::new(self, config).generic_setter_methods()) + .transpose()? + .unwrap_or_default(); + let generics_decl = &self.generics.decl_without_defaults; let generic_args = &self.generics.args; let where_clause = &self.generics.where_clause; @@ -149,6 +159,7 @@ impl BuilderGenCtx { { #finish_fn #(#accessor_methods)* + #generic_setter_methods } }) } diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs index 0741043c..e7e21c75 100644 --- a/bon-macros/src/builder/builder_gen/models.rs +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -1,5 +1,5 @@ use super::member::Member; -use super::top_level_config::{DerivesConfig, OnConfig}; +use super::top_level_config::{DerivesConfig, GenericsConfig, OnConfig}; use crate::normalization::GenericsNamespace; use crate::parsing::{BonCratePath, ItemSigConfig, SpannedKey}; use crate::util::prelude::*; @@ -162,6 +162,9 @@ pub(crate) struct BuilderGenCtx { /// Name of the generic variable that holds the builder's state. pub(super) state_var: syn::Ident, + /// Namespace for generating unique identifiers. + pub(super) namespace: GenericsNamespace, + pub(super) members: Vec, /// Lint suppressions from the original item that will be inherited by all items @@ -172,6 +175,7 @@ pub(crate) struct BuilderGenCtx { pub(super) on: Vec, pub(super) generics: Generics, + pub(super) generics_config: Option, pub(super) assoc_method_ctx: Option, @@ -200,6 +204,7 @@ pub(super) struct BuilderGenCtxParams<'a> { /// Generics to apply to the builder type. pub(super) generics: Generics, + pub(super) generics_config: Option, pub(super) assoc_method_ctx: Option, @@ -219,6 +224,7 @@ impl BuilderGenCtx { const_, on, generics, + generics_config, orig_item_vis, assoc_method_ctx, builder_type, @@ -367,11 +373,13 @@ impl BuilderGenCtx { Ok(Self { bon, state_var, + namespace: namespace.into_owned(), members, allow_attrs, const_, on, generics, + generics_config, assoc_method_ctx, builder_type, state_mod, diff --git a/bon-macros/src/builder/builder_gen/top_level_config/generics.rs b/bon-macros/src/builder/builder_gen/top_level_config/generics.rs new file mode 100644 index 00000000..e9479ff8 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/top_level_config/generics.rs @@ -0,0 +1,60 @@ +use crate::parsing::{ItemSigConfig, ItemSigConfigParsing}; +use crate::util::prelude::*; +use darling::FromMeta; + +#[derive(Debug, Clone, Default)] +pub(crate) struct GenericsConfig { + pub(crate) setters: Option>, +} + +impl FromMeta for GenericsConfig { + fn from_meta(meta: &syn::Meta) -> Result { + meta.require_list()?.require_parens_delim()?; + + #[derive(FromMeta)] + struct Parsed { + #[darling(default, with = parse_setters_config, map = Some)] + setters: Option>, + } + + let parsed = Parsed::from_meta(meta)?; + + Ok(Self { + setters: parsed.setters, + }) + } +} + +fn parse_setters_config(meta: &syn::Meta) -> Result> { + if !cfg!(feature = "experimental-generics-setters") { + bail!( + meta, + "🔬 `generics(setters(...))` attribute is experimental and requires \ + \"experimental-generics-setters\" cargo feature to be enabled", + ); + } + + const DOCS_CONTEXT: &str = "builder struct's impl block"; + let config: ItemSigConfig = + ItemSigConfigParsing::new(meta, Some(DOCS_CONTEXT)).parse()?; + + // Validate that name is provided and contains the placeholder + let name_pattern = config.name.as_ref().ok_or_else(|| { + err!( + meta, + "`name` parameter is required for `generics(setters(...))`; \ + specify a pattern like `name = \"conv_{{}}\"` where `{{}}` will be \ + replaced with the snake_case name of each generic parameter" + ) + })?; + + if !name_pattern.value.contains("{}") { + bail!( + &name_pattern.key, + "the `name` pattern must contain the `{{}}` placeholder, \ + which will be replaced with the snake_case name of each generic parameter" + ); + } + + Ok(config) +} diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index 2b3d51c9..a31b214c 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -1,5 +1,7 @@ +mod generics; mod on; +pub(crate) use generics::GenericsConfig; pub(crate) use on::OnConfig; use crate::parsing::{BonCratePath, ItemSigConfig, ItemSigConfigParsing, SpannedKey}; @@ -11,35 +13,19 @@ use syn::punctuated::Punctuated; use syn::ItemFn; fn parse_finish_fn(meta: &syn::Meta) -> Result { - ItemSigConfigParsing { - meta, - reject_self_mentions: Some("builder struct's impl block"), - } - .parse() + ItemSigConfigParsing::new(meta, Some("builder struct's impl block")).parse() } fn parse_builder_type(meta: &syn::Meta) -> Result { - ItemSigConfigParsing { - meta, - reject_self_mentions: Some("builder struct"), - } - .parse() + ItemSigConfigParsing::new(meta, Some("builder struct")).parse() } fn parse_state_mod(meta: &syn::Meta) -> Result { - ItemSigConfigParsing { - meta, - reject_self_mentions: Some("builder's state module"), - } - .parse() + ItemSigConfigParsing::new(meta, Some("builder's state module")).parse() } fn parse_start_fn(meta: &syn::Meta) -> Result { - ItemSigConfigParsing { - meta, - reject_self_mentions: None, - } - .parse() + ItemSigConfigParsing::new(meta, None).parse() } #[derive(Debug, FromMeta)] @@ -75,6 +61,10 @@ pub(crate) struct TopLevelConfig { /// Specifies the derives to apply to the builder. #[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)] pub(crate) derive: DerivesConfig, + + /// Specifies configuration for generic parameter conversion methods. + #[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)] + pub(crate) generics: Option>, } impl TopLevelConfig { diff --git a/bon-macros/src/parsing/item_sig.rs b/bon-macros/src/parsing/item_sig.rs index 7288ebde..6a0f47f1 100644 --- a/bon-macros/src/parsing/item_sig.rs +++ b/bon-macros/src/parsing/item_sig.rs @@ -5,18 +5,34 @@ 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 ItemSigConfig { - pub(crate) name: Option>, +/// +/// The generic parameter `N` specifies the type used for the name field: +/// - `syn::Ident` (default): For regular identifiers +/// - `String`: For pattern strings (e.g., "conv_{}") +#[derive(Debug, Clone)] +pub(crate) struct ItemSigConfig { + pub(crate) name: Option>, pub(crate) vis: Option>, pub(crate) docs: Option>>, } -impl ItemSigConfig { +impl Default for ItemSigConfig { + fn default() -> Self { + Self { + name: None, + vis: None, + docs: None, + } + } +} + +impl ItemSigConfig { pub(crate) fn name(&self) -> Option<&syn::Ident> { self.name.as_ref().map(|name| &name.value) } +} +impl ItemSigConfig { pub(crate) fn vis(&self) -> Option<&syn::Visibility> { self.vis.as_ref().map(|vis| &vis.value) } @@ -31,31 +47,39 @@ pub(crate) struct ItemSigConfigParsing<'a> { pub(crate) reject_self_mentions: Option<&'static str>, } -impl ItemSigConfigParsing<'_> { - pub(crate) fn parse(self) -> Result { - let meta = self.meta; +impl<'a> ItemSigConfigParsing<'a> { + pub(crate) fn new(meta: &'a syn::Meta, reject_self_mentions: Option<&'static str>) -> Self { + ItemSigConfigParsing { + meta, + reject_self_mentions, + } + } - if let syn::Meta::NameValue(meta) = meta { - let val = &meta.value; - let name = syn::parse2(val.to_token_stream())?; + pub(crate) fn parse(self) -> Result> + where + N: FromMeta, + { + let meta = self.meta; + if let syn::Meta::NameValue(_) = meta { + let name = SpannedKey::from_meta(meta)?; return Ok(ItemSigConfig { - name: Some(SpannedKey::new(&meta.path, name)?), + name: Some(name), vis: None, docs: None, }); } #[derive(Debug, FromMeta)] - struct Full { - name: Option>, + struct Full { + name: Option>, vis: Option>, #[darling(default, with = super::parse_docs, map = Some)] doc: Option>>, } - let full: Full = crate::parsing::parse_non_empty_paren_meta_list(meta)?; + let full: Full = crate::parsing::parse_non_empty_paren_meta_list(meta)?; if let Some(context) = self.reject_self_mentions { if let Some(docs) = &full.doc { diff --git a/bon-sandbox/Cargo.toml b/bon-sandbox/Cargo.toml index 27ceed8d..3edb437c 100644 --- a/bon-sandbox/Cargo.toml +++ b/bon-sandbox/Cargo.toml @@ -34,6 +34,6 @@ derive_builder = "0.20" typed-builder = "0.23" [dependencies.bon] -features = ["experimental-overwritable"] +features = ["experimental-overwritable", "experimental-generics-setters"] path = "../bon" version = "=3.8.2" diff --git a/bon/Cargo.toml b/bon/Cargo.toml index 714d6f04..9c6e2d2e 100644 --- a/bon/Cargo.toml +++ b/bon/Cargo.toml @@ -85,6 +85,13 @@ implied-bounds = ["bon-macros/implied-bounds"] # describing your use case for it. experimental-overwritable = ["bon-macros/experimental-overwritable"] +# 🔬 Experimental! There may be breaking changes to this feature between *minor* releases, +# however, compatibility within patch releases is guaranteed though. +# +# This feature enables the #[builder(generics(setters(...)))] attribute that can be used to +# generate methods for converting generic parameters on builders. +experimental-generics-setters = ["bon-macros/experimental-generics-setters"] + # Legacy experimental attribute. It's left here for backwards compatibility, # and it will be removed in the next major release. # diff --git a/bon/tests/integration/builder/generics_setters.rs b/bon/tests/integration/builder/generics_setters.rs new file mode 100644 index 00000000..3476d5de --- /dev/null +++ b/bon/tests/integration/builder/generics_setters.rs @@ -0,0 +1,170 @@ +use bon::Builder; + +#[test] +fn test_simple_syntax() { + #[derive(Builder)] + #[builder(generics(setters = "conv_{}"))] + struct Sut { + x1: u32, + x2: A, + x3: B, + } + + use sut_builder::{IsUnset, SetX2, SetX3, State}; + + impl SutBuilder { + fn x2_and_x3(self, x2: A2, x3: B2) -> SutBuilder>> + where + S::X2: IsUnset, + S::X3: IsUnset, + { + self.conv_a().x2(x2).conv_b().x3(x3) + } + } + + // Start with () types, then convert to the actual types + let result = Sut::<(), ()>::builder() + .x1(42) + .x2_and_x3("hello", [1, 2, 3]) + .build(); + + assert_eq!(result.x1, 42); + assert_eq!(result.x2, "hello"); + assert_eq!(result.x3, [1, 2, 3]); +} + +#[test] +fn test_complex_syntax_with_name() { + #[derive(Builder)] + #[builder(generics(setters(name = "with_{}")))] + struct Sut { + value: T, + } + + impl SutBuilder { + fn convert_and_set(self, value: T2) -> SutBuilder> + where + S::Value: sut_builder::IsUnset, + { + self.with_t().value(value) + } + } + + let result = Sut::<()>::builder().convert_and_set(42).build(); + assert_eq!(result.value, 42); +} + +#[test] +fn test_complex_syntax_with_vis() { + #[derive(Builder)] + #[builder(generics(setters(name = "conv_{}", vis = "pub(self)")))] + struct Sut { + value: T, + } + + impl SutBuilder { + fn convert_and_set(self, value: T2) -> SutBuilder> + where + S::Value: sut_builder::IsUnset, + { + self.conv_t().value(value) + } + } + + let result = Sut::<()>::builder().convert_and_set(100).build(); + assert_eq!(result.value, 100); +} + +#[test] +fn test_complex_syntax_with_docs() { + #[derive(Builder)] + #[builder(generics(setters(name = "conv_{}", doc { + /// Custom documentation for generic conversion. + })))] + struct Sut { + value: T, + } + + let result = Sut::<()>::builder().conv_t::().value(42).build(); + assert_eq!(result.value, 42); +} + +#[test] +fn test_with_trait_bounds() { + trait MyTrait { + type Assoc; + } + + impl MyTrait for () { + type Assoc = u32; + } + + impl MyTrait for bool { + type Assoc = u64; + } + + #[derive(Builder)] + #[builder(generics(setters(name = "conv_{}")))] + struct AssocTypeField { + value: T::Assoc, + } + + #[derive(Builder)] + #[builder(generics(setters(name = "conv_{}")))] + struct QualifiedPathField { + value: ::Assoc, + } + + let result1 = AssocTypeField::<()>::builder() + .conv_t::() + .value(42u64) + .build(); + assert_eq!(result1.value, 42u64); + + let result2 = QualifiedPathField::<()>::builder() + .conv_t::() + .value(99u64) + .build(); + assert_eq!(result2.value, 99u64); +} + +#[test] +fn test_with_trait_bounds_false_friend() { + // The associated type is also called T, but should not be replaced by the generic + + trait MyTrait { + type T; + } + + impl MyTrait for () { + type T = u32; + } + + impl MyTrait for bool { + type T = u64; + } + + #[derive(Builder)] + #[builder(generics(setters(name = "conv_{}")))] + struct AssocTypeField { + value: T::T, + } + + #[derive(Builder)] + #[builder(generics(setters(name = "conv_{}")))] + struct QualifiedPathField { + value: ::T, + } + + let result1 = AssocTypeField::<()>::builder() + .conv_t::() + .value(42u64) + .build(); + assert_eq!(result1.value, 42u64); + + let result2 = QualifiedPathField::<()>::builder() + .conv_t::() + .value(99u64) + .build(); + assert_eq!(result2.value, 99u64); +} diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index 9f6a86d1..7a749797 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -18,6 +18,8 @@ mod attr_top_level_start_fn; mod attr_with; mod cfgs; mod generics; +#[cfg(feature = "experimental-generics-setters")] +mod generics_setters; mod init_order; mod lints; mod many_params; diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/array.rs b/bon/tests/integration/ui/compile_fail/generics_setters/array.rs new file mode 100644 index 00000000..554f163f --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/array.rs @@ -0,0 +1,18 @@ +use bon::Builder; + +// Test that conversion methods properly detect generic parameter usage in arrays +// and don't allow converting the type after the field has been set. + +#[derive(Builder)] +#[builder(generics(setters(name = "conv_{}")))] +struct ArrayField { + value: [T; 2], +} + +fn main() { + // Test [T; N] - can't change type after setting field + ArrayField::<()>::builder() + .value([(), ()]) + .conv_t::() + .build(); +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/array.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/array.stderr new file mode 100644 index 00000000..0223eb6b --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/array.stderr @@ -0,0 +1,16 @@ +error[E0277]: the member `Set` was already set, but this method requires it to be unset + --> tests/integration/ui/compile_fail/generics_setters/array.rs:16:10 + | +16 | .conv_t::() + | ^^^^^^ the member `Set` was already set, but this method requires it to be unset + | + = help: the trait `IsUnset` is not implemented for `Set` +note: required by a bound in `ArrayFieldBuilder::::conv_t` + --> tests/integration/ui/compile_fail/generics_setters/array.rs:6:10 + | + 6 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `ArrayFieldBuilder::::conv_t` + 7 | #[builder(generics(setters(name = "conv_{}")))] + 8 | struct ArrayField { + | - required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/assoc_type.rs b/bon/tests/integration/ui/compile_fail/generics_setters/assoc_type.rs new file mode 100644 index 00000000..34226506 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/assoc_type.rs @@ -0,0 +1,30 @@ +use bon::Builder; + +// Test that conversion methods properly detect generic parameter usage in associated types +// and don't allow converting the type after the field has been set. + +trait MyTrait { + type Assoc; +} + +impl MyTrait for () { + type Assoc = u32; +} + +impl MyTrait for bool { + type Assoc = u64; +} + +#[derive(Builder)] +#[builder(generics(setters(name = "conv_{}")))] +struct AssocTypeField { + value: T::Assoc, +} + +fn main() { + // Test T::Assoc - can't change type after setting field + AssocTypeField::<()>::builder() + .value(42) + .conv_t::() + .build(); +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/assoc_type.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/assoc_type.stderr new file mode 100644 index 00000000..fdfbbe3b --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/assoc_type.stderr @@ -0,0 +1,16 @@ +error[E0277]: the member `Set` was already set, but this method requires it to be unset + --> tests/integration/ui/compile_fail/generics_setters/assoc_type.rs:28:10 + | +28 | .conv_t::() + | ^^^^^^ the member `Set` was already set, but this method requires it to be unset + | + = help: the trait `IsUnset` is not implemented for `Set` +note: required by a bound in `AssocTypeFieldBuilder::::conv_t` + --> tests/integration/ui/compile_fail/generics_setters/assoc_type.rs:18:10 + | +18 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `AssocTypeFieldBuilder::::conv_t` +19 | #[builder(generics(setters(name = "conv_{}")))] +20 | struct AssocTypeField { + | - required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/fn_pointer.rs b/bon/tests/integration/ui/compile_fail/generics_setters/fn_pointer.rs new file mode 100644 index 00000000..125d293d --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/fn_pointer.rs @@ -0,0 +1,18 @@ +use bon::Builder; + +// Test that conversion methods properly detect generic parameter usage in function pointers +// and don't allow converting the type after the field has been set. + +#[derive(Builder)] +#[builder(generics(setters(name = "conv_{}")))] +struct FnPointerField { + value: fn(T) -> T, +} + +fn main() { + // Test fn(T) -> T - can't change type after setting field + FnPointerField::<()>::builder() + .value(|x| x) + .conv_t::() + .build(); +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/fn_pointer.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/fn_pointer.stderr new file mode 100644 index 00000000..171d9b44 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/fn_pointer.stderr @@ -0,0 +1,16 @@ +error[E0277]: the member `Set` was already set, but this method requires it to be unset + --> tests/integration/ui/compile_fail/generics_setters/fn_pointer.rs:16:10 + | +16 | .conv_t::() + | ^^^^^^ the member `Set` was already set, but this method requires it to be unset + | + = help: the trait `IsUnset` is not implemented for `Set` +note: required by a bound in `FnPointerFieldBuilder::::conv_t` + --> tests/integration/ui/compile_fail/generics_setters/fn_pointer.rs:6:10 + | + 6 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `FnPointerFieldBuilder::::conv_t` + 7 | #[builder(generics(setters(name = "conv_{}")))] + 8 | struct FnPointerField { + | - required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_bounds.rs b/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_bounds.rs new file mode 100644 index 00000000..ea37c115 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_bounds.rs @@ -0,0 +1,18 @@ +use bon::Builder; + +// Test that conversion methods properly handle complex interdependent bounds +// and don't allow converting the type after the field has been set. + +#[derive(Builder)] +#[builder(generics(setters(name = "conv_{}")))] +struct Sut +where + T: IntoIterator, + T::Item: Clone, + U: Clone, +{ + value: T, +} + +fn main() { +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_bounds.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_bounds.stderr new file mode 100644 index 00000000..06998645 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_bounds.stderr @@ -0,0 +1,7 @@ +error: generic conversion methods cannot be generated for interdependent type parameters; the where clause predicate references multiple type parameters: `T`, `U` + + Consider removing `generics(setters(...))` or restructuring your types to avoid interdependencies + --> tests/integration/ui/compile_fail/generics_setters/interdependent_bounds.rs:10:5 + | +10 | T: IntoIterator, + | ^ diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_param_bounds.rs b/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_param_bounds.rs new file mode 100644 index 00000000..21bc9f5d --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_param_bounds.rs @@ -0,0 +1,12 @@ +use bon::Builder; + +// Test that conversion methods reject bounds that reference other type parameters + +#[derive(Builder)] +#[builder(generics(setters(name = "with_{}")))] +struct Sut, Item> { + value1: Iter, +} + +fn main() { +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_param_bounds.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_param_bounds.stderr new file mode 100644 index 00000000..b514575d --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/interdependent_param_bounds.stderr @@ -0,0 +1,7 @@ +error: generic conversion methods cannot be generated for interdependent type parameters; the bounds on generic parameter `Iter` reference other type parameters: `Item` + + Consider removing `generics(setters(...))` or restructuring your types to avoid interdependencies + --> tests/integration/ui/compile_fail/generics_setters/interdependent_param_bounds.rs:7:18 + | +7 | struct Sut, Item> { + | ^^^^^^^^ diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/option.rs b/bon/tests/integration/ui/compile_fail/generics_setters/option.rs new file mode 100644 index 00000000..7656bbd7 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/option.rs @@ -0,0 +1,18 @@ +use bon::Builder; + +// Test that conversion methods properly detect generic parameter usage in Option +// and don't allow converting the type after the field has been set. + +#[derive(Builder)] +#[builder(generics(setters(name = "conv_{}")))] +struct OptionField { + value: core::option::Option, +} + +fn main() { + // Test Option - can't change type after setting field + OptionField::<()>::builder() + .value(()) + .conv_t::() + .build(); +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/option.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/option.stderr new file mode 100644 index 00000000..3c4b96e0 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/option.stderr @@ -0,0 +1,16 @@ +error[E0277]: the member `Set` was already set, but this method requires it to be unset + --> tests/integration/ui/compile_fail/generics_setters/option.rs:16:10 + | +16 | .conv_t::() + | ^^^^^^ the member `Set` was already set, but this method requires it to be unset + | + = help: the trait `IsUnset` is not implemented for `Set` +note: required by a bound in `OptionFieldBuilder::::conv_t` + --> tests/integration/ui/compile_fail/generics_setters/option.rs:6:10 + | + 6 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `OptionFieldBuilder::::conv_t` + 7 | #[builder(generics(setters(name = "conv_{}")))] + 8 | struct OptionField { + | - required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/qualified_path.rs b/bon/tests/integration/ui/compile_fail/generics_setters/qualified_path.rs new file mode 100644 index 00000000..d909a94f --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/qualified_path.rs @@ -0,0 +1,30 @@ +use bon::Builder; + +// Test that conversion methods properly detect generic parameter usage in qualified paths +// and don't allow converting the type after the field has been set. + +trait MyTrait { + type Assoc; +} + +impl MyTrait for () { + type Assoc = u32; +} + +impl MyTrait for bool { + type Assoc = u64; +} + +#[derive(Builder)] +#[builder(generics(setters(name = "conv_{}")))] +struct QualifiedPathField { + value: ::Assoc, +} + +fn main() { + // Test ::Assoc - can't change type after setting field + QualifiedPathField::<()>::builder() + .value(42) + .conv_t::() + .build(); +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/qualified_path.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/qualified_path.stderr new file mode 100644 index 00000000..0a309c29 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/qualified_path.stderr @@ -0,0 +1,16 @@ +error[E0277]: the member `Set` was already set, but this method requires it to be unset + --> tests/integration/ui/compile_fail/generics_setters/qualified_path.rs:28:10 + | +28 | .conv_t::() + | ^^^^^^ the member `Set` was already set, but this method requires it to be unset + | + = help: the trait `IsUnset` is not implemented for `Set` +note: required by a bound in `QualifiedPathFieldBuilder::::conv_t` + --> tests/integration/ui/compile_fail/generics_setters/qualified_path.rs:18:10 + | +18 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `QualifiedPathFieldBuilder::::conv_t` +19 | #[builder(generics(setters(name = "conv_{}")))] +20 | struct QualifiedPathField { + | - required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/raw_pointer.rs b/bon/tests/integration/ui/compile_fail/generics_setters/raw_pointer.rs new file mode 100644 index 00000000..eb8ecf12 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/raw_pointer.rs @@ -0,0 +1,18 @@ +use bon::Builder; + +// Test that conversion methods properly detect generic parameter usage in raw pointers +// and don't allow converting the type after the field has been set. + +#[derive(Builder)] +#[builder(generics(setters(name = "conv_{}")))] +struct RawPointerField { + value: *const T, +} + +fn main() { + // Test *const T - can't change type after setting field + RawPointerField::<()>::builder() + .value(core::ptr::null()) + .conv_t::() + .build(); +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/raw_pointer.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/raw_pointer.stderr new file mode 100644 index 00000000..5af60596 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/raw_pointer.stderr @@ -0,0 +1,16 @@ +error[E0277]: the member `Set` was already set, but this method requires it to be unset + --> tests/integration/ui/compile_fail/generics_setters/raw_pointer.rs:16:10 + | +16 | .conv_t::() + | ^^^^^^ the member `Set` was already set, but this method requires it to be unset + | + = help: the trait `IsUnset` is not implemented for `Set` +note: required by a bound in `RawPointerFieldBuilder::::conv_t` + --> tests/integration/ui/compile_fail/generics_setters/raw_pointer.rs:6:10 + | + 6 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `RawPointerFieldBuilder::::conv_t` + 7 | #[builder(generics(setters(name = "conv_{}")))] + 8 | struct RawPointerField { + | - required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/tuple.rs b/bon/tests/integration/ui/compile_fail/generics_setters/tuple.rs new file mode 100644 index 00000000..47f31e6d --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/tuple.rs @@ -0,0 +1,18 @@ +use bon::Builder; + +// Test that conversion methods properly detect generic parameter usage in tuples +// and don't allow converting the type after the field has been set. + +#[derive(Builder)] +#[builder(generics(setters(name = "conv_{}")))] +struct TupleField { + value: (T, T), +} + +fn main() { + // Test (T, T) - can't change type after setting field + TupleField::<()>::builder() + .value(((), ())) + .conv_t::() + .build(); +} diff --git a/bon/tests/integration/ui/compile_fail/generics_setters/tuple.stderr b/bon/tests/integration/ui/compile_fail/generics_setters/tuple.stderr new file mode 100644 index 00000000..0f699a8f --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/generics_setters/tuple.stderr @@ -0,0 +1,16 @@ +error[E0277]: the member `Set` was already set, but this method requires it to be unset + --> tests/integration/ui/compile_fail/generics_setters/tuple.rs:16:10 + | +16 | .conv_t::() + | ^^^^^^ the member `Set` was already set, but this method requires it to be unset + | + = help: the trait `IsUnset` is not implemented for `Set` +note: required by a bound in `TupleFieldBuilder::::conv_t` + --> tests/integration/ui/compile_fail/generics_setters/tuple.rs:6:10 + | + 6 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `TupleFieldBuilder::::conv_t` + 7 | #[builder(generics(setters(name = "conv_{}")))] + 8 | struct TupleField { + | - required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/mod.rs b/bon/tests/integration/ui/mod.rs index 2ae7515a..5b5a4102 100644 --- a/bon/tests/integration/ui/mod.rs +++ b/bon/tests/integration/ui/mod.rs @@ -12,4 +12,8 @@ fn ui() { if cfg!(feature = "experimental-overwritable") { t.compile_fail("tests/integration/ui/compile_fail/overwritable/*.rs"); } + + if cfg!(feature = "experimental-generics-setters") { + t.compile_fail("tests/integration/ui/compile_fail/generics_setters/*.rs"); + } } diff --git a/scripts/test-msrv.sh b/scripts/test-msrv.sh index 0a5bf9e4..d9808a7c 100755 --- a/scripts/test-msrv.sh +++ b/scripts/test-msrv.sh @@ -53,7 +53,7 @@ step cargo update --precise 0.3.2 -p glob export RUSTFLAGS="${RUSTFLAGS:-} --allow unknown-lints" -features=experimental-overwritable +features=experimental-overwritable,experimental-generics-setters step cargo clippy --all-targets --locked --features "$features" diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index a05f019d..c50dc2c5 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -284,6 +284,10 @@ export default defineConfig({ text: "finish_fn", link: "/reference/builder/top-level/finish_fn", }, + { + text: "generics", + link: "/reference/builder/top-level/generics", + }, { text: "on", link: "/reference/builder/top-level/on", diff --git a/website/doctests/Cargo.toml b/website/doctests/Cargo.toml index 8ab9e1b0..0845a376 100644 --- a/website/doctests/Cargo.toml +++ b/website/doctests/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dev-dependencies] anyhow = "1.0" -bon = { path = "../../bon", features = ["experimental-overwritable", "implied-bounds"] } +bon = { path = "../../bon", features = ["experimental-overwritable", "experimental-generics-setters", "implied-bounds"] } buildstructor = "0.6" macro_rules_attribute = "0.2" tokio = { version = "1.47", features = ["macros", "rt-multi-thread"] } diff --git a/website/src/reference/builder/top-level/generics.md b/website/src/reference/builder/top-level/generics.md new file mode 100644 index 00000000..dcd41ebc --- /dev/null +++ b/website/src/reference/builder/top-level/generics.md @@ -0,0 +1,145 @@ +# `generics` + +**Applies to:** + +::: warning 🔬 Experimental + +This attribute is experimental and requires the `experimental-generics-setters` cargo feature to be enabled. The API may change in minor releases. + +::: + +Generates methods on the builder for converting generic type parameters to different types. This is useful when building up values with different types at different stages of construction. + +**Syntax:** + +```attr +#[builder(generics(setters(...)))] +``` + +## `setters` + +Configures the generic parameter conversion methods. + +**Short syntax** configures just the name pattern: + +```attr +#[builder(generics(setters = "conv_{}"))] +``` + +**Long syntax** provides more flexibility. The `name` parameter is required, while others are optional: + +```attr +#[builder( + generics(setters( + name = "conv_{}", + vis = "pub(crate)", + doc { + /// Custom documentation + } + )) +)] +``` + +### `name` + +**Required.** A pattern string where `{}` will be replaced with the `snake_case` name of each generic parameter. For example, with generic parameters `` and pattern `"with_{}"`, methods `with_t_data()` and `with_t_error()` will be generated. + +### `vis` + +The visibility for the generated conversion methods. Must be enclosed with quotes. Use `""` or [`"pub(self)"`](https://doc.rust-lang.org/reference/visibility-and-privacy.html#pubin-path-pubcrate-pubsuper-and-pubself) for private visibility. + +The default visibility matches the visibility of the [`builder_type`](./builder_type#vis). + +### `doc` + +Custom documentation for the generated conversion methods. The syntax expects a block with doc comments: + +```attr +doc { + /// Custom documentation +} +``` + +Simple documentation is generated by default explaining what the method does. + +## How It Works + +For each generic type parameter (not lifetimes or const generics), a conversion method is generated that: + +1. Takes the current builder and returns a new builder with the type parameter changed +2. Preserves all fields that don't use the converted generic parameter + +This allows you to start with placeholder types (like `()`) and convert them to concrete types as you build. + +## Example + +::: code-group + +```rust [Basic] +use bon::Builder; + +#[derive(Builder)] +#[builder(generics(setters = "with_{}"))] // [!code highlight] +struct Container { + data: TData, + error: TError, + count: u32, +} + +// Start with unit types, then convert to concrete types +let container = Container::<(), ()>::builder() + .count(42) + .with_t_data::() // Convert TData from () to i32 // [!code highlight] + .data(100) + .with_t_error::<&str>() // Convert TError from () to &str // [!code highlight] + .error("error message") + .build(); + +assert_eq!(container.data, 100); +assert_eq!(container.error, "error message"); +assert_eq!(container.count, 42); +``` + +```rust [Custom Methods] +use bon::Builder; + +#[derive(Builder)] +#[builder(generics(setters = "conv_{}"))] // [!code highlight] +struct Data { + value: T, +} + +// Use the generated conversion method to implement custom logic +impl DataBuilder { + fn convert_and_set( + self, + value: T2 + ) -> DataBuilder> + where + S::Value: data_builder::IsUnset, + { + self.conv_t().value(value) // [!code highlight] + } +} + +let data = Data::<()>::builder() + .convert_and_set(42) + .build(); + +assert_eq!(data.value, 42); +``` + +::: + +## Use Cases + +This feature is particularly useful for: + +- **Type-level state machines**: Starting with marker types and converting them as you progress through states +- **Builder patterns with type parameters**: When you need to build complex generic structs step-by-step +- **API design**: Allowing users to specify types incrementally rather than all at once + +## See Also + +- [Typestate API](../../../guide/typestate-api) - Understanding the builder's type system +- [`builder_type`](./builder_type) - Configuring the builder type itself