bon_macros/builder/builder_gen/
input_fn.rs

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