swc_ecma_transforms_optimization/
inline_globals.rs

1use rustc_hash::FxHashMap;
2use swc_atoms::Atom;
3use swc_common::{sync::Lrc, Mark, SyntaxContext};
4use swc_ecma_ast::*;
5use swc_ecma_transforms_base::perf::{ParVisitMut, Parallel};
6use swc_ecma_utils::{parallel::cpu_count, NodeIgnoringSpan};
7use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
8
9/// The key will be compared using [EqIgnoreSpan::eq_ignore_span], and matched
10/// expressions will be replaced with the value.
11pub type GlobalExprMap = Lrc<FxHashMap<NodeIgnoringSpan<'static, Expr>, Expr>>;
12
13/// Create a global inlining pass, which replaces expressions with the specified
14/// value.
15///
16/// See [GlobalExprMap] for description.
17///
18/// Note: Values specified in `global_exprs` have higher precedence than
19pub fn inline_globals(
20    unresolved_mark: Mark,
21    envs: Lrc<FxHashMap<Atom, Expr>>,
22    globals: Lrc<FxHashMap<Atom, Expr>>,
23    global_exprs: GlobalExprMap,
24    typeofs: Lrc<FxHashMap<Atom, Atom>>,
25) -> impl Pass {
26    let unresolved_ctxt = SyntaxContext::default().apply_mark(unresolved_mark);
27
28    visit_mut_pass(InlineGlobals {
29        envs,
30        globals,
31        global_exprs,
32        typeofs,
33        unresolved_ctxt,
34    })
35}
36
37#[derive(Clone)]
38struct InlineGlobals {
39    envs: Lrc<FxHashMap<Atom, Expr>>,
40    globals: Lrc<FxHashMap<Atom, Expr>>,
41    global_exprs: Lrc<FxHashMap<NodeIgnoringSpan<'static, Expr>, Expr>>,
42
43    typeofs: Lrc<FxHashMap<Atom, Atom>>,
44
45    unresolved_ctxt: SyntaxContext,
46}
47
48impl Parallel for InlineGlobals {
49    fn create(&self) -> Self {
50        self.clone()
51    }
52
53    fn merge(&mut self, _: Self) {}
54}
55
56impl VisitMut for InlineGlobals {
57    noop_visit_mut_type!(fail);
58
59    fn visit_mut_class_members(&mut self, members: &mut Vec<ClassMember>) {
60        self.visit_mut_par(cpu_count(), members);
61    }
62
63    fn visit_mut_expr(&mut self, expr: &mut Expr) {
64        if let Expr::Ident(Ident { ctxt, .. }) = expr {
65            // Ignore declared variables
66            if *ctxt != self.unresolved_ctxt {
67                return;
68            }
69        }
70
71        if let Some(value) =
72            Ident::within_ignored_ctxt(|| self.global_exprs.get(&NodeIgnoringSpan::borrowed(expr)))
73        {
74            *expr = value.clone();
75            expr.visit_mut_with(self);
76            return;
77        }
78
79        expr.visit_mut_children_with(self);
80
81        match expr {
82            Expr::Ident(Ident { ref sym, .. }) => {
83                // It's ok because we don't recurse into member expressions.
84                if let Some(value) = self.globals.get(sym) {
85                    let mut value = value.clone();
86                    value.visit_mut_with(self);
87                    *expr = value;
88                }
89            }
90
91            Expr::Unary(UnaryExpr {
92                span,
93                op: op!("typeof"),
94                arg,
95                ..
96            }) => {
97                if let Expr::Ident(Ident {
98                    ref sym,
99                    ctxt: arg_ctxt,
100                    ..
101                }) = &**arg
102                {
103                    // It's a declared variable
104                    if *arg_ctxt != self.unresolved_ctxt {
105                        return;
106                    }
107
108                    // It's ok because we don't recurse into member expressions.
109                    if let Some(value) = self.typeofs.get(sym).cloned() {
110                        *expr = Lit::Str(Str {
111                            span: *span,
112                            raw: None,
113                            value,
114                        })
115                        .into();
116                    }
117                }
118            }
119
120            Expr::Member(MemberExpr { obj, prop, .. }) => match &**obj {
121                Expr::Member(MemberExpr {
122                    obj: first_obj,
123                    prop: inner_prop,
124                    ..
125                }) if inner_prop.is_ident_with("env") => {
126                    if first_obj.is_ident_ref_to("process") {
127                        match prop {
128                            MemberProp::Computed(ComputedPropName { expr: c, .. }) => {
129                                if let Expr::Lit(Lit::Str(Str { value: sym, .. })) = &**c {
130                                    if let Some(env) = self.envs.get(sym) {
131                                        *expr = env.clone();
132                                    }
133                                }
134                            }
135
136                            MemberProp::Ident(IdentName { sym, .. }) => {
137                                if let Some(env) = self.envs.get(sym) {
138                                    *expr = env.clone();
139                                }
140                            }
141                            _ => {}
142                        }
143                    }
144                }
145                _ => (),
146            },
147            _ => {}
148        }
149    }
150
151    fn visit_mut_expr_or_spreads(&mut self, n: &mut Vec<ExprOrSpread>) {
152        self.visit_mut_par(cpu_count(), n);
153    }
154
155    fn visit_mut_exprs(&mut self, n: &mut Vec<Box<Expr>>) {
156        self.visit_mut_par(cpu_count(), n);
157    }
158
159    fn visit_mut_opt_vec_expr_or_spreads(&mut self, n: &mut Vec<Option<ExprOrSpread>>) {
160        self.visit_mut_par(cpu_count(), n);
161    }
162
163    fn visit_mut_prop(&mut self, p: &mut Prop) {
164        p.visit_mut_children_with(self);
165
166        if let Prop::Shorthand(i) = p {
167            // Ignore declared variables
168            if i.ctxt != self.unresolved_ctxt {
169                return;
170            }
171
172            // It's ok because we don't recurse into member expressions.
173            if let Some(mut value) = self.globals.get(&i.sym).cloned().map(Box::new) {
174                value.visit_mut_with(self);
175                *p = Prop::KeyValue(KeyValueProp {
176                    key: PropName::Ident(i.clone().into()),
177                    value,
178                });
179            }
180        }
181    }
182
183    fn visit_mut_prop_or_spreads(&mut self, n: &mut Vec<PropOrSpread>) {
184        self.visit_mut_par(cpu_count(), n);
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use swc_common::Mark;
191    use swc_ecma_transforms_base::resolver;
192    use swc_ecma_transforms_testing::{test, Tester};
193    use swc_ecma_utils::{DropSpan, StmtOrModuleItem};
194
195    use super::*;
196
197    fn mk_map(
198        tester: &mut Tester<'_>,
199        values: &[(&str, &str)],
200        is_env: bool,
201    ) -> FxHashMap<Atom, Expr> {
202        let mut m = FxHashMap::default();
203
204        for (k, v) in values {
205            let v = if is_env {
206                format!("'{v}'")
207            } else {
208                (*v).into()
209            };
210
211            let v = tester
212                .apply_transform(
213                    visit_mut_pass(DropSpan),
214                    "global.js",
215                    ::swc_ecma_parser::Syntax::default(),
216                    None,
217                    &v,
218                )
219                .unwrap();
220
221            let v = match v {
222                Program::Module(mut m) => m.body.pop().and_then(|x| x.into_stmt().ok()),
223                Program::Script(mut s) => s.body.pop(),
224            };
225            assert!(v.is_some());
226            let v = match v.unwrap() {
227                Stmt::Expr(ExprStmt { expr, .. }) => *expr,
228                _ => unreachable!(),
229            };
230
231            m.insert((*k).into(), v);
232        }
233
234        m
235    }
236
237    fn envs(tester: &mut Tester<'_>, values: &[(&str, &str)]) -> Lrc<FxHashMap<Atom, Expr>> {
238        Lrc::new(mk_map(tester, values, true))
239    }
240
241    fn globals(tester: &mut Tester<'_>, values: &[(&str, &str)]) -> Lrc<FxHashMap<Atom, Expr>> {
242        Lrc::new(mk_map(tester, values, false))
243    }
244
245    test!(
246        ::swc_ecma_parser::Syntax::default(),
247        |tester| {
248            let unresolved_mark = Mark::new();
249            let top_level_mark = Mark::new();
250
251            (
252                resolver(unresolved_mark, top_level_mark, false),
253                inline_globals(
254                    unresolved_mark,
255                    envs(tester, &[]),
256                    globals(tester, &[]),
257                    Default::default(),
258                    Default::default(),
259                ),
260            )
261        },
262        issue_215,
263        r#"if (process.env.x === 'development') {}"#
264    );
265
266    test!(
267        ::swc_ecma_parser::Syntax::default(),
268        |tester| {
269            let unresolved_mark = Mark::new();
270            let top_level_mark = Mark::new();
271
272            (
273                resolver(unresolved_mark, top_level_mark, false),
274                inline_globals(
275                    unresolved_mark,
276                    envs(tester, &[("NODE_ENV", "development")]),
277                    globals(tester, &[]),
278                    Default::default(),
279                    Default::default(),
280                ),
281            )
282        },
283        node_env,
284        r#"if (process.env.NODE_ENV === 'development') {}"#
285    );
286
287    test!(
288        ::swc_ecma_parser::Syntax::default(),
289        |tester| {
290            let unresolved_mark = Mark::new();
291            let top_level_mark = Mark::new();
292
293            (
294                resolver(unresolved_mark, top_level_mark, false),
295                inline_globals(
296                    unresolved_mark,
297                    envs(tester, &[]),
298                    globals(tester, &[("__DEBUG__", "true")]),
299                    Default::default(),
300                    Default::default(),
301                ),
302            )
303        },
304        globals_simple,
305        r#"if (__DEBUG__) {}"#
306    );
307
308    test!(
309        ::swc_ecma_parser::Syntax::default(),
310        |tester| {
311            let unresolved_mark = Mark::new();
312            let top_level_mark = Mark::new();
313
314            (
315                resolver(unresolved_mark, top_level_mark, false),
316                inline_globals(
317                    unresolved_mark,
318                    envs(tester, &[]),
319                    globals(tester, &[("debug", "true")]),
320                    Default::default(),
321                    Default::default(),
322                ),
323            )
324        },
325        non_global,
326        r#"if (foo.debug) {}"#
327    );
328
329    test!(
330        Default::default(),
331        |tester| {
332            let unresolved_mark = Mark::new();
333            let top_level_mark = Mark::new();
334
335            (
336                resolver(unresolved_mark, top_level_mark, false),
337                inline_globals(
338                    unresolved_mark,
339                    envs(tester, &[]),
340                    globals(tester, &[]),
341                    Default::default(),
342                    Default::default(),
343                ),
344            )
345        },
346        issue_417_1,
347        "const test = process.env['x']"
348    );
349
350    test!(
351        Default::default(),
352        |tester| {
353            let unresolved_mark = Mark::new();
354            let top_level_mark = Mark::new();
355
356            (
357                resolver(unresolved_mark, top_level_mark, false),
358                inline_globals(
359                    unresolved_mark,
360                    envs(tester, &[("x", "FOO")]),
361                    globals(tester, &[]),
362                    Default::default(),
363                    Default::default(),
364                ),
365            )
366        },
367        issue_417_2,
368        "const test = process.env['x']"
369    );
370
371    test!(
372        Default::default(),
373        |tester| {
374            let unresolved_mark = Mark::new();
375            let top_level_mark = Mark::new();
376
377            (
378                resolver(unresolved_mark, top_level_mark, false),
379                inline_globals(
380                    unresolved_mark,
381                    envs(tester, &[("x", "BAR")]),
382                    globals(tester, &[]),
383                    Default::default(),
384                    Default::default(),
385                ),
386            )
387        },
388        issue_2499_1,
389        "process.env.x = 'foo'"
390    );
391}