swc_ecma_transforms_optimization/
inline_globals.rs

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