swc_plugin_inferno/refresh/
hook.rs

1use std::{fmt::Write, mem};
2
3use base64::prelude::{Engine, BASE64_STANDARD};
4use sha1::{Digest, Sha1};
5use swc_core::common::util::take::Take;
6use swc_core::common::{SourceMap, SourceMapper, Spanned, SyntaxContext, DUMMY_SP};
7use swc_core::ecma::ast::*;
8use swc_core::ecma::utils::{private_ident, quote_ident, ExprFactory};
9use swc_core::ecma::visit::{
10    noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
11};
12
13use super::util::{is_builtin_hook, make_call_expr, make_call_stmt};
14use crate::RefreshOptions;
15
16// function that use hooks
17struct HookSig {
18    handle: Ident,
19    // need to add an extra register, or already inlined
20    hooks: Vec<Hook>,
21}
22
23impl HookSig {
24    fn new(hooks: Vec<Hook>) -> Self {
25        HookSig {
26            handle: private_ident!("_s"),
27            hooks,
28        }
29    }
30}
31
32struct Hook {
33    callee: HookCall,
34    key: String,
35}
36
37// we only consider two kinds of callee as hook call
38#[allow(clippy::large_enum_variant)]
39enum HookCall {
40    Ident(Ident),
41    Member(Expr, IdentName), // for obj and prop
42}
43pub struct HookRegister<'a> {
44    pub options: &'a RefreshOptions,
45    pub ident: Vec<Ident>,
46    pub extra_stmt: Vec<Stmt>,
47    pub current_scope: Vec<SyntaxContext>,
48    pub cm: &'a SourceMap,
49    pub should_reset: bool,
50}
51
52impl<'a> HookRegister<'a> {
53    pub fn gen_hook_handle(&mut self) -> Stmt {
54        VarDecl {
55            span: DUMMY_SP,
56            kind: VarDeclKind::Var,
57            decls: self
58                .ident
59                .take()
60                .into_iter()
61                .map(|id| VarDeclarator {
62                    span: DUMMY_SP,
63                    name: id.into(),
64                    init: Some(Box::new(make_call_expr(
65                        quote_ident!(self.options.refresh_sig.clone()).into(),
66                    ))),
67                    definite: false,
68                })
69                .collect(),
70            declare: false,
71            ..Default::default()
72        }
73        .into()
74    }
75
76    // The second call is around the function itself. This is used to associate a
77    // type with a signature.
78    // Unlike with $RefreshReg$, this needs to work for nested declarations too.
79    fn wrap_with_register(&self, handle: Ident, func: Expr, hooks: Vec<Hook>) -> Expr {
80        let mut args = vec![func.as_arg()];
81        let mut sign = Vec::new();
82        let mut custom_hook = Vec::new();
83
84        for hook in hooks {
85            let name = match &hook.callee {
86                HookCall::Ident(i) => i.clone(),
87                HookCall::Member(_, i) => i.clone().into(),
88            };
89            sign.push(format!("{}{{{}}}", name.sym, hook.key));
90            match &hook.callee {
91                HookCall::Ident(ident) if !is_builtin_hook(&ident.sym) => {
92                    custom_hook.push(hook.callee);
93                }
94                HookCall::Member(Expr::Ident(obj_ident), prop) if !is_builtin_hook(&prop.sym) => {
95                    if obj_ident.sym.as_ref() != "React" {
96                        custom_hook.push(hook.callee);
97                    }
98                }
99                _ => (),
100            };
101        }
102
103        let sign = sign.join("\n");
104        let sign = if self.options.emit_full_signatures {
105            sign
106        } else {
107            let mut hasher = Sha1::new();
108            hasher.update(sign);
109            BASE64_STANDARD.encode(hasher.finalize())
110        };
111
112        args.push(
113            Lit::Str(Str {
114                span: DUMMY_SP,
115                raw: None,
116                value: sign.into(),
117            })
118            .as_arg(),
119        );
120
121        let mut should_reset = self.should_reset;
122
123        let mut custom_hook_in_scope = Vec::new();
124
125        for hook in custom_hook {
126            let ident = match &hook {
127                HookCall::Ident(ident) => Some(ident),
128                HookCall::Member(Expr::Ident(ident), _) => Some(ident),
129                _ => None,
130            };
131            if !ident
132                .map(|id| self.current_scope.contains(&id.ctxt))
133                .unwrap_or(false)
134            {
135                // We don't have anything to put in the array because Hook is out of scope.
136                // Since it could potentially have been edited, remount the component.
137                should_reset = true;
138            } else {
139                custom_hook_in_scope.push(hook);
140            }
141        }
142
143        if should_reset || !custom_hook_in_scope.is_empty() {
144            args.push(should_reset.as_arg());
145        }
146
147        if !custom_hook_in_scope.is_empty() {
148            let elems = custom_hook_in_scope
149                .into_iter()
150                .map(|hook| {
151                    Some(
152                        match hook {
153                            HookCall::Ident(ident) => Expr::from(ident),
154                            HookCall::Member(obj, prop) => MemberExpr {
155                                span: DUMMY_SP,
156                                obj: Box::new(obj),
157                                prop: MemberProp::Ident(prop),
158                            }
159                            .into(),
160                        }
161                        .as_arg(),
162                    )
163                })
164                .collect();
165            args.push(
166                Function {
167                    is_generator: false,
168                    is_async: false,
169                    params: Vec::new(),
170                    decorators: Vec::new(),
171                    span: DUMMY_SP,
172                    body: Some(BlockStmt {
173                        span: DUMMY_SP,
174                        stmts: vec![Stmt::Return(ReturnStmt {
175                            span: DUMMY_SP,
176                            arg: Some(Box::new(Expr::Array(ArrayLit {
177                                span: DUMMY_SP,
178                                elems,
179                            }))),
180                        })],
181                        ..Default::default()
182                    }),
183                    ..Default::default()
184                }
185                .as_arg(),
186            );
187        }
188
189        CallExpr {
190            span: DUMMY_SP,
191            callee: handle.as_callee(),
192            args,
193            ..Default::default()
194        }
195        .into()
196    }
197
198    fn gen_hook_register_stmt(&mut self, ident: Ident, sig: HookSig) {
199        self.ident.push(sig.handle.clone());
200        self.extra_stmt.push(
201            ExprStmt {
202                span: DUMMY_SP,
203                expr: Box::new(self.wrap_with_register(sig.handle, ident.into(), sig.hooks)),
204            }
205            .into(),
206        )
207    }
208}
209
210impl<'a> VisitMut for HookRegister<'a> {
211    noop_visit_mut_type!();
212
213    fn visit_mut_block_stmt(&mut self, b: &mut BlockStmt) {
214        let old_ident = self.ident.take();
215        let old_stmts = self.extra_stmt.take();
216
217        self.current_scope.push(b.ctxt);
218
219        let stmt_count = b.stmts.len();
220        let stmts = mem::replace(&mut b.stmts, Vec::with_capacity(stmt_count));
221
222        for mut stmt in stmts {
223            stmt.visit_mut_children_with(self);
224
225            b.stmts.push(stmt);
226            b.stmts.append(&mut self.extra_stmt);
227        }
228
229        if !self.ident.is_empty() {
230            b.stmts.insert(0, self.gen_hook_handle())
231        }
232
233        self.current_scope.pop();
234        self.ident = old_ident;
235        self.extra_stmt = old_stmts;
236    }
237
238    fn visit_mut_expr(&mut self, e: &mut Expr) {
239        e.visit_mut_children_with(self);
240
241        match e {
242            Expr::Fn(FnExpr { function: f, .. }) if f.body.is_some() => {
243                let sig = collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm);
244
245                if let Some(HookSig { handle, hooks }) = sig {
246                    self.ident.push(handle.clone());
247                    *e = self.wrap_with_register(handle, e.take(), hooks);
248                }
249            }
250            Expr::Arrow(ArrowExpr { body, .. }) => {
251                let sig = collect_hooks_arrow(body, self.cm);
252
253                if let Some(HookSig { handle, hooks }) = sig {
254                    self.ident.push(handle.clone());
255                    *e = self.wrap_with_register(handle, e.take(), hooks);
256                }
257            }
258            _ => (),
259        }
260    }
261
262    fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
263        // we don't want visit_mut_expr to mess up with function name inference
264        // so intercept it here
265
266        for decl in n.decls.iter_mut() {
267            if let VarDeclarator {
268                // it doesn't quite make sense for other Pat to appear here
269                name: Pat::Ident(id),
270                init: Some(init),
271                ..
272            } = decl
273            {
274                match init.as_mut() {
275                    Expr::Fn(FnExpr { function: f, .. }) if f.body.is_some() => {
276                        f.body.visit_mut_with(self);
277                        if let Some(sig) =
278                            collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm)
279                        {
280                            self.gen_hook_register_stmt(Ident::from(&*id), sig);
281                        }
282                    }
283                    Expr::Arrow(ArrowExpr { body, .. }) => {
284                        body.visit_mut_with(self);
285                        if let Some(sig) = collect_hooks_arrow(body, self.cm) {
286                            self.gen_hook_register_stmt(Ident::from(&*id), sig);
287                        }
288                    }
289                    _ => self.visit_mut_expr(init),
290                }
291            } else {
292                decl.visit_mut_children_with(self)
293            }
294        }
295    }
296
297    fn visit_mut_default_decl(&mut self, d: &mut DefaultDecl) {
298        d.visit_mut_children_with(self);
299
300        // only when expr has ident
301        match d {
302            DefaultDecl::Fn(FnExpr {
303                ident: Some(ident),
304                function: f,
305            }) if f.body.is_some() => {
306                if let Some(sig) = collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm) {
307                    self.gen_hook_register_stmt(ident.clone(), sig);
308                }
309            }
310            _ => {}
311        }
312    }
313
314    fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
315        f.visit_mut_children_with(self);
316
317        if let Some(body) = &mut f.function.body {
318            if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
319                self.gen_hook_register_stmt(f.ident.clone(), sig);
320            }
321        }
322    }
323}
324
325fn collect_hooks(stmts: &mut Vec<Stmt>, cm: &SourceMap) -> Option<HookSig> {
326    let mut hook = HookCollector {
327        state: Vec::new(),
328        cm,
329    };
330
331    stmts.visit_with(&mut hook);
332
333    if !hook.state.is_empty() {
334        let sig = HookSig::new(hook.state);
335        stmts.insert(0, make_call_stmt(sig.handle.clone()));
336
337        Some(sig)
338    } else {
339        None
340    }
341}
342
343fn collect_hooks_arrow(body: &mut BlockStmtOrExpr, cm: &SourceMap) -> Option<HookSig> {
344    match body {
345        BlockStmtOrExpr::BlockStmt(block) => collect_hooks(&mut block.stmts, cm),
346        BlockStmtOrExpr::Expr(expr) => {
347            let mut hook = HookCollector {
348                state: Vec::new(),
349                cm,
350            };
351
352            expr.visit_with(&mut hook);
353
354            if !hook.state.is_empty() {
355                let sig = HookSig::new(hook.state);
356                *body = BlockStmtOrExpr::BlockStmt(BlockStmt {
357                    span: expr.span(),
358                    stmts: vec![
359                        make_call_stmt(sig.handle.clone()),
360                        Stmt::Return(ReturnStmt {
361                            span: expr.span(),
362                            arg: Some(Box::new(expr.as_mut().take())),
363                        }),
364                    ],
365                    ..Default::default()
366                });
367                Some(sig)
368            } else {
369                None
370            }
371        }
372    }
373}
374
375struct HookCollector<'a> {
376    state: Vec<Hook>,
377    cm: &'a SourceMap,
378}
379
380fn is_hook_like(s: &str) -> bool {
381    if let Some(s) = s.strip_prefix("use") {
382        s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
383    } else {
384        false
385    }
386}
387
388impl<'a> HookCollector<'a> {
389    fn get_hook_from_call_expr(&self, expr: &CallExpr, lhs: Option<&Pat>) -> Option<Hook> {
390        let callee = if let Callee::Expr(callee) = &expr.callee {
391            Some(callee.as_ref())
392        } else {
393            None
394        }?;
395        let mut hook_call = None;
396        let ident = match callee {
397            Expr::Ident(ident) => {
398                hook_call = Some(HookCall::Ident(ident.clone()));
399                Some(&ident.sym)
400            }
401            // hook cannot be used in class, so we're fine without SuperProp
402            Expr::Member(MemberExpr {
403                obj,
404                prop: MemberProp::Ident(ident),
405                ..
406            }) => {
407                hook_call = Some(HookCall::Member(*obj.clone(), ident.clone()));
408                Some(&ident.sym)
409            }
410            _ => None,
411        }?;
412        let name = if is_hook_like(ident) {
413            Some(ident)
414        } else {
415            None
416        }?;
417        let mut key = if let Some(name) = lhs {
418            self.cm.span_to_snippet(name.span()).unwrap_or_default()
419        } else {
420            String::new()
421        };
422        // Some built-in Hooks reset on edits to arguments.
423        if *name == "useState" && !expr.args.is_empty() {
424            // useState first argument is initial state.
425            let _ = write!(
426                key,
427                "({})",
428                self.cm
429                    .span_to_snippet(expr.args[0].span())
430                    .unwrap_or_default()
431            );
432        } else if name == "useReducer" && expr.args.len() > 1 {
433            // useReducer second argument is initial state.
434            let _ = write!(
435                key,
436                "({})",
437                self.cm
438                    .span_to_snippet(expr.args[1].span())
439                    .unwrap_or_default()
440            );
441        }
442
443        let callee = hook_call?;
444        Some(Hook { callee, key })
445    }
446
447    fn get_hook_from_expr(&self, expr: &Expr, lhs: Option<&Pat>) -> Option<Hook> {
448        if let Expr::Call(call) = expr {
449            self.get_hook_from_call_expr(call, lhs)
450        } else {
451            None
452        }
453    }
454}
455
456impl<'a> Visit for HookCollector<'a> {
457    noop_visit_type!();
458
459    fn visit_block_stmt_or_expr(&mut self, _: &BlockStmtOrExpr) {}
460
461    fn visit_block_stmt(&mut self, _: &BlockStmt) {}
462
463    fn visit_expr(&mut self, expr: &Expr) {
464        expr.visit_children_with(self);
465
466        if let Expr::Call(call) = expr {
467            if let Some(hook) = self.get_hook_from_call_expr(call, None) {
468                self.state.push(hook)
469            }
470        }
471    }
472
473    fn visit_stmt(&mut self, stmt: &Stmt) {
474        match stmt {
475            Stmt::Expr(ExprStmt { expr, .. }) => {
476                if let Some(hook) = self.get_hook_from_expr(expr, None) {
477                    self.state.push(hook)
478                } else {
479                    stmt.visit_children_with(self)
480                }
481            }
482            Stmt::Decl(Decl::Var(var_decl)) => {
483                for decl in &var_decl.decls {
484                    if let Some(init) = &decl.init {
485                        if let Some(hook) = self.get_hook_from_expr(init, Some(&decl.name)) {
486                            self.state.push(hook)
487                        } else {
488                            stmt.visit_children_with(self)
489                        }
490                    } else {
491                        stmt.visit_children_with(self)
492                    }
493                }
494            }
495            Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => {
496                if let Some(hook) = self.get_hook_from_expr(arg.as_ref(), None) {
497                    self.state.push(hook)
498                } else {
499                    stmt.visit_children_with(self)
500                }
501            }
502            _ => stmt.visit_children_with(self),
503        }
504    }
505}