swc_ecma_transforms_react/refresh/
hook.rs

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