swc_ecma_minifier/compress/optimize/
props.rs

1use std::borrow::Borrow;
2
3use swc_common::{util::take::Take, DUMMY_SP};
4use swc_ecma_ast::*;
5use swc_ecma_utils::{contains_this_expr, private_ident, prop_name_eq, ExprExt};
6
7use super::{unused::PropertyAccessOpts, BitCtx, Optimizer};
8use crate::{program_data::VarUsageInfoFlags, util::deeply_contains_this_expr};
9
10/// Methods related to the option `hoist_props`.
11impl Optimizer<'_> {
12    pub(super) fn hoist_props_of_var(
13        &mut self,
14        n: &mut VarDeclarator,
15    ) -> Option<Vec<VarDeclarator>> {
16        if !self.options.hoist_props {
17            log_abort!("hoist_props: option is disabled");
18            return None;
19        }
20        if self.ctx.bit_ctx.contains(BitCtx::IsExported) {
21            log_abort!("hoist_props: Exported variable is not hoisted");
22            return None;
23        }
24        if self.ctx.in_top_level() && !self.options.top_level() {
25            log_abort!("hoist_props: Top-level variable is not hoisted");
26            return None;
27        }
28
29        if let Pat::Ident(name) = &mut n.name {
30            if name.id.ctxt == self.marks.top_level_ctxt
31                && self.options.top_retain.contains(&name.id.sym)
32            {
33                log_abort!("hoist_props: Variable `{}` is retained", name.id.sym);
34                return None;
35            }
36
37            if !self.may_add_ident() {
38                return None;
39            }
40
41            // If a variable is initialized multiple time, we currently don't do anything
42            // smart.
43            let usage = self.data.vars.get(&name.to_id())?;
44            if usage.mutated()
45                || usage.flags.intersects(
46                    VarUsageInfoFlags::USED_ABOVE_DECL
47                        .union(VarUsageInfoFlags::USED_AS_REF)
48                        .union(VarUsageInfoFlags::USED_AS_ARG)
49                        .union(VarUsageInfoFlags::INDEXED_WITH_DYNAMIC_KEY)
50                        .union(VarUsageInfoFlags::USED_RECURSIVELY),
51                )
52            {
53                log_abort!("hoist_props: Variable `{}` is not a candidate", name.id);
54                return None;
55            }
56
57            if usage.accessed_props.is_empty() {
58                log_abort!(
59                    "hoist_props: Variable `{}` is not accessed with known keys",
60                    name.id
61                );
62                return None;
63            }
64
65            let accessed_props_count: u32 = usage.accessed_props.values().sum();
66            if accessed_props_count < usage.ref_count {
67                log_abort!(
68                    "hoist_props: Variable `{}` is directly used without accessing its properties",
69                    name.id
70                );
71                return None;
72            }
73
74            // We should abort if unknown property is used.
75            let mut unknown_used_props = self
76                .data
77                .vars
78                .get(&name.to_id())
79                .map(|v| v.accessed_props.clone())
80                .unwrap_or_default();
81
82            if let Some(Expr::Object(init)) = n.init.as_deref() {
83                for prop in &init.props {
84                    let prop = match prop {
85                        PropOrSpread::Spread(_) => return None,
86                        PropOrSpread::Prop(prop) => prop,
87                        #[cfg(swc_ast_unknown)]
88                        _ => panic!("unable to access unknown nodes"),
89                    };
90
91                    match &**prop {
92                        Prop::KeyValue(p) => {
93                            if !is_expr_fine_for_hoist_props(&p.value) {
94                                return None;
95                            }
96
97                            match &p.key {
98                                PropName::Str(s) => {
99                                    if let Some(v) = unknown_used_props.get_mut(&s.value) {
100                                        *v = 0;
101                                    }
102                                }
103                                PropName::Ident(i) => {
104                                    if let Some(v) = unknown_used_props.get_mut(i.sym.borrow()) {
105                                        *v = 0;
106                                    }
107                                }
108                                _ => return None,
109                            }
110                        }
111                        Prop::Shorthand(p) => {
112                            if let Some(v) = unknown_used_props.get_mut(p.sym.borrow()) {
113                                *v = 0;
114                            }
115                        }
116                        _ => return None,
117                    }
118                }
119            } else {
120                if self.mode.should_be_very_correct() {
121                    return None;
122                }
123            }
124
125            if !unknown_used_props.iter().all(|(_, v)| *v == 0) {
126                log_abort!("[x] unknown used props: {:?}", unknown_used_props);
127                return None;
128            }
129
130            if let Some(init) = n.init.as_deref() {
131                self.mode.store(name.to_id(), init);
132            }
133
134            let mut new_vars = Vec::new();
135
136            let object = n.init.as_mut()?.as_mut_object()?;
137
138            self.changed = true;
139            report_change!(
140                "hoist_props: Hoisting properties of a variable `{}`",
141                name.id.sym
142            );
143
144            for prop in &mut object.props {
145                let prop = match prop {
146                    PropOrSpread::Spread(_) => unreachable!(),
147                    PropOrSpread::Prop(prop) => prop,
148                    #[cfg(swc_ast_unknown)]
149                    _ => panic!("unable to access unknown nodes"),
150                };
151
152                let value = match &mut **prop {
153                    Prop::KeyValue(p) => p.value.take(),
154                    Prop::Shorthand(p) => p.clone().into(),
155                    _ => unreachable!(),
156                };
157
158                let (key, suffix) = match &**prop {
159                    Prop::KeyValue(p) => match &p.key {
160                        PropName::Ident(i) => (i.sym.clone().into(), i.sym.clone()),
161                        PropName::Str(s) => (
162                            s.value.clone(),
163                            s.value
164                                .code_points()
165                                .map(|c| {
166                                    c.to_char()
167                                        .filter(|&c| Ident::is_valid_start(c))
168                                        .unwrap_or('$')
169                                })
170                                .collect::<String>()
171                                .into(),
172                        ),
173                        _ => unreachable!(),
174                    },
175                    Prop::Shorthand(p) => (p.sym.clone().into(), p.sym.clone()),
176                    _ => unreachable!(),
177                };
178
179                let new_var_name = private_ident!(format!("{}_{}", name.id.sym, suffix));
180
181                let new_var = VarDeclarator {
182                    span: DUMMY_SP,
183                    name: new_var_name.clone().into(),
184                    init: Some(value),
185                    definite: false,
186                };
187
188                self.vars
189                    .hoisted_props
190                    .insert((name.to_id(), key), new_var_name);
191
192                new_vars.push(new_var);
193            }
194            // Mark the variable as dropped.
195            n.name.take();
196
197            return Some(new_vars);
198        }
199
200        None
201    }
202
203    pub(super) fn replace_props(&mut self, e: &mut Expr) {
204        let member = match e {
205            Expr::Member(m) => m,
206            Expr::OptChain(m) => match &mut *m.base {
207                OptChainBase::Member(m) => m,
208                _ => return,
209            },
210            _ => return,
211        };
212        if let Expr::Ident(obj) = &*member.obj {
213            let sym = match &member.prop {
214                MemberProp::Ident(i) => i.sym.borrow(),
215                MemberProp::Computed(e) => match &*e.expr {
216                    Expr::Lit(Lit::Str(s)) => &s.value,
217                    _ => return,
218                },
219                _ => return,
220            };
221
222            if let Some(value) = self
223                .vars
224                .hoisted_props
225                .get(&(obj.to_id(), sym.clone()))
226                .cloned()
227            {
228                report_change!("hoist_props: Inlining `{}.{}`", obj.sym, sym);
229                self.changed = true;
230                *e = value.into();
231            }
232        }
233    }
234}
235
236fn is_expr_fine_for_hoist_props(value: &Expr) -> bool {
237    match value {
238        Expr::Ident(..) | Expr::Lit(..) | Expr::Arrow(..) | Expr::Class(..) => true,
239
240        Expr::Fn(f) => !contains_this_expr(&f.function.body),
241
242        Expr::Unary(u) => match u.op {
243            op!("void") | op!("typeof") | op!("!") => is_expr_fine_for_hoist_props(&u.arg),
244            _ => false,
245        },
246
247        Expr::Array(a) => a.elems.iter().all(|elem| match elem {
248            Some(elem) => elem.spread.is_none() && is_expr_fine_for_hoist_props(&elem.expr),
249            None => true,
250        }),
251
252        Expr::Object(o) => o.props.iter().all(|prop| match prop {
253            PropOrSpread::Spread(_) => false,
254            PropOrSpread::Prop(p) => match &**p {
255                Prop::Shorthand(..) => true,
256                Prop::KeyValue(p) => is_expr_fine_for_hoist_props(&p.value),
257                _ => false,
258            },
259            #[cfg(swc_ast_unknown)]
260            _ => panic!("unable to access unknown nodes"),
261        }),
262
263        _ => false,
264    }
265}
266
267impl Optimizer<'_> {
268    /// Converts `{ a: 1 }.a` into `1`.
269    pub(super) fn handle_property_access(&mut self, e: &mut Expr) {
270        if !self.options.props {
271            return;
272        }
273
274        if self
275            .ctx
276            .bit_ctx
277            .intersects(BitCtx::IsUpdateArg | BitCtx::IsCallee | BitCtx::IsExactLhsOfAssign)
278        {
279            return;
280        }
281
282        let me = match e {
283            Expr::Member(m) => m,
284            _ => return,
285        };
286
287        let key = match &me.prop {
288            MemberProp::Ident(prop) => prop,
289            _ => return,
290        };
291
292        let obj = match &mut *me.obj {
293            Expr::Object(o) => o,
294            _ => return,
295        };
296
297        let duplicate_prop = obj
298            .props
299            .iter()
300            .filter(|prop| match prop {
301                PropOrSpread::Spread(_) => false,
302                PropOrSpread::Prop(p) => match &**p {
303                    Prop::Shorthand(p) => p.sym == key.sym,
304                    Prop::KeyValue(p) => prop_name_eq(&p.key, &key.sym),
305                    Prop::Assign(p) => p.key.sym == key.sym,
306                    Prop::Getter(p) => prop_name_eq(&p.key, &key.sym),
307                    Prop::Setter(p) => prop_name_eq(&p.key, &key.sym),
308                    Prop::Method(p) => prop_name_eq(&p.key, &key.sym),
309                    #[cfg(swc_ast_unknown)]
310                    _ => panic!("unable to access unknown nodes"),
311                },
312                #[cfg(swc_ast_unknown)]
313                _ => panic!("unable to access unknown nodes"),
314            })
315            .count()
316            != 1;
317        if duplicate_prop {
318            return;
319        }
320
321        if obj.props.iter().any(|prop| match prop {
322            PropOrSpread::Spread(s) => self.should_preserve_property_access(
323                &s.expr,
324                PropertyAccessOpts {
325                    allow_getter: false,
326                    only_ident: false,
327                },
328            ),
329            PropOrSpread::Prop(p) => match &**p {
330                Prop::Shorthand(..) => false,
331                Prop::KeyValue(p) => {
332                    p.key.is_computed()
333                        || p.value.may_have_side_effects(self.ctx.expr_ctx)
334                        || deeply_contains_this_expr(&p.value)
335                }
336                Prop::Assign(p) => {
337                    p.value.may_have_side_effects(self.ctx.expr_ctx)
338                        || deeply_contains_this_expr(&p.value)
339                }
340                Prop::Getter(p) => p.key.is_computed(),
341                Prop::Setter(p) => p.key.is_computed(),
342                Prop::Method(p) => p.key.is_computed(),
343                #[cfg(swc_ast_unknown)]
344                _ => panic!("unable to access unknown nodes"),
345            },
346            #[cfg(swc_ast_unknown)]
347            _ => panic!("unable to access unknown nodes"),
348        }) {
349            log_abort!("Property accesses should not be inlined to preserve side effects");
350            return;
351        }
352
353        for prop in &obj.props {
354            match prop {
355                PropOrSpread::Spread(_) => {}
356                PropOrSpread::Prop(p) => match &**p {
357                    Prop::Shorthand(_) => {}
358                    Prop::KeyValue(p) => {
359                        if prop_name_eq(&p.key, &key.sym) {
360                            report_change!(
361                                "properties: Inlining a key-value property `{}`",
362                                key.sym
363                            );
364                            self.changed = true;
365                            *e = *p.value.clone();
366                            return;
367                        }
368                    }
369                    Prop::Assign(_) => {}
370                    Prop::Getter(_) => {}
371                    Prop::Setter(_) => {}
372                    Prop::Method(_) => {}
373                    #[cfg(swc_ast_unknown)]
374                    _ => panic!("unable to access unknown nodes"),
375                },
376                #[cfg(swc_ast_unknown)]
377                _ => panic!("unable to access unknown nodes"),
378            }
379        }
380    }
381}