swc_ecma_minifier/compress/pure/
member_expr.rs

1use phf::phf_set;
2use swc_atoms::Atom;
3use swc_common::Spanned;
4use swc_ecma_ast::{
5    ArrayLit, Expr, ExprOrSpread, IdentName, Lit, MemberExpr, MemberProp, ObjectLit, Prop,
6    PropOrSpread, SeqExpr, Str,
7};
8use swc_ecma_utils::{prop_name_eq, ExprExt, Known};
9
10use super::Pure;
11use crate::compress::pure::Ctx;
12
13/// Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
14static ARRAY_SYMBOLS: phf::Set<&str> = phf_set!(
15    // Constructor
16    "constructor",
17    // Properties
18    "length",
19    // Methods
20    "at",
21    "concat",
22    "copyWithin",
23    "entries",
24    "every",
25    "fill",
26    "filter",
27    "find",
28    "findIndex",
29    "findLast",
30    "findLastIndex",
31    "flat",
32    "flatMap",
33    "forEach",
34    "includes",
35    "indexOf",
36    "join",
37    "keys",
38    "lastIndexOf",
39    "map",
40    "pop",
41    "push",
42    "reduce",
43    "reduceRight",
44    "reverse",
45    "shift",
46    "slice",
47    "some",
48    "sort",
49    "splice",
50    "toLocaleString",
51    "toReversed",
52    "toSorted",
53    "toSpliced",
54    "toString",
55    "unshift",
56    "values",
57    "with"
58);
59
60/// Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
61static STRING_SYMBOLS: phf::Set<&str> = phf_set!(
62    // Constructor
63    "constructor",
64    // Properties
65    "length",
66    // Methods
67    "anchor",
68    "at",
69    "big",
70    "blink",
71    "bold",
72    "charAt",
73    "charCodeAt",
74    "codePointAt",
75    "concat",
76    "endsWith",
77    "fixed",
78    "fontcolor",
79    "fontsize",
80    "includes",
81    "indexOf",
82    "isWellFormed",
83    "italics",
84    "lastIndexOf",
85    "link",
86    "localeCompare",
87    "match",
88    "matchAll",
89    "normalize",
90    "padEnd",
91    "padStart",
92    "repeat",
93    "replace",
94    "replaceAll",
95    "search",
96    "slice",
97    "small",
98    "split",
99    "startsWith",
100    "strike",
101    "sub",
102    "substr",
103    "substring",
104    "sup",
105    "toLocaleLowerCase",
106    "toLocaleUpperCase",
107    "toLowerCase",
108    "toString",
109    "toUpperCase",
110    "toWellFormed",
111    "trim",
112    "trimEnd",
113    "trimStart",
114    "valueOf"
115);
116
117/// Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
118static OBJECT_SYMBOLS: phf::Set<&str> = phf_set!(
119    // Constructor
120    "constructor",
121    // Properties
122    "__proto__",
123    // Methods
124    "__defineGetter__",
125    "__defineSetter__",
126    "__lookupGetter__",
127    "__lookupSetter__",
128    "hasOwnProperty",
129    "isPrototypeOf",
130    "propertyIsEnumerable",
131    "toLocaleString",
132    "toString",
133    "valueOf",
134    // removed, but kept in as these are often checked and polyfilled
135    "watch",
136    "unwatch"
137);
138
139fn is_object_symbol(sym: &str) -> bool {
140    OBJECT_SYMBOLS.contains(sym)
141}
142
143fn is_array_symbol(sym: &str) -> bool {
144    // Inherits: Object
145    ARRAY_SYMBOLS.contains(sym) || is_object_symbol(sym)
146}
147
148fn is_string_symbol(sym: &str) -> bool {
149    // Inherits: Object
150    STRING_SYMBOLS.contains(sym) || is_object_symbol(sym)
151}
152
153/// Checks if the given key exists in the given properties, taking the
154/// `__proto__` property and order of keys into account (the order of keys
155/// matters for nested `__proto__` properties).
156///
157/// Returns `None` if the key's existence is uncertain, or `Some` if it is
158/// certain.
159///
160/// A key's existence is uncertain if a `__proto__` property exists and the
161/// value is non-literal.
162fn does_key_exist(key: &str, props: &Vec<PropOrSpread>) -> Option<bool> {
163    for prop in props {
164        match prop {
165            PropOrSpread::Prop(prop) => match &**prop {
166                Prop::Shorthand(ident) => {
167                    if ident.sym == key {
168                        return Some(true);
169                    }
170                }
171
172                Prop::KeyValue(prop) => {
173                    if key != "__proto__" && prop_name_eq(&prop.key, "__proto__") {
174                        // If __proto__ is defined, we need to check the contents of it,
175                        // as well as any nested __proto__ objects
176                        if let Some(object) = prop.value.as_object() {
177                            // __proto__ is an ObjectLiteral, check if key exists in it
178                            let exists = does_key_exist(key, &object.props);
179                            if exists.is_none() {
180                                return None;
181                            } else if exists.is_some_and(|exists| exists) {
182                                return Some(true);
183                            }
184                        } else {
185                            // __proto__ is not a literal, it is impossible to know if the
186                            // key exists or not
187                            return None;
188                        }
189                    } else {
190                        // Normal key
191                        if prop_name_eq(&prop.key, key) {
192                            return Some(true);
193                        }
194                    }
195                }
196
197                // invalid
198                Prop::Assign(_) => {
199                    return None;
200                }
201
202                Prop::Getter(getter) => {
203                    if prop_name_eq(&getter.key, key) {
204                        return Some(true);
205                    }
206                }
207
208                Prop::Setter(setter) => {
209                    if prop_name_eq(&setter.key, key) {
210                        return Some(true);
211                    }
212                }
213
214                Prop::Method(method) => {
215                    if prop_name_eq(&method.key, key) {
216                        return Some(true);
217                    }
218                }
219
220                #[cfg(swc_ast_unknown)]
221                _ => panic!("unable to access unknown nodes"),
222            },
223
224            _ => {
225                return None;
226            }
227        }
228    }
229
230    // No key was found and there's no uncertainty, meaning the key certainly
231    // doesn't exist
232    Some(false)
233}
234
235impl Pure<'_> {
236    /// Optimizes the following:
237    ///
238    /// - `''[0]`, `''[1]`, `''[-1]` -> `void 0`
239    /// - `''[[]]` -> `void 0`
240    /// - `''["a"]`, `''.a` -> `void 0`
241    ///
242    /// For String, Array and Object literals.
243    /// Special cases like `''.charCodeAt`, `[].push` etc are kept intact.
244    /// In-bound indexes (like `[1][0]`) and `length` are handled in the
245    /// simplifier.
246    ///
247    /// Does nothing if `pristine_globals` is `false`.
248    pub(super) fn optimize_member_expr(
249        &mut self,
250        obj: &mut Expr,
251        prop: &MemberProp,
252    ) -> Option<Expr> {
253        if !self.options.pristine_globals
254            || self
255                .ctx
256                .intersects(Ctx::IS_CALLEE.union(Ctx::IS_LHS_OF_ASSIGN))
257        {
258            return None;
259        }
260
261        /// Taken from `simplify::expr`.
262        ///
263        /// `x.length` is handled as `IndexStr`, since `x.length` calls for
264        /// String and Array are handled in `simplify::expr` (the `length`
265        /// prototype for both of these types cannot be changed).
266        #[derive(Clone, PartialEq)]
267        enum KnownOp {
268            // [a, b][2]
269            //
270            // ({})[1]
271            Index(f64),
272
273            /// ({}).foo
274            ///
275            /// ({}).length
276            IndexStr(Atom),
277        }
278
279        let op = match prop {
280            MemberProp::Ident(IdentName { sym, .. }) => KnownOp::IndexStr(sym.clone()),
281
282            MemberProp::Computed(c) => match &*c.expr {
283                Expr::Lit(Lit::Num(n)) => KnownOp::Index(n.value),
284
285                Expr::Ident(..) => {
286                    return None;
287                }
288
289                _ => {
290                    let Known(s) = c.expr.as_pure_string(self.expr_ctx) else {
291                        return None;
292                    };
293
294                    if let Ok(n) = s.parse::<f64>() {
295                        KnownOp::Index(n)
296                    } else {
297                        KnownOp::IndexStr(Atom::from(s))
298                    }
299                }
300            },
301
302            _ => {
303                return None;
304            }
305        };
306
307        match obj {
308            Expr::Seq(SeqExpr { exprs, span }) => {
309                // Optimize when last value in a SeqExpr is being indexed
310                // while preserving side effects.
311                //
312                // (0, {a: 5}).a
313                //
314                // (0, f(), {a: 5}).a
315                //
316                // (0, f(), [1, 2])[0]
317                //
318                // etc.
319
320                // Try to optimize with obj being the last expr
321                let replacement = self.optimize_member_expr(exprs.last_mut()?, prop)?;
322
323                // Replace last element with replacement
324                let mut exprs: Vec<Box<Expr>> = exprs.drain(..(exprs.len() - 1)).collect();
325                exprs.push(Box::new(replacement));
326
327                Some(SeqExpr { span: *span, exprs }.into())
328            }
329
330            Expr::Lit(Lit::Str(Str { value, span, .. })) => {
331                match op {
332                    KnownOp::Index(idx) => {
333                        if idx.fract() != 0.0 || idx < 0.0 || idx as usize >= value.len() {
334                            Some(*Expr::undefined(*span))
335                        } else {
336                            // idx is in bounds, this is handled in simplify
337                            None
338                        }
339                    }
340
341                    KnownOp::IndexStr(key) => {
342                        if key == "length" {
343                            // handled in simplify::expr
344                            return None;
345                        }
346
347                        if is_string_symbol(key.as_str()) {
348                            None
349                        } else {
350                            Some(*Expr::undefined(*span))
351                        }
352                    }
353                }
354            }
355
356            Expr::Array(ArrayLit { elems, span, .. }) => {
357                // do nothing if spread exists
358                let has_spread = elems.iter().any(|elem| {
359                    elem.as_ref()
360                        .map(|elem| elem.spread.is_some())
361                        .unwrap_or(false)
362                });
363
364                if has_spread {
365                    return None;
366                }
367
368                match op {
369                    KnownOp::Index(idx) => {
370                        if idx >= 0.0 && (idx as usize) < elems.len() && idx.fract() == 0.0 {
371                            // idx is in bounds, handled in simplify
372                            return None;
373                        }
374
375                        // Replacement is certain at this point, and is always undefined
376
377                        // Extract side effects
378                        let mut exprs = Vec::new();
379                        elems.drain(..).flatten().for_each(|elem| {
380                            self.expr_ctx.extract_side_effects_to(&mut exprs, *elem.expr);
381                        });
382
383                        Some(if exprs.is_empty() {
384                            // No side effects, replacement is:
385                            // (0, void 0)
386                            SeqExpr {
387                                span: *span,
388                                exprs: vec![0.into(), Expr::undefined(*span)]
389                            }.into()
390                        } else {
391                            // Side effects exist, replacement is:
392                            // (x(), y(), void 0)
393                            // Where `x()` and `y()` are side effects.
394                            exprs.push(Expr::undefined(*span));
395
396                            SeqExpr {
397                                span: *span,
398                                exprs
399                            }.into()
400                        })
401                    }
402
403                    KnownOp::IndexStr(key) if key != "length" /* handled in simplify */ => {
404                        // If the property is a known symbol, e.g. [].push
405                        let is_known_symbol = is_array_symbol(&key);
406
407                        if is_known_symbol {
408                            // We need to check if this is already optimized as if we don't,
409                            // it'll lead to infinite optimization when the visitor visits
410                            // again.
411                            //
412                            // A known symbol expression is already optimized if all
413                            // non-side effects have been removed.
414                            let optimized_len = elems
415                                .iter()
416                                .flatten()
417                                .filter(|elem| elem.expr.may_have_side_effects(self.expr_ctx))
418                                .count();
419
420                            if optimized_len == elems.len() {
421                                // Already optimized
422                                return None;
423                            }
424                        }
425
426                        // Extract side effects
427                        let mut exprs = Vec::new();
428                        elems.drain(..).flatten().for_each(|elem| {
429                            self.expr_ctx.extract_side_effects_to(&mut exprs, *elem.expr);
430                        });
431
432                        Some(if is_known_symbol {
433                            // [x(), y()].push
434                            MemberExpr {
435                                span: *span,
436                                obj: ArrayLit {
437                                    span: *span,
438                                    elems: exprs
439                                        .into_iter()
440                                        .map(|elem| Some(ExprOrSpread {
441                                            spread: None,
442                                            expr: elem,
443                                        }))
444                                        .collect()
445                                }.into(),
446                                prop: prop.clone(),
447                            }.into()
448                        } else {
449                            let val = Expr::undefined(
450                                *span);
451
452                            if exprs.is_empty() {
453                                // No side effects, replacement is:
454                                // (0, void 0)
455                                SeqExpr {
456                                    span: val.span(),
457                                    exprs: vec![0.into(), val]
458                                }.into()
459                            } else {
460                                // Side effects exist, replacement is:
461                                // (x(), y(), void 0)
462                                // Where `x()` and `y()` are side effects.
463                                exprs.push(val);
464
465                                SeqExpr {
466                                    span: *span,
467                                    exprs
468                                }.into()
469                            }
470                        })
471                    }
472
473                    _ => None
474                }
475            }
476
477            Expr::Object(ObjectLit { props, span }) => {
478                // Do nothing if there are invalid keys.
479                //
480                // Objects with one or more keys that are not literals or identifiers
481                // are impossible to optimize as we don't know for certain if a given
482                // key is actually invalid, e.g. `{[bar()]: 5}`, since we don't know
483                // what `bar()` returns.
484                let contains_invalid_key = props
485                    .iter()
486                    .any(|prop| !matches!(prop, PropOrSpread::Prop(prop) if matches!(&**prop, Prop::KeyValue(kv) if kv.key.is_ident() || kv.key.is_str() || kv.key.is_num())));
487
488                if contains_invalid_key {
489                    return None;
490                }
491
492                // Get key as Atom
493                let key = match op {
494                    KnownOp::Index(i) => Atom::from(i.to_string()),
495                    KnownOp::IndexStr(key) if key != *"yield" => key,
496                    _ => {
497                        return None;
498                    }
499                };
500
501                // Check if key exists
502                let exists = does_key_exist(&key, props);
503                if exists.is_none() || exists.is_some_and(|exists| exists) {
504                    // Valid properties are handled in simplify
505                    return None;
506                }
507
508                let is_known_symbol = is_object_symbol(&key);
509                if is_known_symbol {
510                    // Like with arrays, we need to check if this is already optimized
511                    // before returning Some so we don't end up in an infinite loop.
512                    //
513                    // The same logic with arrays applies; read above.
514                    let optimized_len = props
515                        .iter()
516                        .filter(|prop| {
517                            matches!(prop, PropOrSpread::Prop(prop) if matches!(&**prop, Prop::KeyValue(prop) if prop.value.may_have_side_effects(self.expr_ctx)))
518                        })
519                        .count();
520
521                    if optimized_len == props.len() {
522                        // Already optimized
523                        return None;
524                    }
525                }
526
527                // Can be optimized fully or partially
528                Some(*self.expr_ctx.preserve_effects(
529                    *span,
530                    if is_known_symbol {
531                        // Valid key, e.g. "hasOwnProperty". Replacement:
532                        // (foo(), bar(), {}.hasOwnProperty)
533                        MemberExpr {
534                            span: *span,
535                            obj: ObjectLit {
536                                span: *span,
537                                props: Vec::new(),
538                            }
539                            .into(),
540                            prop: MemberProp::Ident(IdentName::new(key, *span)),
541                        }
542                        .into()
543                    } else {
544                        // Invalid key. Replace with side effects plus `undefined`.
545                        Expr::undefined(*span)
546                    },
547                    props.drain(..).map(|x| match x {
548                        PropOrSpread::Prop(prop) => match *prop {
549                            Prop::KeyValue(kv) => kv.value,
550                            _ => unreachable!(),
551                        },
552                        _ => unreachable!(),
553                    }),
554                ))
555            }
556
557            _ => None,
558        }
559    }
560}