swc_ecma_transforms_react/pure_annotations/
mod.rs1use 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
10pub 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 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 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}