bon_macros/builder/builder_gen/input_fn/
mod.rs

1mod validation;
2
3use super::models::{AssocMethodReceiverCtxParams, FinishFnParams};
4use super::top_level_config::TopLevelConfig;
5use super::{
6    AssocMethodCtxParams, BuilderGenCtx, FinishFnBody, Generics, Member, MemberOrigin, RawMember,
7};
8use crate::builder::builder_gen::models::{BuilderGenCtxParams, BuilderTypeParams, StartFnParams};
9use crate::normalization::{GenericsNamespace, NormalizeSelfTy, SyntaxVariant};
10use crate::parsing::{ItemSigConfig, SpannedKey};
11use crate::util::prelude::*;
12use std::borrow::Cow;
13use std::rc::Rc;
14use syn::punctuated::Punctuated;
15use syn::spanned::Spanned;
16use syn::visit_mut::VisitMut;
17
18pub(crate) struct FnInputCtx<'a> {
19    namespace: &'a GenericsNamespace,
20    fn_item: SyntaxVariant<syn::ItemFn>,
21    impl_ctx: Option<Rc<ImplCtx>>,
22    config: TopLevelConfig,
23
24    start_fn: StartFnParams,
25    self_ty_prefix: Option<String>,
26}
27
28pub(crate) struct FnInputCtxParams<'a> {
29    pub(crate) namespace: &'a GenericsNamespace,
30    pub(crate) fn_item: SyntaxVariant<syn::ItemFn>,
31    pub(crate) impl_ctx: Option<Rc<ImplCtx>>,
32    pub(crate) config: TopLevelConfig,
33}
34
35pub(crate) struct ImplCtx {
36    pub(crate) self_ty: Box<syn::Type>,
37    pub(crate) generics: syn::Generics,
38
39    /// Lint suppressions from the original item that will be inherited by all items
40    /// generated by the macro. If the original syntax used `#[expect(...)]`,
41    /// then it must be represented as `#[allow(...)]` here.
42    pub(crate) allow_attrs: Vec<syn::Attribute>,
43}
44
45impl<'a> FnInputCtx<'a> {
46    pub(crate) fn new(params: FnInputCtxParams<'a>) -> Result<Self> {
47        let start_fn = params.config.start_fn.clone();
48
49        let start_fn_ident = start_fn
50            .name
51            .map(SpannedKey::into_value)
52            .unwrap_or_else(|| {
53                let fn_ident = &params.fn_item.norm.sig.ident;
54
55                // Special case for the method named `new`. We rename it to `builder`
56                // since this is the name that is conventionally used by starting
57                // function in the builder pattern. We also want to make
58                // the `#[builder]` attribute on the method `new` fully compatible
59                // with deriving a builder from a struct.
60                if params.impl_ctx.is_some() && fn_ident == "new" {
61                    syn::Ident::new("builder", fn_ident.span())
62                } else {
63                    fn_ident.clone()
64                }
65            });
66
67        let start_fn = StartFnParams {
68            ident: start_fn_ident,
69
70            vis: start_fn.vis.map(SpannedKey::into_value),
71
72            docs: start_fn
73                .docs
74                .map(SpannedKey::into_value)
75                .unwrap_or_else(|| {
76                    params
77                        .fn_item
78                        .norm
79                        .attrs
80                        .iter()
81                        .filter(|attr| attr.is_doc_expr())
82                        .cloned()
83                        .collect()
84                }),
85
86            // Override on the start fn to use the generics from the
87            // target function itself. We must not duplicate the generics
88            // from the impl block here
89            generics: Some(Generics::new(
90                params
91                    .fn_item
92                    .norm
93                    .sig
94                    .generics
95                    .params
96                    .iter()
97                    .cloned()
98                    .collect(),
99                params.fn_item.norm.sig.generics.where_clause.clone(),
100            )),
101            // We don't use params.fn_item.orig.span() here because that span
102            // only contains the annotations before the function and the
103            // function name. Block contains function name + implementation
104            // which is what rustdoc normally links to.
105            span: Some(params.fn_item.orig.block.span()),
106        };
107
108        let self_ty_prefix = params.impl_ctx.as_deref().and_then(|impl_ctx| {
109            let prefix = impl_ctx
110                .self_ty
111                .as_path()?
112                .path
113                .segments
114                .last()?
115                .ident
116                .to_string();
117
118            Some(prefix)
119        });
120
121        let ctx = Self {
122            namespace: params.namespace,
123            fn_item: params.fn_item,
124            impl_ctx: params.impl_ctx,
125            config: params.config,
126            self_ty_prefix,
127            start_fn,
128        };
129
130        ctx.validate()?;
131
132        Ok(ctx)
133    }
134
135    fn assoc_method_ctx(&self) -> Result<Option<AssocMethodCtxParams>> {
136        let self_ty = match self.impl_ctx.as_deref() {
137            Some(impl_ctx) => impl_ctx.self_ty.clone(),
138            None => return Ok(None),
139        };
140
141        Ok(Some(AssocMethodCtxParams {
142            self_ty,
143            receiver: self.assoc_method_receiver_ctx_params()?,
144        }))
145    }
146
147    fn assoc_method_receiver_ctx_params(&self) -> Result<Option<AssocMethodReceiverCtxParams>> {
148        let receiver = match self.fn_item.norm.sig.receiver() {
149            Some(receiver) => receiver,
150            None => return Ok(None),
151        };
152
153        let builder_attr_on_receiver = receiver
154            .attrs
155            .iter()
156            .find(|attr| attr.path().is_ident("builder"));
157
158        if let Some(attr) = builder_attr_on_receiver {
159            bail!(
160                attr,
161                "#[builder] attributes on the receiver are not supported"
162            );
163        }
164
165        let self_ty = match self.impl_ctx.as_deref() {
166            Some(impl_ctx) => &impl_ctx.self_ty,
167            None => return Ok(None),
168        };
169
170        let mut without_self_keyword = receiver.ty.clone();
171
172        NormalizeSelfTy { self_ty }.visit_type_mut(&mut without_self_keyword);
173
174        Ok(Some(AssocMethodReceiverCtxParams {
175            with_self_keyword: receiver.clone(),
176            without_self_keyword,
177        }))
178    }
179
180    fn generics(&self) -> Generics {
181        let impl_ctx = self.impl_ctx.as_ref();
182        let norm_fn_params = &self.fn_item.norm.sig.generics.params;
183        let params = impl_ctx
184            .map(|impl_ctx| merge_generic_params(&impl_ctx.generics.params, norm_fn_params))
185            .unwrap_or_else(|| norm_fn_params.iter().cloned().collect());
186
187        let where_clauses = [
188            self.fn_item.norm.sig.generics.where_clause.clone(),
189            impl_ctx.and_then(|impl_ctx| impl_ctx.generics.where_clause.clone()),
190        ];
191
192        let where_clause = where_clauses
193            .into_iter()
194            .flatten()
195            .reduce(|mut combined, clause| {
196                combined.predicates.extend(clause.predicates);
197                combined
198            });
199
200        Generics::new(params, where_clause)
201    }
202
203    pub(crate) fn adapted_fn(&self) -> Result<syn::ItemFn> {
204        let mut orig = self.fn_item.orig.clone();
205
206        if let Some(name) = self.config.start_fn.name.as_deref() {
207            if *name == orig.sig.ident {
208                bail!(
209                    &name,
210                    "the starting function name must be different from the name \
211                    of the positional function under the #[builder] attribute"
212                )
213            }
214        } else {
215            // By default the original positional function becomes hidden.
216            orig.vis = syn::Visibility::Inherited;
217
218            // Remove all doc comments from the function itself to avoid docs duplication
219            // which may lead to duplicating doc tests, which in turn implies repeated doc
220            // tests execution, which means worse tests performance.
221            //
222            // We don't do this for the case when the positional function is exposed
223            // alongside the builder which implies that the docs should be visible
224            // as the function itself is visible.
225            orig.attrs.retain(|attr| !attr.is_doc_expr());
226
227            let bon = &self.config.bon;
228
229            orig.attrs.extend([
230                syn::parse_quote!(#[doc(hidden)]),
231                // We don't rename the function immediately, but instead defer the renaming
232                // to a later stage. This is because the original name of the function can
233                // be used by other macros that may need a stable identifier.
234                //
235                // For example, if `#[tracing::instrument]` is placed on the function,
236                // the function name will be used as a span name.
237                syn::parse_quote!(#[#bon::__::__privatize]),
238            ]);
239        }
240
241        // Remove any `#[builder]` attributes that were meant for this proc macro.
242        orig.attrs.retain(|attr| !attr.path().is_ident("builder"));
243
244        // Remove all doc comments attributes from function arguments, because they are
245        // not valid in that position in regular Rust code. The cool trick is that they
246        // are still valid syntactically when a proc macro like this one pre-processes
247        // them and removes them from the expanded code. We use the doc comments to put
248        // them on the generated setter methods.
249        //
250        // We also strip all `builder(...)` attributes because this macro processes them
251        // and they aren't needed in the output.
252        for arg in &mut orig.sig.inputs {
253            arg.attrs_mut()
254                .retain(|attr| !attr.is_doc_expr() && !attr.path().is_ident("builder"));
255        }
256
257        orig.attrs.push(syn::parse_quote!(#[allow(
258            // It's fine if there are too many positional arguments in the function
259            // because the whole purpose of this macro is to fight with this problem
260            // at the call site by generating a builder, while keeping the fn definition
261            // site the same with tons of positional arguments which don't harm readability
262            // there because their names are explicitly specified at the definition site.
263            clippy::too_many_arguments,
264
265            // It's fine to use many bool arguments in the function signature because
266            // all of them will be named at the call site when the builder is used.
267            clippy::fn_params_excessive_bools,
268        )]));
269
270        Ok(orig)
271    }
272
273    pub(crate) fn into_builder_gen_ctx(self) -> Result<BuilderGenCtx> {
274        let assoc_method_ctx = self.assoc_method_ctx()?;
275
276        let members = self
277            .fn_item
278            .apply_ref(|fn_item| fn_item.sig.inputs.iter().filter_map(syn::FnArg::as_typed))
279            .into_iter()
280            .map(|arg| {
281                let pat = match arg.norm.pat.as_ref() {
282                    syn::Pat::Ident(pat) => pat,
283                    _ => bail!(
284                        &arg.orig.pat,
285                        "use a simple `identifier: type` syntax for the function argument; \
286                        destructuring patterns in arguments aren't supported by the `#[builder]`",
287                    ),
288                };
289
290                let ty = SyntaxVariant {
291                    norm: arg.norm.ty.clone(),
292                    orig: arg.orig.ty.clone(),
293                };
294
295                Ok(RawMember {
296                    attrs: &arg.norm.attrs,
297                    ident: pat.ident.clone(),
298                    ty,
299                    span: pat.ident.span(),
300                })
301            })
302            .collect::<Result<Vec<_>>>()?;
303
304        let members = Member::from_raw(&self.config, MemberOrigin::FnArg, members)?;
305
306        let generics = self.generics();
307        let mut adapted_fn_sig = self.adapted_fn()?.sig;
308
309        if self.config.start_fn.name.is_none() {
310            crate::privatize::privatize_fn_name(&mut adapted_fn_sig);
311        }
312
313        let finish_fn_body = FnCallBody {
314            sig: adapted_fn_sig,
315            impl_ctx: self.impl_ctx.clone(),
316        };
317
318        let ItemSigConfig {
319            name: finish_fn_ident,
320            vis: finish_fn_vis,
321            docs: finish_fn_docs,
322        } = self.config.finish_fn;
323
324        let is_special_builder_method = self.impl_ctx.is_some()
325            && (self.fn_item.norm.sig.ident == "new" || self.fn_item.norm.sig.ident == "builder");
326
327        let finish_fn_ident = finish_fn_ident
328            .map(SpannedKey::into_value)
329            .unwrap_or_else(|| {
330                // For `builder` methods the `build` finisher is more conventional
331                if is_special_builder_method {
332                    format_ident!("build")
333                } else {
334                    format_ident!("call")
335                }
336            });
337
338        let finish_fn_docs = finish_fn_docs
339            .map(SpannedKey::into_value)
340            .unwrap_or_else(|| {
341                vec![syn::parse_quote! {
342                    /// Finishes building and performs the requested action.
343                }]
344            });
345
346        let finish_fn = FinishFnParams {
347            ident: finish_fn_ident,
348            vis: finish_fn_vis.map(SpannedKey::into_value),
349            unsafety: self.fn_item.norm.sig.unsafety,
350            asyncness: self.fn_item.norm.sig.asyncness,
351            special_attrs: get_propagated_attrs(&self.fn_item.norm.attrs)?,
352            body: Box::new(finish_fn_body),
353            output: self.fn_item.norm.sig.output,
354            attrs: finish_fn_docs,
355        };
356
357        let fn_allows = self
358            .fn_item
359            .norm
360            .attrs
361            .iter()
362            .filter_map(syn::Attribute::to_allow);
363
364        let allow_attrs = self
365            .impl_ctx
366            .as_ref()
367            .into_iter()
368            .flat_map(|impl_ctx| impl_ctx.allow_attrs.iter().cloned())
369            .chain(fn_allows)
370            .collect();
371
372        let builder_ident = || {
373            let user_override = self.config.builder_type.name.map(SpannedKey::into_value);
374
375            if let Some(user_override) = user_override {
376                return user_override;
377            }
378
379            let ty_prefix = self.self_ty_prefix.unwrap_or_default();
380
381            // A special case for the `new` or `builder` method.
382            // We don't insert the `Builder` suffix in this case because
383            // this special case should be compatible with deriving
384            // a builder from a struct.
385            //
386            // We can arrive inside of this branch only if the function under
387            // the macro is called `new` or `builder`.
388            if is_special_builder_method {
389                return format_ident!("{ty_prefix}Builder");
390            }
391
392            let pascal_case_fn = self.fn_item.norm.sig.ident.snake_to_pascal_case();
393
394            format_ident!("{ty_prefix}{pascal_case_fn}Builder")
395        };
396
397        let builder_type = BuilderTypeParams {
398            ident: builder_ident(),
399            derives: self.config.derive,
400            docs: self.config.builder_type.docs.map(SpannedKey::into_value),
401            vis: self.config.builder_type.vis.map(SpannedKey::into_value),
402        };
403
404        BuilderGenCtx::new(BuilderGenCtxParams {
405            bon: self.config.bon,
406            namespace: Cow::Borrowed(self.namespace),
407            members,
408
409            allow_attrs,
410
411            const_: self.config.const_,
412            on: self.config.on,
413
414            assoc_method_ctx,
415            generics,
416            orig_item_vis: self.fn_item.norm.vis,
417
418            builder_type,
419            state_mod: self.config.state_mod,
420            start_fn: self.start_fn,
421            finish_fn,
422        })
423    }
424}
425
426struct FnCallBody {
427    sig: syn::Signature,
428    impl_ctx: Option<Rc<ImplCtx>>,
429}
430
431impl FinishFnBody for FnCallBody {
432    fn generate(&self, ctx: &BuilderGenCtx) -> TokenStream {
433        let asyncness = &self.sig.asyncness;
434        let maybe_await = asyncness.is_some().then(|| quote!(.await));
435
436        // Filter out lifetime generic arguments, because they are not needed
437        // to be specified explicitly when calling the function. This also avoids
438        // the problem that it's not always possible to specify lifetimes in
439        // the turbofish syntax. See the problem of late-bound lifetimes specification
440        // in the issue https://github.com/rust-lang/rust/issues/42868
441        let generic_args = self
442            .sig
443            .generics
444            .params
445            .iter()
446            .filter(|arg| !matches!(arg, syn::GenericParam::Lifetime(_)))
447            .map(syn::GenericParam::to_generic_argument);
448
449        let prefix = ctx
450            .assoc_method_ctx
451            .as_ref()
452            .and_then(|ctx| {
453                let ident = &ctx.receiver.as_ref()?.field_ident;
454                Some(quote!(self.#ident.))
455            })
456            .or_else(|| {
457                let self_ty = &self.impl_ctx.as_deref()?.self_ty;
458                Some(quote!(<#self_ty>::))
459            });
460
461        let fn_ident = &self.sig.ident;
462
463        // The variables with values of members are in scope for this expression.
464        let member_vars = ctx.members.iter().map(Member::orig_ident);
465
466        quote! {
467            #prefix #fn_ident::<#(#generic_args,)*>(
468                #( #member_vars ),*
469            )
470            #maybe_await
471        }
472    }
473}
474
475/// To merge generic params we need to make sure lifetimes are always the first
476/// in the resulting list according to Rust syntax restrictions.
477fn merge_generic_params(
478    left: &Punctuated<syn::GenericParam, syn::Token![,]>,
479    right: &Punctuated<syn::GenericParam, syn::Token![,]>,
480) -> Vec<syn::GenericParam> {
481    let is_lifetime = |param: &&_| matches!(param, &&syn::GenericParam::Lifetime(_));
482
483    let (left_lifetimes, left_rest): (Vec<_>, Vec<_>) = left.iter().partition(is_lifetime);
484    let (right_lifetimes, right_rest): (Vec<_>, Vec<_>) = right.iter().partition(is_lifetime);
485
486    left_lifetimes
487        .into_iter()
488        .chain(right_lifetimes)
489        .chain(left_rest)
490        .chain(right_rest)
491        .cloned()
492        .collect()
493}
494
495const PROPAGATED_ATTRIBUTES: &[&str] = &["must_use", "track_caller", "target_feature"];
496
497fn get_propagated_attrs(attrs: &[syn::Attribute]) -> Result<Vec<syn::Attribute>> {
498    PROPAGATED_ATTRIBUTES
499        .iter()
500        .copied()
501        .filter_map(|needle| find_propagated_attr(attrs, needle).transpose())
502        .collect()
503}
504
505fn find_propagated_attr(attrs: &[syn::Attribute], needle: &str) -> Result<Option<syn::Attribute>> {
506    let mut iter = attrs
507        .iter()
508        .filter(|attr| attr.meta.path().is_ident(needle));
509
510    let result = iter.next();
511
512    if let Some(second) = iter.next() {
513        bail!(
514            second,
515            "found multiple #[{}], but bon only works with exactly one or zero.",
516            needle
517        );
518    }
519
520    if let Some(attr) = result {
521        if let syn::AttrStyle::Inner(_) = attr.style {
522            bail!(
523                attr,
524                "#[{}] attribute must be placed on the function itself, \
525                not inside it.",
526                needle
527            );
528        }
529    }
530
531    Ok(result.cloned())
532}