swc_ecma_transforms_react/pure_annotations/
mod.rs1use 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
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, (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 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 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}