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