swc_ecma_minifier/metadata/
mod.rs1use rustc_hash::FxHashSet;
2use swc_common::{
3 comments::{Comment, CommentKind, Comments},
4 Span, Spanned,
5};
6use swc_ecma_ast::*;
7use swc_ecma_usage_analyzer::marks::Marks;
8use swc_ecma_utils::NodeIgnoringSpan;
9use swc_ecma_visit::{
10 noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
11};
12
13use crate::option::CompressOptions;
14
15#[cfg(test)]
16mod tests;
17
18pub(crate) fn info_marker<'a>(
20 options: Option<&'a CompressOptions>,
21 comments: Option<&'a dyn Comments>,
22 marks: Marks,
23 ) -> impl 'a + VisitMut {
25 let pure_funcs = options.map(|options| {
26 options
27 .pure_funcs
28 .iter()
29 .map(|f| NodeIgnoringSpan::borrowed(f.as_ref()))
30 .collect()
31 });
32 InfoMarker {
33 options,
34 comments,
35 marks,
36 pure_funcs,
37 state: Default::default(),
39 pure_callee: Default::default(),
40 }
41}
42
43#[derive(Default)]
44struct State {
45 is_in_export: bool,
46}
47
48struct InfoMarker<'a> {
49 #[allow(dead_code)]
50 options: Option<&'a CompressOptions>,
51 pure_funcs: Option<FxHashSet<NodeIgnoringSpan<'a, Expr>>>,
52 pure_callee: FxHashSet<Id>,
53
54 comments: Option<&'a dyn Comments>,
55 marks: Marks,
56 state: State,
58}
59
60impl InfoMarker<'_> {
61 fn is_pure_callee(&self, callee: &Expr) -> bool {
62 match callee {
63 Expr::Ident(callee) => {
64 if self.pure_callee.contains(&callee.to_id()) {
65 return true;
66 }
67 }
68
69 Expr::Seq(callee) => {
70 if has_pure(self.comments, callee.span) {
71 return true;
72 }
73 }
74 _ => (),
75 }
76
77 if let Some(pure_fns) = &self.pure_funcs {
78 if let Expr::Ident(..) = callee {
79 if Ident::within_ignored_ctxt(|| {
81 pure_fns.contains(&NodeIgnoringSpan::borrowed(callee))
83 }) {
84 return true;
85 }
86 }
87 }
88
89 has_pure(self.comments, callee.span())
90 }
91}
92
93impl VisitMut for InfoMarker<'_> {
94 noop_visit_mut_type!(fail);
95
96 fn visit_mut_call_expr(&mut self, n: &mut CallExpr) {
97 n.visit_mut_children_with(self);
98
99 if has_noinline(self.comments, n.span)
101 || match &n.callee {
102 Callee::Expr(e) => has_noinline(self.comments, e.span()),
103 _ => false,
104 }
105 {
106 n.ctxt = n.ctxt.apply_mark(self.marks.noinline);
107 }
108
109 if match &n.callee {
112 Callee::Expr(e) => self.is_pure_callee(e),
113 _ => false,
114 } || has_pure(self.comments, n.span)
115 {
116 if !n.span.is_dummy_ignoring_cmt() {
117 n.ctxt = n.ctxt.apply_mark(self.marks.pure);
118 }
119 } else if let Some(pure_fns) = &self.pure_funcs {
120 if let Callee::Expr(e) = &n.callee {
121 Ident::within_ignored_ctxt(|| {
123 if pure_fns.contains(&NodeIgnoringSpan::borrowed(e)) {
124 n.ctxt = n.ctxt.apply_mark(self.marks.pure);
125 };
126 })
127 }
128 }
129 }
130
131 fn visit_mut_export_default_decl(&mut self, e: &mut ExportDefaultDecl) {
132 self.state.is_in_export = true;
133 e.visit_mut_children_with(self);
134 self.state.is_in_export = false;
135 }
136
137 fn visit_mut_export_default_expr(&mut self, e: &mut ExportDefaultExpr) {
138 self.state.is_in_export = true;
139 e.visit_mut_children_with(self);
140 self.state.is_in_export = false;
141 }
142
143 fn visit_mut_fn_expr(&mut self, n: &mut FnExpr) {
144 n.visit_mut_children_with(self);
145
146 if !self.state.is_in_export
147 && n.function
148 .params
149 .iter()
150 .any(|p| is_param_one_of(p, &["module", "__unused_webpack_module"]))
151 && n.function.params.iter().any(|p| {
152 is_param_one_of(
153 p,
154 &[
155 "exports",
156 "__webpack_require__",
157 "__webpack_exports__",
158 "__unused_webpack_exports",
159 ],
160 )
161 })
162 {
163 }
170 }
171
172 fn visit_mut_ident(&mut self, _: &mut Ident) {}
173
174 fn visit_mut_lit(&mut self, _: &mut Lit) {}
175
176 fn visit_mut_module(&mut self, n: &mut Module) {
177 n.visit_with(&mut InfoCollector {
178 comments: self.comments,
179 pure_callees: &mut self.pure_callee,
180 });
181
182 n.visit_mut_children_with(self);
183 }
184
185 fn visit_mut_new_expr(&mut self, n: &mut NewExpr) {
186 n.visit_mut_children_with(self);
187
188 if has_pure(self.comments, n.span) {
189 n.ctxt = n.ctxt.apply_mark(self.marks.pure);
190 }
191 }
192
193 fn visit_mut_script(&mut self, n: &mut Script) {
194 n.visit_with(&mut InfoCollector {
195 comments: self.comments,
196 pure_callees: &mut self.pure_callee,
197 });
198
199 n.visit_mut_children_with(self);
200 }
201
202 fn visit_mut_tagged_tpl(&mut self, n: &mut TaggedTpl) {
203 n.visit_mut_children_with(self);
204
205 if has_pure(self.comments, n.span) || self.is_pure_callee(&n.tag) {
206 if !n.span.is_dummy_ignoring_cmt() {
207 n.ctxt = n.ctxt.apply_mark(self.marks.pure);
208 }
209 }
210 }
211
212 fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
213 n.visit_mut_children_with(self);
214
215 if has_const_ann(self.comments, n.span) {
216 n.ctxt = n.ctxt.apply_mark(self.marks.const_ann);
217 }
218 }
219}
220
221fn is_param_one_of(p: &Param, allowed: &[&str]) -> bool {
222 match &p.pat {
223 Pat::Ident(i) => allowed.contains(&&*i.id.sym),
224 _ => false,
225 }
226}
227
228const NO_SIDE_EFFECTS_FLAG: &str = "NO_SIDE_EFFECTS";
229
230struct InfoCollector<'a> {
231 comments: Option<&'a dyn Comments>,
232
233 pure_callees: &'a mut FxHashSet<Id>,
234}
235
236impl Visit for InfoCollector<'_> {
237 noop_visit_type!(fail);
238
239 fn visit_export_decl(&mut self, f: &ExportDecl) {
240 f.visit_children_with(self);
241
242 if let Decl::Fn(f) = &f.decl {
243 if has_flag(self.comments, f.function.span, NO_SIDE_EFFECTS_FLAG) {
244 self.pure_callees.insert(f.ident.to_id());
245 }
246 }
247 }
248
249 fn visit_fn_decl(&mut self, f: &FnDecl) {
250 f.visit_children_with(self);
251
252 if has_flag(self.comments, f.function.span, NO_SIDE_EFFECTS_FLAG) {
253 self.pure_callees.insert(f.ident.to_id());
254 }
255 }
256
257 fn visit_fn_expr(&mut self, f: &FnExpr) {
258 f.visit_children_with(self);
259
260 if let Some(ident) = &f.ident {
261 if has_flag(self.comments, f.function.span, NO_SIDE_EFFECTS_FLAG) {
262 self.pure_callees.insert(ident.to_id());
263 }
264 }
265 }
266
267 fn visit_var_decl(&mut self, decl: &VarDecl) {
268 decl.visit_children_with(self);
269
270 for v in &decl.decls {
271 if let Pat::Ident(ident) = &v.name {
272 if let Some(init) = &v.init {
273 if has_flag(self.comments, decl.span, NO_SIDE_EFFECTS_FLAG)
274 || has_flag(self.comments, v.span, NO_SIDE_EFFECTS_FLAG)
275 || has_flag(self.comments, init.span(), NO_SIDE_EFFECTS_FLAG)
276 {
277 self.pure_callees.insert(ident.to_id());
278 }
279 }
280 }
281 }
282 }
283}
284
285pub(super) fn has_const_ann(comments: Option<&dyn Comments>, span: Span) -> bool {
287 find_comment(comments, span, |c| {
288 if c.kind == CommentKind::Block {
289 if !c.text.starts_with('*') {
290 return false;
291 }
292 let t = c.text[1..].trim();
293 if t.starts_with("@const") {
295 return true;
296 }
297 }
298
299 false
300 })
301}
302
303pub(super) fn has_noinline(comments: Option<&dyn Comments>, span: Span) -> bool {
305 has_flag(comments, span, "NOINLINE")
306}
307
308pub(super) fn has_pure(comments: Option<&dyn Comments>, span: Span) -> bool {
310 span.is_pure() || has_flag(comments, span, "PURE")
311}
312
313fn find_comment<F>(comments: Option<&dyn Comments>, span: Span, mut op: F) -> bool
314where
315 F: FnMut(&Comment) -> bool,
316{
317 let mut found = false;
318 if let Some(comments) = comments {
319 let cs = comments.get_leading(span.lo);
320 if let Some(cs) = cs {
321 for c in &cs {
322 found |= op(c);
323 if found {
324 break;
325 }
326 }
327 }
328 }
329
330 found
331}
332
333fn has_flag(comments: Option<&dyn Comments>, span: Span, text: &'static str) -> bool {
334 if span.is_dummy_ignoring_cmt() {
335 return false;
336 }
337
338 comments.has_flag(span.lo, text)
339}