hooks_macro_core/
detect.rs

1use proc_macro2::{Span, TokenStream};
2use syn::{ext::IdentExt, parse::Parse, spanned::Spanned};
3
4pub mod is_hook {
5
6    pub fn ident(ident: &syn::Ident) -> bool {
7        ident.to_string().starts_with("use_")
8    }
9
10    pub fn expr_path(path: &syn::ExprPath) -> bool {
11        if let Some(last) = path.path.segments.last() {
12            ident(&last.ident)
13        } else {
14            false
15        }
16    }
17
18    pub fn expr_call_with_path(path: &syn::ExprPath) -> bool {
19        expr_path(path)
20    }
21
22    pub fn expr_method_call(expr: &syn::ExprMethodCall) -> bool {
23        ident(&expr.method)
24    }
25
26    pub fn expr_macro(expr: &syn::ExprMacro) -> bool {
27        expr.mac
28            .path
29            .get_ident()
30            .map_or(false, |ident| ident == "h")
31    }
32}
33
34/// attr is `#[not_hook]` or `#![not_hook]`
35pub fn attr_is_not_hook(attr: &syn::Attribute) -> bool {
36    match &attr.meta {
37        syn::Meta::Path(path) => path.get_ident().map_or(false, |ident| ident == "not_hook"),
38        _ => false,
39    }
40}
41
42struct ExprOfStmtMut<'a> {
43    expr: &'a mut syn::Expr,
44    stmt_attrs: Option<&'a mut Vec<syn::Attribute>>,
45}
46
47impl<'a> ExprOfStmtMut<'a> {
48    fn try_from(stmt: &'a mut syn::Stmt) -> Option<Self> {
49        match stmt {
50            syn::Stmt::Local(local) => {
51                if let Some(syn::LocalInit {
52                    eq_token: _,
53                    expr,
54                    diverge: _, // diverge is not top level
55                }) = &mut local.init
56                {
57                    Some(Self {
58                        expr,
59                        stmt_attrs: Some(&mut local.attrs),
60                    })
61                } else {
62                    None
63                }
64            }
65            syn::Stmt::Item(_) => {
66                // Items are untouched
67                None
68            }
69            syn::Stmt::Expr(expr, _) => Some(Self {
70                expr,
71                stmt_attrs: None,
72            }),
73            // Macros are untouched because it might not expand to expr
74            syn::Stmt::Macro(_) => None,
75        }
76    }
77}
78
79pub struct DetectedHooks {
80    pub hooks: Vec<crate::DetectedHook>,
81    pub not_hook_attrs: Vec<syn::Attribute>,
82}
83
84pub fn detect_hooks<'a>(
85    stmts: impl Iterator<Item = &'a mut syn::Stmt>,
86    hooks_core_path: &syn::Path,
87) -> DetectedHooks {
88    let mut used_hooks = vec![];
89
90    let mut mutate = MutateHookExpr::new(|expr| {
91        let mut expr_attrs = vec![];
92        let mut h_ident = None;
93        let mut paren_token = None;
94        let mut hook_id = None;
95
96        if let syn::Expr::Macro(m) = expr {
97            expr_attrs = std::mem::take(&mut m.attrs);
98
99            h_ident = Some(m.mac.path.get_ident().unwrap().clone());
100
101            paren_token = Some(match &mut m.mac.delimiter {
102                syn::MacroDelimiter::Paren(p) => *p,
103                syn::MacroDelimiter::Brace(d) => syn::token::Paren(d.span),
104                syn::MacroDelimiter::Bracket(d) => syn::token::Paren(d.span),
105            });
106
107            let HMacroContent {
108                explicit_hook_id,
109                expr: actual_expr,
110            } = syn::parse2(std::mem::take(&mut m.mac.tokens)).unwrap();
111
112            hook_id = explicit_hook_id.map(|h| h.0);
113
114            *expr = syn::Expr::Verbatim(actual_expr);
115        }
116
117        let span = Span::call_site().located_at(expr.span());
118
119        let actual_expr = std::mem::replace(
120            expr,
121            syn::Expr::Call(syn::ExprCall {
122                attrs: expr_attrs,
123                func: Box::new(syn::Expr::Path(syn::ExprPath {
124                    attrs: vec![],
125                    qself: None,
126                    path: {
127                        let mut p = hooks_core_path.clone();
128                        p.segments
129                            .push(syn::Ident::new("UpdateHookUninitialized", span).into());
130                        p.segments
131                            .push(h_ident.unwrap_or_else(|| syn::Ident::new("h", span)).into());
132                        p
133                    },
134                })),
135                paren_token: paren_token.unwrap_or_default(),
136                args: Default::default(),
137            }),
138        );
139
140        let hook_id = hook_id.unwrap_or_else(|| {
141            let idx = used_hooks.len();
142            syn::Ident::new(&format!("__hooks_hook_{idx}"), span)
143        });
144
145        if let syn::Expr::Call(syn::ExprCall { args, .. }) = expr {
146            args.extend([
147                actual_expr,
148                syn::Expr::Path(syn::ExprPath {
149                    attrs: vec![],
150                    qself: None,
151                    path: hook_id.clone().into(),
152                }),
153            ]);
154        } else {
155            unreachable!()
156        };
157
158        used_hooks.push(crate::DetectedHook { ident: hook_id })
159    });
160
161    for stmt in stmts {
162        if let Some(ExprOfStmtMut { expr, stmt_attrs }) = ExprOfStmtMut::try_from(stmt) {
163            if stmt_attrs.map_or(true, |attrs| mutate.not_hook_attrs.might_be_hook(attrs)) {
164                mutate.mutate_if_expr_is_hook(expr);
165            }
166        }
167    }
168
169    DetectedHooks {
170        not_hook_attrs: mutate.unwrap_not_hook_attrs(),
171        hooks: used_hooks,
172    }
173}
174
175/// tokens inside `h![...]`
176struct HMacroContent {
177    explicit_hook_id: Option<(syn::Ident, syn::Token![=])>,
178    expr: TokenStream,
179}
180
181impl Parse for HMacroContent {
182    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
183        Ok(Self {
184            explicit_hook_id: if input.peek(syn::Ident::peek_any) && input.peek2(syn::Token![=]) {
185                Some((input.parse()?, input.parse()?))
186            } else {
187                None
188            },
189            expr: input.parse()?,
190        })
191    }
192}
193
194/// attribute must be `#[not_hook]` and `#![not_hook]`
195struct NotHookAttrs(Vec<syn::Attribute>);
196
197impl NotHookAttrs {
198    /// remove `#[not_hook]` and `#![not_hook]` from attrs
199    /// append removed attributes to `append_removed_to`.
200    /// return true if no attributes are removed (which means this might be a hook)
201    pub fn might_be_hook(&mut self, attrs: &mut Vec<syn::Attribute>) -> bool {
202        let mut nothing_is_removed = true;
203        attrs.retain_mut(|attr| {
204            if attr_is_not_hook(attr) {
205                let src = syn::Attribute {
206                    pound_token: attr.pound_token,
207                    style: attr.style,
208                    bracket_token: attr.bracket_token,
209                    meta: syn::Meta::Path(syn::Path {
210                        leading_colon: None,
211                        segments: Default::default(),
212                    }),
213                };
214                nothing_is_removed = false;
215                self.0.push(std::mem::replace(attr, src));
216                false
217            } else {
218                true
219            }
220        });
221
222        nothing_is_removed
223    }
224}
225
226pub struct MutateHookExpr<F: FnMut(&mut syn::Expr)> {
227    mutate_hook_expr: F,
228    not_hook_attrs: NotHookAttrs,
229}
230
231impl<F: FnMut(&mut syn::Expr)> MutateHookExpr<F> {
232    pub fn new(mutate_hook_expr: F) -> Self {
233        Self {
234            mutate_hook_expr,
235            not_hook_attrs: NotHookAttrs(vec![]),
236        }
237    }
238
239    pub fn mutate_if_expr_is_hook(&mut self, expr: &mut syn::Expr) {
240        macro_rules! process_inner_expressions {
241        ($e:ident . $field:ident) => {
242            process_inner_expressions! { $e { $field } }
243        };
244        ($e:ident { $($field:ident),+ $(,)? }) => {
245            if self.not_hook_attrs.might_be_hook(&mut $e.attrs) {
246                $(
247                    self.mutate_if_expr_is_hook(&mut $e.$field);
248                )+
249            }
250        };
251    }
252
253        match expr {
254            syn::Expr::Array(array) => {
255                if self.not_hook_attrs.might_be_hook(&mut array.attrs) {
256                    for elem in array.elems.iter_mut() {
257                        self.mutate_if_expr_is_hook(elem);
258                    }
259                }
260            }
261            syn::Expr::Assign(e) => process_inner_expressions!(e { left, right }),
262            syn::Expr::Async(_) => {
263                // `async {}` is untouched
264            }
265            syn::Expr::Await(e) => process_inner_expressions!(e.base),
266            syn::Expr::Binary(e) => process_inner_expressions!(e { left, right }),
267            syn::Expr::Block(_) => {
268                // `{}` is untouched because it is not top level
269            }
270            syn::Expr::Break(_) => {
271                // `break` is untouched
272                // because there cannot be any break in top level.
273            }
274            syn::Expr::Call(c) => {
275                if self.not_hook_attrs.might_be_hook(&mut c.attrs) {
276                    for arg in c.args.iter_mut() {
277                        self.mutate_if_expr_is_hook(arg);
278                    }
279
280                    if let syn::Expr::Path(path) = &*c.func {
281                        if is_hook::expr_call_with_path(path) {
282                            (self.mutate_hook_expr)(expr);
283                        }
284                    } else {
285                        self.mutate_if_expr_is_hook(&mut c.func);
286                    }
287                }
288            }
289            syn::Expr::Cast(e) => process_inner_expressions!(e.expr),
290            syn::Expr::Closure(_) => {
291                // `|| {}` is untouched
292                // because exprs in the body are not top level
293            }
294            syn::Expr::Continue(_) => {
295                // `continue` is untouched
296                // with the same reason as `break`
297            }
298            syn::Expr::Field(e) => process_inner_expressions!(e.base),
299            syn::Expr::ForLoop(e) => process_inner_expressions!(e.expr),
300            syn::Expr::Group(e) => process_inner_expressions!(e.expr),
301            syn::Expr::If(e) => process_inner_expressions!(e.cond),
302            syn::Expr::Index(e) => process_inner_expressions!(e { expr, index }),
303            syn::Expr::Let(e) => process_inner_expressions!(e.expr),
304            syn::Expr::Lit(_) => {
305                // literals are untouched
306                // because there is no hook
307            }
308            syn::Expr::Loop(_) => {
309                // `loop {}` is untouched
310                // because there are no exprs in top level
311            }
312            syn::Expr::Macro(m) => {
313                if self.not_hook_attrs.might_be_hook(&mut m.attrs) && is_hook::expr_macro(m) {
314                    (self.mutate_hook_expr)(expr);
315                }
316            }
317            syn::Expr::Match(e) => process_inner_expressions!(e.expr),
318            syn::Expr::MethodCall(m) => {
319                if self.not_hook_attrs.might_be_hook(&mut m.attrs) {
320                    for arg in m.args.iter_mut() {
321                        self.mutate_if_expr_is_hook(arg);
322                    }
323                    self.mutate_if_expr_is_hook(&mut m.receiver);
324
325                    if is_hook::ident(&m.method) {
326                        (self.mutate_hook_expr)(expr);
327                    }
328                }
329            }
330            syn::Expr::Paren(e) => process_inner_expressions!(e.expr),
331            syn::Expr::Path(_) => {
332                // `std::mem::replace` is untouched
333                // because there is no function call
334            }
335            syn::Expr::Range(r) => {
336                if (self.not_hook_attrs).might_be_hook(&mut r.attrs) {
337                    if let Some(e) = &mut r.start {
338                        self.mutate_if_expr_is_hook(e);
339                    }
340                    if let Some(e) = &mut r.end {
341                        self.mutate_if_expr_is_hook(e);
342                    }
343                }
344            }
345            syn::Expr::Reference(e) => process_inner_expressions!(e.expr),
346            syn::Expr::Repeat(_) => {
347                // `[expr; N]` is untouched
348                // because the expr is not considered top level
349            }
350            syn::Expr::Return(e) => {
351                if self.not_hook_attrs.might_be_hook(&mut e.attrs) {
352                    if let Some(expr) = &mut e.expr {
353                        self.mutate_if_expr_is_hook(expr);
354                    }
355                }
356            }
357            syn::Expr::Struct(s) => {
358                if self.not_hook_attrs.might_be_hook(&mut s.attrs) {
359                    for field in s.fields.iter_mut() {
360                        process_inner_expressions!(field.expr);
361                    }
362                    if let Some(e) = &mut s.rest {
363                        self.mutate_if_expr_is_hook(e);
364                    }
365                }
366            }
367            syn::Expr::Try(e) => process_inner_expressions!(e.expr),
368            syn::Expr::TryBlock(_) => {
369                // `try {}` is untouched
370                // because there are no exprs in top level
371            }
372            syn::Expr::Tuple(t) => {
373                if self.not_hook_attrs.might_be_hook(&mut t.attrs) {
374                    for elem in t.elems.iter_mut() {
375                        self.mutate_if_expr_is_hook(elem);
376                    }
377                }
378            }
379            syn::Expr::Unary(e) => process_inner_expressions!(e.expr),
380            syn::Expr::Unsafe(_) => {
381                // `unsafe {}` is untouched
382                // because there are no exprs in top level
383            }
384            syn::Expr::Verbatim(_) => {
385                // untouched because not interpreted by Syn
386            }
387            syn::Expr::While(e) => process_inner_expressions!(e.cond),
388            syn::Expr::Yield(_) => {
389                // `yield` is untouched
390                // with the same reason as `break`
391            }
392            syn::Expr::Const(_) => {}
393            syn::Expr::Infer(_) => {}
394            _ => {
395                // unknown exprs are untouched
396                // Adding new variants or changing behavior of current variants
397                // would be a BREAKING CHANGE
398            }
399        }
400    }
401
402    pub fn unwrap_not_hook_attrs(self) -> Vec<syn::Attribute> {
403        self.not_hook_attrs.0
404    }
405}