swc_ecma_transforms_optimization/
inline_globals.rs

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