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}