swc_ecma_minifier/metadata/
mod.rs

1use 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
18/// This pass analyzes the comment and convert it to a mark.
19pub(crate) fn info_marker<'a>(
20    options: Option<&'a CompressOptions>,
21    comments: Option<&'a dyn Comments>,
22    marks: Marks,
23    // unresolved_mark: Mark,
24) -> 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        // unresolved_mark,
38        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    // unresolved_mark: Mark,
57    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                // Check for pure_funcs
80                if Ident::within_ignored_ctxt(|| {
81                    //
82                    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        // TODO: remove after we figure out how to move comments properly
100        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        // We check callee in some cases because we move comments
110        // See https://github.com/swc-project/swc/issues/7241
111        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                // Check for pure_funcs
122                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            // if is_standalone(&mut n.function, self.unresolved_mark) {
164            //     // self.state.is_bundle = true;
165
166            //     // n.function.span =
167            //     // n.function.span.apply_mark(self.marks.standalone);
168            // }
169        }
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
285/// Check for `/** @const */`.
286pub(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            //
294            if t.starts_with("@const") {
295                return true;
296            }
297        }
298
299        false
300    })
301}
302
303/// Check for `/*#__NOINLINE__*/`
304pub(super) fn has_noinline(comments: Option<&dyn Comments>, span: Span) -> bool {
305    has_flag(comments, span, "NOINLINE")
306}
307
308/// Check for `/*#__PURE__*/`
309pub(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}