bon_macros/util/
ide.rs

1// We must make sure this code never panics. It must be able to handle all possible inputs.
2#![deny(
3    clippy::unwrap_used,
4    clippy::expect_used,
5    clippy::panic,
6    clippy::unreachable,
7    clippy::unimplemented,
8    clippy::todo,
9    clippy::exit
10)]
11
12use crate::util::prelude::*;
13use proc_macro2::TokenTree;
14use syn::parse::{Parse, ParseStream, Parser};
15use syn::{token, Token};
16
17pub(crate) fn parse_comma_separated_meta(input: ParseStream<'_>) -> syn::Result<Vec<Meta>> {
18    let mut output = vec![];
19
20    while !input.is_empty() {
21        let value = input.parse::<Meta>()?;
22
23        output.push(value);
24
25        while !input.is_empty() {
26            if input.peek(Token![,]) {
27                input.parse::<Token![,]>()?;
28                break;
29            }
30
31            // Skip the invalid token (comma is expected only).
32            input.parse::<TokenTree>()?;
33        }
34    }
35
36    Ok(output)
37}
38
39#[derive(Clone, Debug)]
40pub(crate) enum Meta {
41    None,
42    Path(syn::Path),
43    List(MetaList),
44    NameValue(MetaNameValue),
45}
46
47impl Parse for Meta {
48    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
49        let path = loop {
50            let path = input.call(syn::Path::parse_mod_style).ok();
51
52            if let Some(path) = path {
53                break path;
54            }
55
56            if input.parse::<TokenTree>().is_err() {
57                return Ok(Self::None);
58            }
59        };
60
61        let meta = if input.peek(token::Paren) {
62            let content;
63            syn::parenthesized!(content in input);
64
65            Self::List(MetaList {
66                path,
67                tokens: content.parse()?,
68            })
69        } else if input.peek(token::Bracket) {
70            let content;
71            syn::bracketed!(content in input);
72
73            Self::List(MetaList {
74                path,
75                tokens: content.parse()?,
76            })
77        } else if input.peek(token::Brace) {
78            let content;
79            syn::braced!(content in input);
80
81            Self::List(MetaList {
82                path,
83                tokens: content.parse()?,
84            })
85        } else if input.peek(Token![=]) {
86            Self::NameValue(MetaNameValue {
87                path,
88                eq_token: input.parse()?,
89                value: input.parse().ok(),
90            })
91        } else {
92            Self::Path(path)
93        };
94
95        Ok(meta)
96    }
97}
98
99#[derive(Clone, Debug)]
100pub(crate) struct MetaList {
101    pub(crate) path: syn::Path,
102    pub(crate) tokens: TokenStream,
103}
104
105#[derive(Clone, Debug)]
106pub(crate) struct MetaNameValue {
107    pub(crate) path: syn::Path,
108
109    #[allow(dead_code)]
110    pub(crate) eq_token: syn::Token![=],
111
112    #[allow(dead_code)]
113    pub(crate) value: Option<syn::Expr>,
114}
115
116fn paths_from_meta(meta: Vec<Meta>) -> Vec<syn::Path> {
117    meta.into_iter()
118        .filter_map(|meta| match meta {
119            Meta::Path(path) => Some(path),
120            Meta::NameValue(meta) => Some(meta.path),
121            Meta::List(meta) => Some(meta.path),
122            Meta::None => None,
123        })
124        .collect()
125}
126
127/// This is a highly experimental function that attempts to generate code that
128/// can be used by IDEs to provide hints and completions for attributes in macros.
129///
130/// The idea is that we parse a potentially incomplete syntax of the macro invocation
131/// and then we generate a set of modules that contain `use` statements with the
132/// identifiers passed to the macro.
133///
134/// By placing these input identifiers in the right places inside of `use` statements
135/// we can hint the IDEs to provide completions for the attributes based on what's
136/// available in the module the use statement references.
137pub(crate) fn generate_completion_triggers(meta: Vec<Meta>) -> TokenStream {
138    let bon = meta
139        .iter()
140        .find_map(|meta| match meta {
141            Meta::NameValue(meta) if meta.path.is_ident("crate") => Some(meta.value.as_ref()),
142            _ => None,
143        })
144        .flatten()
145        .and_then(|expr| Some(expr.require_path_mod_style().ok()?.clone()))
146        .unwrap_or_else(|| syn::parse_quote!(::bon));
147
148    let completions = CompletionsSchema::with_children(
149        "builder_top_level",
150        vec![
151            CompletionsSchema::leaf("builder_type"),
152            CompletionsSchema::leaf("start_fn"),
153            CompletionsSchema::leaf("finish_fn"),
154            CompletionsSchema::leaf("state_mod"),
155            CompletionsSchema::leaf("on").set_custom_filter(|meta| {
156                if let Some(first) = meta.first() {
157                    if let Meta::Path(path) = first {
158                        if path.is_ident("into")
159                            || path.is_ident("required")
160                            || path.is_ident("overwritable")
161                        {
162                            return;
163                        }
164                    }
165
166                    meta.remove(0);
167                }
168            }),
169            CompletionsSchema::leaf("derive"),
170        ],
171    );
172
173    let completion_triggers = completions.generate_completion_triggers(&bon, meta, &[]);
174
175    quote! {
176        // The special `rust_analyzer` CFG is enabled only when Rust Analyzer is
177        // running its code analysis. This allows us to provide code that is
178        // useful only for Rust Analyzer for it to provide hints and completions.
179        #[allow(unexpected_cfgs)]
180        const _: () = {
181            #[cfg(rust_analyzer)]
182            {
183                #completion_triggers
184            }
185        };
186    }
187}
188
189struct CompletionsSchema {
190    key: &'static str,
191    children: Vec<CompletionsSchema>,
192    custom_filter: Option<fn(&mut Vec<Meta>)>,
193}
194
195impl CompletionsSchema {
196    fn leaf(key: &'static str) -> Self {
197        Self {
198            key,
199            children: vec![],
200            custom_filter: None,
201        }
202    }
203
204    fn with_children(key: &'static str, children: Vec<Self>) -> Self {
205        Self {
206            key,
207            children,
208            custom_filter: None,
209        }
210    }
211
212    fn set_custom_filter(mut self, custom_filter: fn(&mut Vec<Meta>)) -> Self {
213        self.custom_filter = Some(custom_filter);
214        self
215    }
216
217    fn generate_completion_triggers(
218        &self,
219        bon: &syn::Path,
220        mut meta: Vec<Meta>,
221        module_prefix: &[&syn::Ident],
222    ) -> TokenStream {
223        if let Some(custom_filter) = self.custom_filter {
224            custom_filter(&mut meta);
225        };
226
227        let module_suffix = syn::Ident::new(self.key, Span::call_site());
228        let module_name = module_prefix
229            .iter()
230            .copied()
231            .chain([&module_suffix])
232            .collect::<Vec<_>>();
233
234        let child_completion_triggers = self
235            .children
236            .iter()
237            .map(|child| {
238                let child_metas = meta
239                    .iter()
240                    .filter_map(|meta| {
241                        let meta = match meta {
242                            Meta::List(meta) => meta,
243                            _ => return None,
244                        };
245
246                        if !meta.path.is_ident(&child.key) {
247                            return None;
248                        }
249
250                        parse_comma_separated_meta.parse2(meta.tokens.clone()).ok()
251                    })
252                    .concat();
253
254                child.generate_completion_triggers(bon, child_metas, &module_name)
255            })
256            .collect::<Vec<_>>();
257
258        let paths = paths_from_meta(meta);
259        let module_name_snake_case =
260            syn::Ident::new(&module_name.iter().join("_"), Span::call_site());
261
262        quote! {
263            mod #module_name_snake_case {
264                // We separately import everything from the IDE hints module
265                // and then put the `paths` in the `use self::{ ... }` statement
266                // to avoid Rust Analyzer from providing completions for the
267                // `self` keyword in the `use` statement. It works because
268                // `use self::self` is not a valid syntax.
269                use #bon::__::ide #(::#module_name)* ::*;
270                use self::{ #( #paths as _, )* };
271            }
272
273            #(#child_completion_triggers)*
274        }
275    }
276}