swc_ecma_transforms_react/pure_annotations/
mod.rs

1use rustc_hash::FxHashMap;
2use swc_atoms::{atom, Atom};
3use swc_common::{comments::Comments, Span};
4use swc_ecma_ast::*;
5use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
6
7#[cfg(test)]
8mod tests;
9
10/// A pass to add a /*#__PURE__#/ annotation to calls to known pure calls.
11///
12/// This pass adds a /*#__PURE__#/ annotation to calls to known pure top-level
13/// React methods, so that terser and other minifiers can safely remove them
14/// during dead code elimination.
15/// See https://reactjs.org/docs/react-api.html
16pub fn pure_annotations<C>(comments: Option<C>) -> impl Pass
17where
18    C: Comments,
19{
20    visit_mut_pass(PureAnnotations {
21        imports: Default::default(),
22        comments,
23    })
24}
25
26struct PureAnnotations<C>
27where
28    C: Comments,
29{
30    imports: FxHashMap<Id, (Atom, Atom)>,
31    comments: Option<C>,
32}
33
34impl<C> VisitMut for PureAnnotations<C>
35where
36    C: Comments,
37{
38    noop_visit_mut_type!();
39
40    fn visit_mut_module(&mut self, module: &mut Module) {
41        // Pass 1: collect imports
42        for item in &module.body {
43            if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item {
44                let src_str = &*import.src.value;
45                if src_str != "react" && src_str != "react-dom" {
46                    continue;
47                }
48
49                for specifier in &import.specifiers {
50                    let src = import.src.value.clone();
51                    match specifier {
52                        ImportSpecifier::Named(named) => {
53                            let imported = match &named.imported {
54                                Some(ModuleExportName::Ident(imported)) => imported.sym.clone(),
55                                Some(ModuleExportName::Str(..)) => named.local.sym.clone(),
56                                None => named.local.sym.clone(),
57                                #[cfg(swc_ast_unknown)]
58                                Some(_) => continue,
59                            };
60                            self.imports.insert(named.local.to_id(), (src, imported));
61                        }
62                        ImportSpecifier::Default(default) => {
63                            self.imports
64                                .insert(default.local.to_id(), (src, atom!("default")));
65                        }
66                        ImportSpecifier::Namespace(ns) => {
67                            self.imports.insert(ns.local.to_id(), (src, atom!("*")));
68                        }
69                        #[cfg(swc_ast_unknown)]
70                        _ => (),
71                    }
72                }
73            }
74        }
75
76        if self.imports.is_empty() {
77            return;
78        }
79
80        // Pass 2: add pure annotations.
81        module.visit_mut_children_with(self);
82    }
83
84    fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
85        let is_react_call = match &call.callee {
86            Callee::Expr(expr) => match &**expr {
87                Expr::Ident(ident) => {
88                    if let Some((src, specifier)) = self.imports.get(&ident.to_id()) {
89                        is_pure(src, specifier)
90                    } else {
91                        false
92                    }
93                }
94                Expr::Member(member) => match &*member.obj {
95                    Expr::Ident(ident) => {
96                        if let Some((src, specifier)) = self.imports.get(&ident.to_id()) {
97                            if &**specifier == "default" || &**specifier == "*" {
98                                match &member.prop {
99                                    MemberProp::Ident(ident) => is_pure(src, &ident.sym),
100                                    _ => false,
101                                }
102                            } else {
103                                false
104                            }
105                        } else {
106                            false
107                        }
108                    }
109                    _ => false,
110                },
111                _ => false,
112            },
113            _ => false,
114        };
115
116        if is_react_call {
117            if let Some(comments) = &self.comments {
118                if call.span.lo.is_dummy() {
119                    call.span.lo = Span::dummy_with_cmt().lo;
120                }
121
122                comments.add_pure_comment(call.span.lo);
123            }
124        }
125
126        call.visit_mut_children_with(self);
127    }
128}
129
130fn is_pure(src: &Atom, specifier: &Atom) -> bool {
131    match &**src {
132        "react" => matches!(
133            &**specifier,
134            "cloneElement"
135                | "createContext"
136                | "createElement"
137                | "createFactory"
138                | "createRef"
139                | "forwardRef"
140                | "isValidElement"
141                | "memo"
142                | "lazy"
143        ),
144        "react-dom" => matches!(&**specifier, "createPortal"),
145        _ => false,
146    }
147}