swc_ecma_transforms_react/pure_annotations/
mod.rs

1use rustc_hash::FxHashMap;
2use swc_atoms::{atom, Atom, Wtf8Atom};
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, (Wtf8Atom, 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 Some(src_str) = import.src.value.as_str() else {
45                    continue;
46                };
47                if src_str != "react" && src_str != "react-dom" {
48                    continue;
49                }
50
51                for specifier in &import.specifiers {
52                    let src = import.src.value.clone();
53                    match specifier {
54                        ImportSpecifier::Named(named) => {
55                            let imported: Atom = match &named.imported {
56                                Some(ModuleExportName::Ident(imported)) => imported.sym.clone(),
57                                Some(ModuleExportName::Str(s)) => {
58                                    s.value.to_atom_lossy().into_owned()
59                                }
60                                None => named.local.sym.clone(),
61                                #[cfg(swc_ast_unknown)]
62                                Some(_) => continue,
63                            };
64                            self.imports.insert(named.local.to_id(), (src, imported));
65                        }
66                        ImportSpecifier::Default(default) => {
67                            self.imports
68                                .insert(default.local.to_id(), (src, atom!("default")));
69                        }
70                        ImportSpecifier::Namespace(ns) => {
71                            self.imports.insert(ns.local.to_id(), (src, atom!("*")));
72                        }
73                        #[cfg(swc_ast_unknown)]
74                        _ => (),
75                    }
76                }
77            }
78        }
79
80        if self.imports.is_empty() {
81            return;
82        }
83
84        // Pass 2: add pure annotations.
85        module.visit_mut_children_with(self);
86    }
87
88    fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
89        let is_react_call = match &call.callee {
90            Callee::Expr(expr) => match &**expr {
91                Expr::Ident(ident) => {
92                    if let Some((src, specifier)) = self.imports.get(&ident.to_id()) {
93                        is_pure(src, specifier)
94                    } else {
95                        false
96                    }
97                }
98                Expr::Member(member) => match &*member.obj {
99                    Expr::Ident(ident) => {
100                        if let Some((src, specifier)) = self.imports.get(&ident.to_id()) {
101                            if &**specifier == "default" || &**specifier == "*" {
102                                match &member.prop {
103                                    MemberProp::Ident(ident) => is_pure(src, &ident.sym),
104                                    _ => false,
105                                }
106                            } else {
107                                false
108                            }
109                        } else {
110                            false
111                        }
112                    }
113                    _ => false,
114                },
115                _ => false,
116            },
117            _ => false,
118        };
119
120        if is_react_call {
121            if let Some(comments) = &self.comments {
122                if call.span.lo.is_dummy() {
123                    call.span.lo = Span::dummy_with_cmt().lo;
124                }
125
126                comments.add_pure_comment(call.span.lo);
127            }
128        }
129
130        call.visit_mut_children_with(self);
131    }
132}
133
134fn is_pure(src: &Wtf8Atom, specifier: &Atom) -> bool {
135    let Some(src) = src.as_str() else {
136        return false;
137    };
138    let specifier = specifier.as_str();
139
140    match src {
141        "react" => matches!(
142            specifier,
143            "cloneElement"
144                | "createContext"
145                | "createElement"
146                | "createFactory"
147                | "createRef"
148                | "forwardRef"
149                | "isValidElement"
150                | "memo"
151                | "lazy"
152        ),
153        "react-dom" => specifier == "createPortal",
154        _ => false,
155    }
156}