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        return Err(no_builder_attrs_error(&other_items));
47    }
48
49    orig_impl_block.items = builder_fns;
50
51    // We do this back-and-forth with normalizing various syntax and saving original
52    // to provide cleaner code generation that is easier to consume for IDEs and for
53    // rust-analyzer specifically.
54    //
55    // For codegen logic we would like to have everything normalized. For example, we
56    // want to assume `Self` is replaced with the original type and all lifetimes are
57    // named, and `impl Traits` are desugared into type parameters.
58    //
59    // However, in output code we want to preserve existing `Self` references to make
60    // sure rust-analyzer highlights them properly. If we just strip `Self` from output
61    // code, then rust-analyzer won't be able to associate what `Self` token maps to in
62    // the input. It would highlight `Self` as an "unresolved symbol"
63    let mut norm_impl_block = orig_impl_block.clone();
64
65    crate::normalization::NormalizeLifetimes::new(&namespace)
66        .visit_item_impl_mut(&mut norm_impl_block);
67
68    crate::normalization::NormalizeImplTraits::new(&namespace)
69        .visit_item_impl_mut(&mut norm_impl_block);
70
71    // Retain a variant of the impl block without the normalized `Self` mentions.
72    // This way we preserve the original code that the user wrote with `Self` mentions
73    // as much as possible, therefore IDE's are able to provide correct syntax highlighting
74    // for `Self` mentions, because they aren't removed from the generated code output
75    let mut norm_selfful_impl_block = norm_impl_block.clone();
76
77    crate::normalization::NormalizeSelfTy {
78        self_ty: &norm_impl_block.self_ty.clone(),
79    }
80    .visit_item_impl_mut(&mut norm_impl_block);
81
82    let impl_ctx = Rc::new(ImplCtx {
83        self_ty: norm_impl_block.self_ty,
84        generics: norm_impl_block.generics,
85        allow_attrs: norm_impl_block
86            .attrs
87            .iter()
88            .filter_map(syn::Attribute::to_allow)
89            .collect(),
90    });
91
92    let outputs = orig_impl_block
93        .items
94        .into_iter()
95        .zip(norm_impl_block.items)
96        .map(|(orig_item, norm_item)| {
97            let norm_fn = match norm_item {
98                syn::ImplItem::Fn(norm_fn) => norm_fn,
99                _ => unreachable!(),
100            };
101            let orig_fn = match orig_item {
102                syn::ImplItem::Fn(orig_fn) => orig_fn,
103                _ => unreachable!(),
104            };
105
106            let norm_fn = conv_impl_item_fn_into_fn_item(norm_fn)?;
107            let orig_fn = conv_impl_item_fn_into_fn_item(orig_fn)?;
108
109            let mut config = TopLevelConfig::parse_for_fn(&orig_fn, None)?;
110
111            if let BonCratePath::Explicit(path) = config.bon {
112                bail!(
113                    &path,
114                    "`crate` parameter should be specified via `#[bon(crate = path::to::bon)]` \
115                    when impl block syntax is used; no need to specify it in the method's \
116                    `#[builder]` attribute"
117                );
118            }
119
120            config.bon.clone_from(&impl_params.bon);
121
122            let fn_item = SyntaxVariant {
123                orig: orig_fn,
124                norm: norm_fn,
125            };
126
127            let ctx = FnInputCtx::new(FnInputCtxParams {
128                namespace: &namespace,
129                fn_item,
130                impl_ctx: Some(impl_ctx.clone()),
131                config,
132            })?;
133
134            let adapted_fn = ctx.adapted_fn()?;
135            let warnings = ctx.warnings();
136
137            let mut output = ctx.into_builder_gen_ctx()?.output()?;
138            output.other_items.extend(warnings);
139
140            Result::<_>::Ok((adapted_fn, output))
141        })
142        .collect::<Result<Vec<_>>>()?;
143
144    let new_impl_items = outputs.iter().flat_map(|(adapted_fn, output)| {
145        let start_fn = &output.start_fn;
146        [syn::parse_quote!(#adapted_fn), syn::parse_quote!(#start_fn)]
147    });
148
149    norm_selfful_impl_block.items = other_items;
150    norm_selfful_impl_block.items.extend(new_impl_items);
151
152    let other_items = outputs.iter().map(|(_, output)| &output.other_items);
153
154    Ok(quote! {
155        // Keep the original impl block at the top. It seems like rust-analyzer
156        // does better job of highlighting syntax when it is here. Assuming
157        // this is because rust-analyzer prefers the first occurrence of the
158        // span when highlighting.
159        //
160        // See this issue for details: https://github.com/rust-lang/rust-analyzer/issues/18438
161        #norm_selfful_impl_block
162
163        #(#other_items)*
164    })
165}
166
167fn conv_impl_item_fn_into_fn_item(func: syn::ImplItemFn) -> Result<syn::ItemFn> {
168    let syn::ImplItemFn {
169        attrs,
170        vis,
171        defaultness,
172        sig,
173        block,
174    } = func;
175
176    if let Some(defaultness) = &defaultness {
177        bail!(defaultness, "Default functions are not supported yet");
178    }
179
180    Ok(syn::ItemFn {
181        attrs,
182        vis,
183        sig,
184        block: Box::new(block),
185    })
186}
187
188fn no_builder_attrs_error(other_items: &[syn::ImplItem]) -> Error {
189    let builder_like_err = other_items.iter().find_map(|item| {
190        let item = match item {
191            syn::ImplItem::Fn(fn_item) => fn_item,
192            _ => return None,
193        };
194
195        let builder_like = item
196            .attrs
197            .iter()
198            .find(|attr| attr.path().ends_with_segment("builder"))?;
199
200        let builder_like_str = darling::util::path_to_string(builder_like.path());
201        let builder_like_prefix = builder_like_str
202            .strip_suffix("builder")
203            .unwrap_or(&builder_like_str);
204
205        Some(err!(
206            &builder_like.path(),
207            "#[bon] macro found no #[builder] attributes in the impl block, but \
208            it looks like this attribute was meant for #[bon]; note that #[bon] \
209            expects a bare #[builder] attribute without the `{builder_like_prefix}` \
210            prefix; #[builder] acts as a simple config attribute for the active \
211            #[bon] attribute in impl blocks; more info on inert vs active attributes: \
212            https://doc.rust-lang.org/reference/attributes.html#active-and-inert-attributes"
213        ))
214    });
215
216    if let Some(err) = builder_like_err {
217        return err;
218    }
219
220    err!(
221        &Span::call_site(),
222        "there are no #[builder] functions in the impl block, so there is no \
223        need for a #[bon] attribute here"
224    )
225}