bon_macros/builder/
item_impl.rs

1use super::builder_gen::input_fn::{FnInputCtx, FnInputCtxParams, ImplCtx};
2use super::builder_gen::TopLevelConfig;
3use crate::normalization::{GenericsNamespace, SyntaxVariant};
4use crate::util::prelude::*;
5use darling::ast::NestedMeta;
6use darling::FromMeta;
7use std::rc::Rc;
8use syn::visit::Visit;
9use syn::visit_mut::VisitMut;
10
11#[derive(FromMeta)]
12pub(crate) struct ImplInputParams {
13    /// Overrides the path to the `bon` crate. This is usedfule when the macro is
14    /// wrapped in another macro that also reexports `bon`.
15    #[darling(rename = "crate", default, map = Some, with = crate::parsing::parse_bon_crate_path)]
16    bon: Option<syn::Path>,
17}
18
19// ImplInputParams will evolve in the future where we'll probably want to move from it
20#[allow(clippy::needless_pass_by_value)]
21pub(crate) fn generate(
22    impl_params: ImplInputParams,
23    mut orig_impl_block: syn::ItemImpl,
24) -> Result<TokenStream> {
25    let mut namespace = GenericsNamespace::default();
26    namespace.visit_item_impl(&orig_impl_block);
27
28    if let Some((_, trait_path, _)) = &orig_impl_block.trait_ {
29        bail!(trait_path, "Impls of traits are not supported yet");
30    }
31
32    let (builder_fns, other_items): (Vec<_>, Vec<_>) =
33        orig_impl_block.items.into_iter().partition(|item| {
34            let fn_item = match item {
35                syn::ImplItem::Fn(fn_item) => fn_item,
36                _ => return false,
37            };
38
39            fn_item
40                .attrs
41                .iter()
42                .any(|attr| attr.path().is_ident("builder"))
43        });
44
45    if builder_fns.is_empty() {
46        bail!(
47            &Span::call_site(),
48            "there are no #[builder] functions in the impl block, so there is no \
49            need for a #[bon] attribute here"
50        );
51    }
52
53    orig_impl_block.items = builder_fns;
54
55    // We do this back-and-forth with normalizing various syntax and saving original
56    // to provide cleaner code generation that is easier to consume for IDEs and for
57    // rust-analyzer specifically.
58    //
59    // For codegen logic we would like to have everything normalized. For example, we
60    // want to assume `Self` is replaced with the original type and all lifetimes are
61    // named, and `impl Traits` are desugared into type parameters.
62    //
63    // However, in output code we want to preserve existing `Self` references to make
64    // sure rust-analyzer highlights them properly. If we just strip `Self` from output
65    // code, then rust-analyzer won't be able to associate what `Self` token maps to in
66    // the input. It would highlight `Self` as an "unresolved symbol"
67    let mut norm_impl_block = orig_impl_block.clone();
68
69    crate::normalization::NormalizeLifetimes::new(&namespace)
70        .visit_item_impl_mut(&mut norm_impl_block);
71
72    crate::normalization::NormalizeImplTraits::new(&namespace)
73        .visit_item_impl_mut(&mut norm_impl_block);
74
75    // Retain a variant of the impl block without the normalized `Self` mentions.
76    // This way we preserve the original code that the user wrote with `Self` mentions
77    // as much as possible, therefore IDE's are able to provide correct syntax highlighting
78    // for `Self` mentions, because they aren't removed from the generated code output
79    let mut norm_selfful_impl_block = norm_impl_block.clone();
80
81    crate::normalization::NormalizeSelfTy {
82        self_ty: &norm_impl_block.self_ty.clone(),
83    }
84    .visit_item_impl_mut(&mut norm_impl_block);
85
86    let impl_ctx = Rc::new(ImplCtx {
87        self_ty: norm_impl_block.self_ty,
88        generics: norm_impl_block.generics,
89        allow_attrs: norm_impl_block
90            .attrs
91            .iter()
92            .filter_map(syn::Attribute::to_allow)
93            .collect(),
94    });
95
96    let outputs = orig_impl_block
97        .items
98        .into_iter()
99        .zip(norm_impl_block.items)
100        .map(|(orig_item, norm_item)| {
101            let norm_fn = match norm_item {
102                syn::ImplItem::Fn(norm_fn) => norm_fn,
103                _ => unreachable!(),
104            };
105            let orig_fn = match orig_item {
106                syn::ImplItem::Fn(orig_fn) => orig_fn,
107                _ => unreachable!(),
108            };
109
110            let norm_fn = conv_impl_item_fn_into_fn_item(norm_fn)?;
111            let orig_fn = conv_impl_item_fn_into_fn_item(orig_fn)?;
112
113            let meta = orig_fn
114                .attrs
115                .iter()
116                .filter(|attr| attr.path().is_ident("builder"))
117                .map(|attr| {
118                    if let syn::Meta::List(_) = attr.meta {
119                        crate::parsing::require_non_empty_paren_meta_list_or_name_value(
120                            &attr.meta,
121                        )?;
122                    }
123                    let meta_list = darling::util::parse_attribute_to_meta_list(attr)?;
124                    NestedMeta::parse_meta_list(meta_list.tokens).map_err(Into::into)
125                })
126                .collect::<Result<Vec<_>>>()?
127                .into_iter()
128                .flatten()
129                .collect::<Vec<_>>();
130
131            let mut config = TopLevelConfig::parse_for_fn(&meta)?;
132
133            if let Some(bon) = config.bon {
134                bail!(
135                    &bon,
136                    "`crate` parameter should be specified via `#[bon(crate = path::to::bon)]` \
137                    when impl block syntax is used; no need to specify it in the method's \
138                    `#[builder]` attribute"
139                );
140            }
141
142            config.bon.clone_from(&impl_params.bon);
143
144            let fn_item = SyntaxVariant {
145                orig: orig_fn,
146                norm: norm_fn,
147            };
148
149            let ctx = FnInputCtx::new(FnInputCtxParams {
150                namespace: &namespace,
151                fn_item,
152                impl_ctx: Some(impl_ctx.clone()),
153                config,
154            });
155
156            Result::<_>::Ok((ctx.adapted_fn()?, ctx.into_builder_gen_ctx()?.output()?))
157        })
158        .collect::<Result<Vec<_>>>()?;
159
160    let new_impl_items = outputs.iter().flat_map(|(adapted_fn, output)| {
161        let start_fn = &output.start_fn;
162        [syn::parse_quote!(#adapted_fn), syn::parse_quote!(#start_fn)]
163    });
164
165    norm_selfful_impl_block.items = other_items;
166    norm_selfful_impl_block.items.extend(new_impl_items);
167
168    let other_items = outputs.iter().map(|(_, output)| &output.other_items);
169
170    Ok(quote! {
171        // Keep the original impl block at the top. It seems like rust-analyzer
172        // does better job of highlighting syntax when it is here. Assuming
173        // this is because rust-analyzer prefers the first occurrence of the
174        // span when highlighting.
175        //
176        // See this issue for details: https://github.com/rust-lang/rust-analyzer/issues/18438
177        #norm_selfful_impl_block
178
179        #(#other_items)*
180    })
181}
182
183fn conv_impl_item_fn_into_fn_item(func: syn::ImplItemFn) -> Result<syn::ItemFn> {
184    let syn::ImplItemFn {
185        attrs,
186        vis,
187        defaultness,
188        sig,
189        block,
190    } = func;
191
192    if let Some(defaultness) = &defaultness {
193        bail!(defaultness, "Default functions are not supported yet");
194    }
195
196    Ok(syn::ItemFn {
197        attrs,
198        vis,
199        sig,
200        block: Box::new(block),
201    })
202}