diff --git a/bon-macros/src/builder/builder_gen/builder_derives.rs b/bon-macros/src/builder/builder_gen/builder_derives.rs index e0c6be36..f72638db 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives.rs @@ -1,3 +1,4 @@ +use super::builder_params::BuilderDerives; use super::BuilderGenCtx; use crate::builder::builder_gen::Member; use crate::util::prelude::*; @@ -5,18 +6,15 @@ use quote::quote; impl BuilderGenCtx { pub(crate) fn builder_derives(&self) -> TokenStream2 { - let derives = match &self.builder_derives { - Some(derives) => derives, - None => return quote!(), - }; + let BuilderDerives { clone, debug } = &self.builder_type.derives; let mut tokens = TokenStream2::new(); - if derives.clone.is_present() { + if clone.is_present() { tokens.extend(self.derive_clone()); } - if derives.debug.is_present() { + if debug.is_present() { tokens.extend(self.derive_debug()); } @@ -39,7 +37,7 @@ impl BuilderGenCtx { fn derive_clone(&self) -> TokenStream2 { let generics_decl = &self.generics.decl_without_defaults; let generic_args = &self.generics.args; - let builder_ident = &self.builder_ident; + let builder_ident = &self.builder_type.ident; let clone = quote!(::core::clone::Clone); @@ -89,7 +87,7 @@ impl BuilderGenCtx { fn derive_debug(&self) -> TokenStream2 { let generics_decl = &self.generics.decl_without_defaults; let generic_args = &self.generics.args; - let builder_ident = &self.builder_ident; + let builder_ident = &self.builder_type.ident; let debug = quote!(::core::fmt::Debug); diff --git a/bon-macros/src/builder/builder_gen/builder_params.rs b/bon-macros/src/builder/builder_gen/builder_params.rs index 8b277cc0..2fd4ed3a 100644 --- a/bon-macros/src/builder/builder_gen/builder_params.rs +++ b/bon-macros/src/builder/builder_gen/builder_params.rs @@ -6,19 +6,41 @@ use syn::parse::Parse; use syn::spanned::Spanned; use syn::visit::Visit; +fn parse_finish_fn(meta: &syn::Meta) -> Result { + ItemParamsParsing { + meta, + allow_vis: false, + reject_self_mentions: Some("builder struct's impl block"), + } + .parse() +} + +fn parse_builder_type(meta: &syn::Meta) -> Result { + ItemParamsParsing { + meta, + allow_vis: false, + reject_self_mentions: Some("builder struct"), + } + .parse() +} + #[derive(Debug, FromMeta)] pub(crate) struct BuilderParams { - pub(crate) finish_fn: Option, - pub(crate) builder_type: Option, + #[darling(default, with = parse_finish_fn)] + pub(crate) finish_fn: ItemParams, + + #[darling(default, with = parse_builder_type)] + pub(crate) builder_type: ItemParams, #[darling(multiple)] pub(crate) on: Vec, /// Specifies the derives to apply to the builder. - pub(crate) derive: Option, + #[darling(default)] + pub(crate) derive: BuilderDerives, } -#[derive(Debug, FromMeta)] +#[derive(Debug, Clone, Default, FromMeta)] pub(crate) struct BuilderDerives { #[darling(rename = "Clone")] pub(crate) clone: darling::util::Flag, @@ -108,21 +130,47 @@ impl FromMeta for OnParams { } } -#[derive(Debug, Default)] +#[derive(Debug, Clone, Default)] pub(crate) struct ItemParams { pub(crate) name: Option, pub(crate) vis: Option, + pub(crate) docs: Option>, } -impl FromMeta for ItemParams { - fn from_meta(meta: &syn::Meta) -> Result { +pub(crate) struct ItemParamsParsing<'a> { + pub(crate) meta: &'a syn::Meta, + pub(crate) allow_vis: bool, + pub(crate) reject_self_mentions: Option<&'static str>, +} + +impl ItemParamsParsing<'_> { + pub(crate) fn parse(self) -> Result { + let params = Self::params_from_meta(self.meta)?; + + if !self.allow_vis { + if let Some(vis) = ¶ms.vis { + bail!(vis, "visibility can't be overridden for this item"); + } + } + + if let Some(context) = self.reject_self_mentions { + if let Some(docs) = ¶ms.docs { + super::reject_self_mentions_in_docs(context, docs)?; + } + } + + Ok(params) + } + + fn params_from_meta(meta: &syn::Meta) -> Result { if let syn::Meta::NameValue(meta) = meta { let val = &meta.value; let name = syn::parse2(quote!(#val))?; - return Ok(Self { + return Ok(ItemParams { name: Some(name), vis: None, + docs: None, }); } @@ -130,6 +178,7 @@ impl FromMeta for ItemParams { struct Full { name: Option, vis: Option, + docs: Option, } let full = Full::from_meta(meta)?; @@ -139,6 +188,7 @@ impl FromMeta for ItemParams { Full { name: None, vis: None, + docs: None, } ); @@ -146,11 +196,28 @@ impl FromMeta for ItemParams { bail!(meta, "expected at least one parameter in parentheses"); } - let me = Self { + let docs = full + .docs + .map(|docs| { + let docs = docs.require_list()?; + let docs = docs.parse_args_with(syn::Attribute::parse_outer)?; + + for attr in &docs { + if !attr.is_doc() { + bail!(attr, "expected a doc comment"); + } + } + + Ok(docs) + }) + .transpose()?; + + let params = ItemParams { name: full.name, vis: full.vis, + docs, }; - Ok(me) + Ok(params) } } diff --git a/bon-macros/src/builder/builder_gen/input_func.rs b/bon-macros/src/builder/builder_gen/input_func.rs index d90bb3f7..ef7f8d43 100644 --- a/bon-macros/src/builder/builder_gen/input_func.rs +++ b/bon-macros/src/builder/builder_gen/input_func.rs @@ -3,6 +3,8 @@ use super::{ generic_param_to_arg, AssocMethodCtx, AssocMethodReceiverCtx, BuilderGenCtx, FinishFunc, FinishFuncBody, Generics, Member, MemberOrigin, RawMember, StartFunc, }; +use crate::builder::builder_gen::builder_params::ItemParams; +use crate::builder::builder_gen::BuilderType; use crate::normalization::NormalizeSelfTy; use crate::util::prelude::*; use darling::util::SpannedValue; @@ -134,8 +136,10 @@ impl FuncInputCtx { } fn builder_ident(&self) -> syn::Ident { - if let Some(builder_type) = &self.params.base.builder_type { - return builder_type.clone(); + let user_override = self.params.base.builder_type.name.as_ref(); + + if let Some(user_override) = user_override { + return user_override.clone(); } if self.is_method_new() { @@ -326,7 +330,13 @@ impl FuncInputCtx { self.norm_func.sig.ident.clone() }; - let finish_func_ident = self.params.base.finish_fn.unwrap_or_else(|| { + let ItemParams { + name: finish_func_ident, + vis: _, + docs: finish_func_docs, + } = self.params.base.finish_fn; + + let finish_func_ident = finish_func_ident.unwrap_or_else(|| { // For `new` methods the `build` finisher is more conventional if is_method_new { quote::format_ident!("build") @@ -335,6 +345,12 @@ impl FuncInputCtx { } }); + let finish_func_docs = finish_func_docs.unwrap_or_else(|| { + vec![syn::parse_quote! { + /// Finishes building and performs the requested action. + }] + }); + let finish_func = FinishFunc { ident: finish_func_ident, unsafety: self.norm_func.sig.unsafety, @@ -342,7 +358,7 @@ impl FuncInputCtx { must_use: get_must_use_attribute(&self.norm_func.attrs)?, body: Box::new(finish_func_body), output: self.norm_func.sig.output, - docs: "Finishes building and performs the requested action.".to_owned(), + attrs: finish_func_docs, }; let fn_allows = self @@ -382,20 +398,24 @@ impl FuncInputCtx { )), }; + let builder_type = BuilderType { + ident: builder_ident, + derives: self.params.base.derive, + docs: self.params.base.builder_type.docs, + }; + let ctx = BuilderGenCtx { members, allow_attrs, on_params: self.params.base.on, - builder_derives: self.params.base.derive, - - builder_ident, assoc_method_ctx: receiver, generics, vis: self.norm_func.vis, + builder_type, start_func, finish_func, }; diff --git a/bon-macros/src/builder/builder_gen/input_struct.rs b/bon-macros/src/builder/builder_gen/input_struct.rs index bd57f96d..23c54a1d 100644 --- a/bon-macros/src/builder/builder_gen/input_struct.rs +++ b/bon-macros/src/builder/builder_gen/input_struct.rs @@ -1,8 +1,9 @@ -use super::builder_params::{BuilderParams, ItemParams}; +use super::builder_params::{BuilderParams, ItemParams, ItemParamsParsing}; use super::{ AssocMethodCtx, BuilderGenCtx, FinishFunc, FinishFuncBody, Generics, Member, MemberOrigin, RawMember, StartFunc, }; +use crate::builder::builder_gen::BuilderType; use crate::util::prelude::*; use darling::FromMeta; use quote::quote; @@ -12,7 +13,18 @@ use syn::visit_mut::VisitMut; pub(crate) struct StructInputParams { #[darling(flatten)] base: BuilderParams, - start_fn: Option, + + #[darling(default, with = parse_start_fn)] + start_fn: ItemParams, +} + +fn parse_start_fn(meta: &syn::Meta) -> Result { + ItemParamsParsing { + meta, + allow_vis: true, + reject_self_mentions: None, + } + .parse() } impl StructInputParams { @@ -85,16 +97,20 @@ impl StructInputCtx { }) } - fn builder_ident(&self) -> syn::Ident { - if let Some(builder_type) = &self.params.base.builder_type { - return builder_type.clone(); - } + pub(crate) fn into_builder_gen_ctx(self) -> Result { + let builder_type = { + let ItemParams { name, vis: _, docs } = self.params.base.builder_type; - quote::format_ident!("{}Builder", self.norm_struct.ident.raw_name()) - } + let builder_ident = name.unwrap_or_else(|| { + quote::format_ident!("{}Builder", self.norm_struct.ident.raw_name()) + }); - pub(crate) fn into_builder_gen_ctx(self) -> Result { - let builder_ident = self.builder_ident(); + BuilderType { + derives: self.params.base.derive.clone(), + ident: builder_ident, + docs, + } + }; fn fields(struct_item: &syn::ItemStruct) -> Result<&syn::FieldsNamed> { match &struct_item.fields { @@ -140,16 +156,20 @@ impl StructInputCtx { let ItemParams { name: start_func_ident, vis: start_func_vis, - } = self.params.start_fn.unwrap_or_default(); + docs: start_func_docs, + } = self.params.start_fn; let start_func_ident = start_func_ident .unwrap_or_else(|| syn::Ident::new("builder", self.norm_struct.ident.span())); - let finish_func_ident = self - .params - .base - .finish_fn - .unwrap_or_else(|| syn::Ident::new("build", start_func_ident.span())); + let ItemParams { + name: finish_func_ident, + vis: _, + docs: finish_func_docs, + } = self.params.base.finish_fn; + + let finish_func_ident = + finish_func_ident.unwrap_or_else(|| syn::Ident::new("build", start_func_ident.span())); let struct_ty = &self.struct_ty; let finish_func = FinishFunc { @@ -161,18 +181,26 @@ impl StructInputCtx { }), body: Box::new(finish_func_body), output: syn::parse_quote!(-> #struct_ty), - docs: "Finishes building and returns the requested object.".to_owned(), + attrs: finish_func_docs.unwrap_or_else(|| { + vec![syn::parse_quote! { + /// Finishes building and returns the requested object + }] + }), }; - let start_func_docs = format!( - "Create an instance of [`{}`] using the builder syntax", - self.norm_struct.ident - ); + let start_func_docs = start_func_docs.unwrap_or_else(|| { + let docs = format!( + "Create an instance of [`{}`] using the builder syntax", + self.norm_struct.ident + ); + + vec![syn::parse_quote!(#[doc = #docs])] + }); let start_func = StartFunc { ident: start_func_ident, vis: start_func_vis, - attrs: vec![syn::parse_quote!(#[doc = #start_func_docs])], + attrs: start_func_docs, generics: None, }; @@ -194,14 +222,12 @@ impl StructInputCtx { allow_attrs, on_params: self.params.base.on, - builder_derives: self.params.base.derive, - - builder_ident, assoc_method_ctx, generics, vis: self.norm_struct.vis, + builder_type, start_func, finish_func, }; diff --git a/bon-macros/src/builder/builder_gen/member/mod.rs b/bon-macros/src/builder/builder_gen/member/mod.rs index f24f8839..4f8b8d4f 100644 --- a/bon-macros/src/builder/builder_gen/member/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/mod.rs @@ -124,43 +124,8 @@ pub(crate) struct SkippedMember { } impl NamedMember { - fn reject_self_references_in_docs(&self) -> Result { - for doc in &self.docs { - let doc = match doc.as_doc() { - Some(doc) => doc, - _ => continue, - }; - - let doc = match &doc { - syn::Expr::Lit(doc) => doc, - _ => continue, - }; - - let doc = match &doc.lit { - syn::Lit::Str(doc) => doc, - _ => continue, - }; - - let self_references = ["[`Self`]", "[Self]"]; - - if self_references - .iter() - .any(|self_ref| doc.value().contains(self_ref)) - { - bail!( - &doc.span(), - "The documentation for the member should not reference `Self` \ - because it will be moved to the builder struct context where \ - `Self` changes meaning. Use explicit type names instead.", - ); - } - } - - Ok(()) - } - fn validate(&self) -> Result { - self.reject_self_references_in_docs()?; + super::reject_self_mentions_in_docs("builder struct's impl block", &self.docs)?; if let Some(default) = &self.params.default { if self.norm_ty.is_option() { diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index 05f4d416..90bab8cc 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -34,33 +34,35 @@ pub(crate) struct BuilderGenCtx { /// generated by the macro. If the original syntax used `#[expect(...)]`, /// then it must be represented as `#[allow(...)]` here. allow_attrs: Vec, - on_params: Vec, - builder_derives: Option, generics: Generics, vis: syn::Visibility, assoc_method_ctx: Option, + builder_type: BuilderType, start_func: StartFunc, finish_func: FinishFunc, - - builder_ident: syn::Ident, } struct FinishFunc { ident: syn::Ident, + + /// Additional attributes to apply to the item + attrs: Vec, + unsafety: Option, asyncness: Option, /// must_use: Option, body: Box, output: syn::ReturnType, - docs: String, } struct StartFunc { ident: syn::Ident, + + /// Additional attributes to apply to the item attrs: Vec, /// Overrides the common generics @@ -70,6 +72,15 @@ struct StartFunc { vis: Option, } +struct BuilderType { + ident: syn::Ident, + + derives: BuilderDerives, + + /// Optional docs override + docs: Option>, +} + pub(crate) trait FinishFuncBody { /// Generate the `finish` function body from the ready-made variables. /// The generated function body may assume that there are variables @@ -197,7 +208,7 @@ impl BuilderGenCtx { .map(|member| &member.generic_var_ident) .collect::>(); - let builder_ident = &self.builder_ident; + let builder_ident = &self.builder_type.ident; let allows = allow_warnings_on_member_types(); @@ -274,7 +285,7 @@ impl BuilderGenCtx { } fn start_func(&self) -> Result { - let builder_ident = &self.builder_ident; + let builder_ident = &self.builder_type.ident; let docs = &self.start_func.attrs; let vis = self.start_func.vis.as_ref().unwrap_or(&self.vis); @@ -424,7 +435,7 @@ impl BuilderGenCtx { fn builder_decl(&self) -> TokenStream2 { let vis = &self.vis; - let builder_ident = &self.builder_ident; + let builder_ident = &self.builder_type.ident; let generics_decl = &self.generics.decl_with_defaults; let where_clause = &self.generics.where_clause; let phantom_data = self.phantom_data(); @@ -450,11 +461,17 @@ impl BuilderGenCtx { self.finish_func.ident ); - let docs = format!( - "Use builder syntax to set the required parameters and finish \ - by calling the method [`Self::{}()`].", - self.finish_func.ident - ); + let docs = self.builder_type.docs.clone().unwrap_or_else(|| { + let doc = format!( + "Use builder syntax to set the required parameters and finish \ + by calling the method [`Self::{}()`].", + self.finish_func.ident + ); + + vec![syn::parse_quote! { + #[doc = #doc] + }] + }); let allows = allow_warnings_on_member_types(); @@ -492,7 +509,7 @@ impl BuilderGenCtx { #vis type #initial_state_type_alias_ident = (#(#unset_state_types,)*); #[must_use = #must_use_message] - #[doc = #docs] + #(#docs)* #allows #[allow( // We use `__private` prefix for all fields intentionally to hide them @@ -592,7 +609,7 @@ impl BuilderGenCtx { fn members_label(&self, member: &NamedMember) -> syn::Ident { quote::format_ident!( "{}__{}", - self.builder_ident.raw_name(), + self.builder_type.ident.raw_name(), member.setter_method_core_name() ) } @@ -625,7 +642,7 @@ impl BuilderGenCtx { let asyncness = &self.finish_func.asyncness; let unsafety = &self.finish_func.unsafety; let must_use = &self.finish_func.must_use; - let docs = &self.finish_func.docs; + let attrs = &self.finish_func.attrs; let vis = &self.vis; let finish_func_ident = &self.finish_func.ident; let output = &self.finish_func.output; @@ -650,7 +667,7 @@ impl BuilderGenCtx { .collect::>>()?; Ok(quote! { - #[doc = #docs] + #(#attrs)* #[inline(always)] #[allow( // This is intentional. We want the builder syntax to compile away @@ -679,7 +696,7 @@ impl BuilderGenCtx { fn setter_methods(&self) -> Result<(TokenStream2, TokenStream2)> { let generics_decl = &self.generics.decl_without_defaults; let generic_args = &self.generics.args; - let builder_ident = &self.builder_ident; + let builder_ident = &self.builder_type.ident; let where_clause = &self.generics.where_clause; let state_type_vars = self @@ -805,3 +822,42 @@ fn allow_warnings_on_member_types() -> TokenStream2 { #[allow(unused_parens)] } } + +/// Validates the docs for the presence of `Self` mentions to prevent users from +/// shooting themselves in the foot where they would think that `Self` resolves +/// to the current item the docs were placed on, when in fact the docs are moved +/// to a different context where `Self` has a different meaning. +fn reject_self_mentions_in_docs(context: &'static str, attrs: &[syn::Attribute]) -> Result { + for attr in attrs { + let doc = match attr.as_doc() { + Some(doc) => doc, + _ => continue, + }; + + let doc = match &doc { + syn::Expr::Lit(doc) => doc, + _ => continue, + }; + + let doc = match &doc.lit { + syn::Lit::Str(doc) => doc, + _ => continue, + }; + + let self_references = ["[`Self`]", "[Self]"]; + + if self_references + .iter() + .any(|self_ref| doc.value().contains(self_ref)) + { + bail!( + &doc.span(), + "the documentation should not reference `Self` because it will \ + be moved to the {context} where `Self` changes meaning, which \ + may confuse the reader of this code; use explicit type names instead.", + ); + } + } + + Ok(()) +} diff --git a/bon-macros/src/builder/builder_gen/setter_methods.rs b/bon-macros/src/builder/builder_gen/setter_methods.rs index 06dae463..3c48435c 100644 --- a/bon-macros/src/builder/builder_gen/setter_methods.rs +++ b/bon-macros/src/builder/builder_gen/setter_methods.rs @@ -144,7 +144,7 @@ impl<'a> MemberSettersCtx<'a> { .next() .map(|_| quote!(__private_start_fn_args: self.__private_start_fn_args,)); - let builder_ident = &self.builder_gen.builder_ident; + let builder_ident = &self.builder_gen.builder_type.ident; let member_exprs = self.builder_gen.named_members().map(|other_member| { if other_member.norm_ident == self.member.norm_ident { diff --git a/bon/src/private/mod.rs b/bon/src/private/mod.rs index bb416f13..015003d8 100644 --- a/bon/src/private/mod.rs +++ b/bon/src/private/mod.rs @@ -152,7 +152,7 @@ macro_rules! __eval_cfg_callback { $($rest:tt)* ) => { // The `pred_id` is required to be a unique identifier for the current - // predicate evaluation so that we can use in a `use` statement to define + // predicate evaluation so that we can use it in a `use` statement to define // a new unique name for the macro to call. #[cfg($($pred)*)] #[doc(hidden)] diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index dcf7f39e..62ed539f 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -16,11 +16,38 @@ mod website_doctests { include!(concat!(env!("OUT_DIR"), "/website_doctests.rs")); } +/// Docs on the [`Self`] struct #[derive(bon::Builder)] +#[builder( + builder_type( + docs { + /// Docs on [`GreeterOverriddenBuilder`] + /// the builder type + }, + name = GreeterOverriddenBuilder, + ), + start_fn( + docs { + /// Docs on + /// [`Self::start_fn_override`] + }, + name = start_fn_override, + ), + finish_fn( + docs { + /// Docs on + /// [`GreeterOverriddenBuilder::finish_fn_override()`] + }, + name = finish_fn_override, + ) +)] pub struct Greeter { + /// Docs on + /// the `name` field _name: String, - // #[cfg_attr(all(), builder(start_fn = init))] + /// Docs on + /// the `level` field _level: usize, } diff --git a/release-plz.toml b/release-plz.toml index b6cdd24a..2e05d6b0 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -6,10 +6,11 @@ git_tag_enable = false changelog_update = false [[package]] +name = "bon" + changelog_include = ["bon-macros"] changelog_path = "website/changelog.md" changelog_update = true -name = "bon" git_release_enable = true git_release_name = "v{{ version }}" diff --git a/website/blog/bon-builder-v2-3-release.md b/website/blog/bon-builder-v2-3-release.md index 03a8f19a..76e6df7a 100644 --- a/website/blog/bon-builder-v2-3-release.md +++ b/website/blog/bon-builder-v2-3-release.md @@ -121,7 +121,7 @@ You may also combine these attributes with [`#[builder(into)]`](../reference/bui On the previous week's update (2.2 release) [a promise was made](./bon-builder-v2-2-release#guaranteed-msrv) to reduce the MSRV (minimum supported Rust version) from the initial 1.70.0 even further, and this has been done 🎉! -This is the lowest possible MSRV we can guarantee for now. The choice of this version was made based on our design requirements for const generics supports described in [the comment here](https://github.com/elastio/bon/blob/3217b4b0349f03f0b2a5853310f420c5b8b005a7/bon/Cargo.toml#L21-L28). +This is the lowest possible MSRV we can guarantee for now. The choice of this version was made based on our design requirements for const generics support described in [the comment here](https://github.com/elastio/bon/blob/3217b4b0349f03f0b2a5853310f420c5b8b005a7/bon/Cargo.toml#L21-L28). ## Deprecation warnings diff --git a/website/blog/how-to-do-named-function-arguments-in-rust.md b/website/blog/how-to-do-named-function-arguments-in-rust.md index e1cdcb61..a9a597dc 100644 --- a/website/blog/how-to-do-named-function-arguments-in-rust.md +++ b/website/blog/how-to-do-named-function-arguments-in-rust.md @@ -134,7 +134,7 @@ struct User { id: u32, // This attribute makes the setter accept `impl Into` - // which let's us pass an `&str` directly and it'll be automatically + // which lets us pass an `&str` directly and it'll be automatically // converted into `String`. #[builder(into)] name: String, diff --git a/website/guide/overview.md b/website/guide/overview.md index bc9c949e..b9e6939e 100644 --- a/website/guide/overview.md +++ b/website/guide/overview.md @@ -291,7 +291,7 @@ If you can't figure something out, consult the docs and maybe use that search ` X (Twitter) - Profile of the maintainer. There are only posts about bon and Rust in general there. + Profile of the maintainer. There are only posts about bon and Rust in general here.