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::parsing::BonCratePath;
5use crate::util::prelude::*;
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 useful when the macro is
14    /// wrapped in another macro that also reexports `bon`.
15    #[darling(rename = "crate", default)]
16    bon: BonCratePath,
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 mut config = TopLevelConfig::parse_for_fn(&orig_fn, None)?;
114
115            if let BonCratePath::Explicit(path) = config.bon {
116                bail!(
117                    &path,
118                    "`crate` parameter should be specified via `#[bon(crate = path::to::bon)]` \
119                    when impl block syntax is used; no need to specify it in the method's \
120                    `#[builder]` attribute"
121                );
122            }
123
124            config.bon.clone_from(&impl_params.bon);
125
126            let fn_item = SyntaxVariant {
127                orig: orig_fn,
128                norm: norm_fn,
129            };
130
131            let ctx = FnInputCtx::new(FnInputCtxParams {
132                namespace: &namespace,
133                fn_item,
134                impl_ctx: Some(impl_ctx.clone()),
135                config,
136            })?;
137
138            let adapted_fn = ctx.adapted_fn()?;
139            let warnings = ctx.warnings();
140
141            let mut output = ctx.into_builder_gen_ctx()?.output()?;
142            output.other_items.extend(warnings);
143
144            Result::<_>::Ok((adapted_fn, output))
145        })
146        .collect::<Result<Vec<_>>>()?;
147
148    let new_impl_items = outputs.iter().flat_map(|(adapted_fn, output)| {
149        let start_fn = &output.start_fn;
150        [syn::parse_quote!(#adapted_fn), syn::parse_quote!(#start_fn)]
151    });
152
153    norm_selfful_impl_block.items = other_items;
154    norm_selfful_impl_block.items.extend(new_impl_items);
155
156    let other_items = outputs.iter().map(|(_, output)| &output.other_items);
157
158    Ok(quote! {
159        // Keep the original impl block at the top. It seems like rust-analyzer
160        // does better job of highlighting syntax when it is here. Assuming
161        // this is because rust-analyzer prefers the first occurrence of the
162        // span when highlighting.
163        //
164        // See this issue for details: https://github.com/rust-lang/rust-analyzer/issues/18438
165        #norm_selfful_impl_block
166
167        #(#other_items)*
168    })
169}
170
171fn conv_impl_item_fn_into_fn_item(func: syn::ImplItemFn) -> Result<syn::ItemFn> {
172    let syn::ImplItemFn {
173        attrs,
174        vis,
175        defaultness,
176        sig,
177        block,
178    } = func;
179
180    if let Some(defaultness) = &defaultness {
181        bail!(defaultness, "Default functions are not supported yet");
182    }
183
184    Ok(syn::ItemFn {
185        attrs,
186        vis,
187        sig,
188        block: Box::new(block),
189    })
190}