From 570c47a9acd7363428f58dfea2f41c097f73ec1f Mon Sep 17 00:00:00 2001 From: Levi Date: Tue, 2 Jul 2024 13:15:45 +1200 Subject: [PATCH] feat(es/minifier): Handle more indexing expression (#8750) **Related issue:** - Closes #8747 --- Cargo.lock | 1 + ...BindingPatternAndAssignment2.2.minified.js | 2 +- ...gEvaluationOrder(target=es5).2.minified.js | 2 +- ...cturingParameterDeclaration5.2.minified.js | 2 +- .../parserForStatement9.2.minified.js | 4 +- ...teStringInIndexExpressionES6.2.minified.js | 1 - ...typeFromPropertyAssignment35.2.minified.js | 2 +- crates/swc_ecma_minifier/Cargo.toml | 9 +- .../src/compress/pure/evaluate.rs | 26 + .../src/compress/pure/member_expr.rs | 555 ++++++++++++++++++ .../src/compress/pure/mod.rs | 18 + .../tests/fixture/member_expr/array/input.js | 62 ++ .../tests/fixture/member_expr/array/output.js | 1 + .../member_expr/array_side_effects/input.js | 22 + .../member_expr/array_side_effects/output.js | 7 + .../fixture/member_expr/assignment/input.js | 1 + .../fixture/member_expr/assignment/output.js | 1 + .../tests/fixture/member_expr/callee/input.js | 6 + .../fixture/member_expr/callee/output.js | 5 + .../tests/fixture/member_expr/config.json | 6 + .../tests/fixture/member_expr/object/input.js | 20 + .../fixture/member_expr/object/output.js | 1 + .../member_expr/object_side_effects/input.js | 17 + .../member_expr/object_side_effects/output.js | 1 + .../tests/fixture/member_expr/seq/input.js | 2 + .../tests/fixture/member_expr/seq/output.js | 1 + .../tests/fixture/member_expr/string/input.js | 72 +++ .../fixture/member_expr/string/output.js | 1 + .../output.js | 2 +- .../compress/arrow/object_parens/output.js | 14 +- .../compress/comparing/issue_2857_6/output.js | 18 +- .../compress/evaluate/prop_function/output.js | 14 +- .../compress/evaluate/unsafe_array/output.js | 19 +- .../evaluate/unsafe_array_bad_index/output.js | 2 +- .../unsafe_string_bad_index/output.js | 2 +- .../array_literal_with_spread_2a/output.js | 16 +- .../array_literal_with_spread_3a/output.js | 20 +- .../array_literal_with_spread_3b/output.js | 5 +- .../array_literal_with_spread_4a/output.js | 28 +- .../compress/harmony/issue_2345/output.js | 10 +- .../compress/issue_t50/issue_t50/config.json | 2 +- .../compress/issue_t50/issue_t50/output.js | 8 +- .../issue_t50/issue_t50_const/config.json | 2 +- .../issue_t50/issue_t50_const/output.js | 8 +- .../issue_t50/issue_t50_let/config.json | 2 +- .../issue_t50/issue_t50_let/output.js | 8 +- .../evaluate_array_length/output.js | 12 +- .../compress/properties/lhs_prop_1/output.js | 4 +- .../reduce_vars/issue_2450_5/output.js | 8 +- .../reduce_vars/issue_3042_1/output.js | 5 +- .../reduce_vars/issue_3042_2/output.js | 11 +- .../compress/reduce_vars/obj_for_1/output.js | 4 +- .../src/simplify/expr/mod.rs | 373 +++++++----- .../src/simplify/expr/tests.rs | 62 +- crates/swc_ecma_utils/src/lib.rs | 12 +- 55 files changed, 1245 insertions(+), 274 deletions(-) create mode 100644 crates/swc_ecma_minifier/src/compress/pure/member_expr.rs create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/array/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/array/output.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/array_side_effects/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/array_side_effects/output.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/assignment/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/assignment/output.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/callee/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/callee/output.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/config.json create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/object/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/object/output.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/object_side_effects/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/object_side_effects/output.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/seq/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/seq/output.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/string/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/member_expr/string/output.js diff --git a/Cargo.lock b/Cargo.lock index da049ad3c638..59b6d8cb4c56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4422,6 +4422,7 @@ dependencies = [ "num_cpus", "once_cell", "parking_lot", + "phf", "pretty_assertions", "radix_fmt", "rayon", diff --git a/crates/swc/tests/tsc-references/destructuringArrayBindingPatternAndAssignment2.2.minified.js b/crates/swc/tests/tsc-references/destructuringArrayBindingPatternAndAssignment2.2.minified.js index 645f448a283c..d7a68a59e955 100644 --- a/crates/swc/tests/tsc-references/destructuringArrayBindingPatternAndAssignment2.2.minified.js +++ b/crates/swc/tests/tsc-references/destructuringArrayBindingPatternAndAssignment2.2.minified.js @@ -1,7 +1,7 @@ //// [destructuringArrayBindingPatternAndAssignment2.ts] import { _ as _sliced_to_array } from "@swc/helpers/_/_sliced_to_array"; import { _ as _to_consumable_array } from "@swc/helpers/_/_to_consumable_array"; -var _ref_1 = (_sliced_to_array([][0], 1)[0], _sliced_to_array([][1], 1)); +var _ref_1 = (_sliced_to_array(void 0, 1)[0], _sliced_to_array(void 0, 1)); _sliced_to_array(_ref_1[0], 1)[0]; var _undefined = _sliced_to_array(void 0, 2), _undefined_1 = (_sliced_to_array(_undefined[0], 1)[0], _sliced_to_array(_undefined[1], 1)); _sliced_to_array(_undefined_1[0], 1)[0]; diff --git a/crates/swc/tests/tsc-references/destructuringEvaluationOrder(target=es5).2.minified.js b/crates/swc/tests/tsc-references/destructuringEvaluationOrder(target=es5).2.minified.js index bb915583548d..8d0a296b2d5e 100644 --- a/crates/swc/tests/tsc-references/destructuringEvaluationOrder(target=es5).2.minified.js +++ b/crates/swc/tests/tsc-references/destructuringEvaluationOrder(target=es5).2.minified.js @@ -6,7 +6,7 @@ import { _ as _sliced_to_array } from "@swc/helpers/_/_sliced_to_array"; import { _ as _to_property_key } from "@swc/helpers/_/_to_property_key"; var trace = [], order = function(n) { return trace.push(n); -}, tmp = [][0]; +}, tmp = void 0; (void 0 === tmp ? order(0) : tmp)[order(1)]; var tmp1 = {}; (void 0 === tmp1 ? order(0) : tmp1)[order(1)]; diff --git a/crates/swc/tests/tsc-references/destructuringParameterDeclaration5.2.minified.js b/crates/swc/tests/tsc-references/destructuringParameterDeclaration5.2.minified.js index 11948df45f64..6b80395c471e 100644 --- a/crates/swc/tests/tsc-references/destructuringParameterDeclaration5.2.minified.js +++ b/crates/swc/tests/tsc-references/destructuringParameterDeclaration5.2.minified.js @@ -42,4 +42,4 @@ new Class(), d0({ y: new SubClass() }).y, ({ y: new Class() -}).y, ({}).y; +}).y; diff --git a/crates/swc/tests/tsc-references/parserForStatement9.2.minified.js b/crates/swc/tests/tsc-references/parserForStatement9.2.minified.js index 60c2ce4a14e0..356823ddeb48 100644 --- a/crates/swc/tests/tsc-references/parserForStatement9.2.minified.js +++ b/crates/swc/tests/tsc-references/parserForStatement9.2.minified.js @@ -1,3 +1,3 @@ //// [parserForStatement9.ts] -for(var tmp = [][0], x = void 0 === tmp ? ('a' in {}) : tmp; !x; x = !x)console.log(x); -for(var _ref_x = {}.x, x1 = void 0 === _ref_x ? ('a' in {}) : _ref_x; !x1; x1 = !x1)console.log(x1); +for(var tmp = void 0, x = void 0 === tmp ? ('a' in {}) : tmp; !x; x = !x)console.log(x); +for(var _ref_x = void 0, x1 = void 0 === _ref_x ? ('a' in {}) : _ref_x; !x1; x1 = !x1)console.log(x1); diff --git a/crates/swc/tests/tsc-references/templateStringInIndexExpressionES6.2.minified.js b/crates/swc/tests/tsc-references/templateStringInIndexExpressionES6.2.minified.js index 4d27068f2f2c..e76fe481e054 100644 --- a/crates/swc/tests/tsc-references/templateStringInIndexExpressionES6.2.minified.js +++ b/crates/swc/tests/tsc-references/templateStringInIndexExpressionES6.2.minified.js @@ -1,2 +1 @@ //// [templateStringInIndexExpressionES6.ts] -"abc0abc"["0"]; diff --git a/crates/swc/tests/tsc-references/typeFromPropertyAssignment35.2.minified.js b/crates/swc/tests/tsc-references/typeFromPropertyAssignment35.2.minified.js index 8cd50b5afeaf..1e93b85d4594 100644 --- a/crates/swc/tests/tsc-references/typeFromPropertyAssignment35.2.minified.js +++ b/crates/swc/tests/tsc-references/typeFromPropertyAssignment35.2.minified.js @@ -5,4 +5,4 @@ Emu.D = function _class() { _class_call_check(this, _class), this._model = 1; }; //// [second.js] -({}).D._wrapperInstance; +(void 0)._wrapperInstance; diff --git a/crates/swc_ecma_minifier/Cargo.toml b/crates/swc_ecma_minifier/Cargo.toml index 5dbd9e453ab1..06f2d990f058 100644 --- a/crates/swc_ecma_minifier/Cargo.toml +++ b/crates/swc_ecma_minifier/Cargo.toml @@ -9,9 +9,9 @@ name = "swc_ecma_minifier" repository = "https://github.com/swc-project/swc.git" version = "0.197.1" - [package.metadata.docs.rs] - all-features = true - rustdoc-args = ["--cfg", "docsrs"] +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] [lib] bench = false @@ -49,6 +49,7 @@ ryu-js = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tracing = { workspace = true } +phf = { workspace = true } swc_atoms = { version = "0.6.5", path = "../swc_atoms" } swc_common = { version = "0.34.0", path = "../swc_common" } @@ -80,4 +81,4 @@ testing = { version = "0.36.0", path = "../testing" } [[bench]] harness = false -name = "full" +name = "full" \ No newline at end of file diff --git a/crates/swc_ecma_minifier/src/compress/pure/evaluate.rs b/crates/swc_ecma_minifier/src/compress/pure/evaluate.rs index 897133d17055..d65833dfda50 100644 --- a/crates/swc_ecma_minifier/src/compress/pure/evaluate.rs +++ b/crates/swc_ecma_minifier/src/compress/pure/evaluate.rs @@ -2,6 +2,8 @@ use radix_fmt::Radix; use swc_common::{util::take::Take, Spanned, SyntaxContext}; use swc_ecma_ast::*; use swc_ecma_utils::{number::ToJsString, ExprExt, IsEmpty, Value}; +#[cfg(feature = "debug")] +use {crate::debug::dump, tracing::debug}; use super::Pure; use crate::compress::util::{eval_as_number, is_pure_undefined_or_null}; @@ -639,6 +641,30 @@ impl Pure<'_> { } } + pub(super) fn eval_member_expr(&mut self, e: &mut Expr) { + let member_expr = match e { + Expr::Member(x) => x, + _ => return, + }; + + #[cfg(feature = "debug")] + debug!( + "before: optimize_member_expr: {}", + dump(&*member_expr, false) + ); + + if let Some(replacement) = + self.optimize_member_expr(&mut member_expr.obj, &member_expr.prop) + { + *e = replacement; + self.changed = true; + report_change!("member_expr: Optimized member expression"); + + #[cfg(feature = "debug")] + debug!("after: optimize_member_expr: {}", dump(&*e, false)); + } + } + fn eval_trivial_two(&mut self, a: &Expr, b: &mut Expr) { if let Expr::Assign(AssignExpr { left: a_left, diff --git a/crates/swc_ecma_minifier/src/compress/pure/member_expr.rs b/crates/swc_ecma_minifier/src/compress/pure/member_expr.rs new file mode 100644 index 000000000000..22b57e09c253 --- /dev/null +++ b/crates/swc_ecma_minifier/src/compress/pure/member_expr.rs @@ -0,0 +1,555 @@ +use phf::phf_set; +use swc_atoms::{Atom, JsWord}; +use swc_common::Spanned; +use swc_ecma_ast::{ + ArrayLit, Expr, ExprOrSpread, Ident, Lit, MemberExpr, MemberProp, ObjectLit, Prop, + PropOrSpread, SeqExpr, Str, +}; +use swc_ecma_utils::{prop_name_eq, ExprExt, Known}; + +use super::Pure; + +/// Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array +static ARRAY_SYMBOLS: phf::Set<&str> = phf_set!( + // Constructor + "constructor", + // Properties + "length", + // Methods + "at", + "concat", + "copyWithin", + "entries", + "every", + "fill", + "filter", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "pop", + "push", + "reduce", + "reduceRight", + "reverse", + "shift", + "slice", + "some", + "sort", + "splice", + "toLocaleString", + "toReversed", + "toSorted", + "toSpliced", + "toString", + "unshift", + "values", + "with" +); + +/// Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String +static STRING_SYMBOLS: phf::Set<&str> = phf_set!( + // Constructor + "constructor", + // Properties + "length", + // Methods + "anchor", + "at", + "big", + "blink", + "bold", + "charAt", + "charCodeAt", + "codePointAt", + "concat", + "endsWith", + "fixed", + "fontcolor", + "fontsize", + "includes", + "indexOf", + "isWellFormed", + "italics", + "lastIndexOf", + "link", + "localeCompare", + "match", + "matchAll", + "normalize", + "padEnd", + "padStart", + "repeat", + "replace", + "replaceAll", + "search", + "slice", + "small", + "split", + "startsWith", + "strike", + "sub", + "substr", + "substring", + "sup", + "toLocaleLowerCase", + "toLocaleUpperCase", + "toLowerCase", + "toString", + "toUpperCase", + "toWellFormed", + "trim", + "trimEnd", + "trimStart", + "valueOf" +); + +/// Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object +static OBJECT_SYMBOLS: phf::Set<&str> = phf_set!( + // Constructor + "constructor", + // Properties + "__proto__", + // Methods + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", + // removed, but kept in as these are often checked and polyfilled + "watch", + "unwatch" +); + +fn is_object_symbol(sym: &str) -> bool { + OBJECT_SYMBOLS.contains(sym) +} + +fn is_array_symbol(sym: &str) -> bool { + // Inherits: Object + ARRAY_SYMBOLS.contains(sym) || is_object_symbol(sym) +} + +fn is_string_symbol(sym: &str) -> bool { + // Inherits: Object + STRING_SYMBOLS.contains(sym) || is_object_symbol(sym) +} + +/// Checks if the given key exists in the given properties, taking the +/// `__proto__` property and order of keys into account (the order of keys +/// matters for nested `__proto__` properties). +/// +/// Returns `None` if the key's existence is uncertain, or `Some` if it is +/// certain. +/// +/// A key's existence is uncertain if a `__proto__` property exists and the +/// value is non-literal. +fn does_key_exist(key: &str, props: &Vec) -> Option { + for prop in props { + match prop { + PropOrSpread::Prop(prop) => match &**prop { + Prop::Shorthand(ident) => { + if ident.sym == key { + return Some(true); + } + } + + Prop::KeyValue(prop) => { + if key != "__proto__" && prop_name_eq(&prop.key, "__proto__") { + // If __proto__ is defined, we need to check the contents of it, + // as well as any nested __proto__ objects + if let Some(object) = prop.value.as_object() { + // __proto__ is an ObjectLiteral, check if key exists in it + let exists = does_key_exist(key, &object.props); + if exists.is_none() { + return None; + } else if exists.is_some_and(|exists| exists) { + return Some(true); + } + } else { + // __proto__ is not a literal, it is impossible to know if the + // key exists or not + return None; + } + } else { + // Normal key + if prop_name_eq(&prop.key, key) { + return Some(true); + } + } + } + + // invalid + Prop::Assign(_) => { + return None; + } + + Prop::Getter(getter) => { + if prop_name_eq(&getter.key, key) { + return Some(true); + } + } + + Prop::Setter(setter) => { + if prop_name_eq(&setter.key, key) { + return Some(true); + } + } + + Prop::Method(method) => { + if prop_name_eq(&method.key, key) { + return Some(true); + } + } + }, + + _ => { + return None; + } + } + } + + // No key was found and there's no uncertainty, meaning the key certainly + // doesn't exist + Some(false) +} + +impl Pure<'_> { + /// Optimizes the following: + /// + /// - `''[0]`, `''[1]`, `''[-1]` -> `void 0` + /// - `''[[]]` -> `void 0` + /// - `''["a"]`, `''.a` -> `void 0` + /// + /// For String, Array and Object literals. + /// Special cases like `''.charCodeAt`, `[].push` etc are kept intact. + /// In-bound indexes (like `[1][0]`) and `length` are handled in the + /// simplifier. + /// + /// Does nothing if `pristine_globals` is `false`. + pub(super) fn optimize_member_expr( + &mut self, + obj: &mut Expr, + prop: &MemberProp, + ) -> Option { + if !self.options.pristine_globals || self.ctx.is_lhs_of_assign || self.ctx.is_callee { + return None; + } + + /// Taken from `simplify::expr`. + /// + /// `x.length` is handled as `IndexStr`, since `x.length` calls for + /// String and Array are handled in `simplify::expr` (the `length` + /// prototype for both of these types cannot be changed). + #[derive(Clone, PartialEq)] + enum KnownOp { + // [a, b][2] + // + // ({})[1] + Index(f64), + + /// ({}).foo + /// + /// ({}).length + IndexStr(JsWord), + } + + let op = match prop { + MemberProp::Ident(Ident { sym, .. }) => { + if self.ctx.is_callee { + return None; + } + + KnownOp::IndexStr(sym.clone()) + } + + MemberProp::Computed(c) => match &*c.expr { + Expr::Lit(Lit::Num(n)) => KnownOp::Index(n.value), + + Expr::Ident(..) => { + return None; + } + + _ => { + let Known(s) = c.expr.as_pure_string(&self.expr_ctx) else { + return None; + }; + + if let Ok(n) = s.parse::() { + KnownOp::Index(n) + } else { + KnownOp::IndexStr(JsWord::from(s)) + } + } + }, + + _ => { + return None; + } + }; + + match obj { + Expr::Seq(SeqExpr { exprs, span }) => { + // Optimize when last value in a SeqExpr is being indexed + // while preserving side effects. + // + // (0, {a: 5}).a + // + // (0, f(), {a: 5}).a + // + // (0, f(), [1, 2])[0] + // + // etc. + + // Try to optimize with obj being the last expr + let replacement = self.optimize_member_expr(exprs.last_mut()?, prop)?; + + // Replace last element with replacement + let mut exprs: Vec> = exprs.drain(..(exprs.len() - 1)).collect(); + exprs.push(Box::new(replacement)); + + Some(Expr::Seq(SeqExpr { span: *span, exprs })) + } + + Expr::Lit(Lit::Str(Str { value, span, .. })) => { + match op { + KnownOp::Index(idx) => { + if idx.fract() != 0.0 || idx < 0.0 || idx as usize >= value.len() { + Some(*Expr::undefined(*span)) + } else { + // idx is in bounds, this is handled in simplify + None + } + } + + KnownOp::IndexStr(key) => { + if key == "length" { + // handled in simplify::expr + return None; + } + + if is_string_symbol(key.as_str()) { + None + } else { + Some(*Expr::undefined(*span)) + } + } + } + } + + Expr::Array(ArrayLit { elems, span, .. }) => { + // do nothing if spread exists + let has_spread = elems.iter().any(|elem| { + elem.as_ref() + .map(|elem| elem.spread.is_some()) + .unwrap_or(false) + }); + + if has_spread { + return None; + } + + match op { + KnownOp::Index(idx) => { + if idx >= 0.0 && (idx as usize) < elems.len() && idx.fract() == 0.0 { + // idx is in bounds, handled in simplify + return None; + } + + // Replacement is certain at this point, and is always undefined + + // Extract side effects + let mut exprs = vec![]; + elems.drain(..).flatten().for_each(|elem| { + self.expr_ctx.extract_side_effects_to(&mut exprs, *elem.expr); + }); + + Some(if exprs.is_empty() { + // No side effects, replacement is: + // (0, void 0) + Expr::Seq(SeqExpr { + span: *span, + exprs: vec![0.into(), Expr::undefined(*span)] + }) + } else { + // Side effects exist, replacement is: + // (x(), y(), void 0) + // Where `x()` and `y()` are side effects. + exprs.push(Expr::undefined(*span)); + + Expr::Seq(SeqExpr { + span: *span, + exprs + }) + }) + } + + KnownOp::IndexStr(key) if key != "length" /* handled in simplify */ => { + // If the property is a known symbol, e.g. [].push + let is_known_symbol = is_array_symbol(&key); + + if is_known_symbol { + // We need to check if this is already optimized as if we don't, + // it'll lead to infinite optimization when the visitor visits + // again. + // + // A known symbol expression is already optimized if all + // non-side effects have been removed. + let optimized_len = elems + .iter() + .flatten() + .filter(|elem| elem.expr.may_have_side_effects(&self.expr_ctx)) + .count(); + + if optimized_len == elems.len() { + // Already optimized + return None; + } + } + + // Extract side effects + let mut exprs = vec![]; + elems.drain(..).flatten().for_each(|elem| { + self.expr_ctx.extract_side_effects_to(&mut exprs, *elem.expr); + }); + + Some(if is_known_symbol { + // [x(), y()].push + Expr::Member(MemberExpr { + span: *span, + obj: Box::new(Expr::Array(ArrayLit { + span: *span, + elems: exprs + .into_iter() + .map(|elem| Some(ExprOrSpread { + spread: None, + expr: elem, + })) + .collect() + })), + prop: prop.clone(), + }) + } else { + let val = Expr::undefined(*span); + + if exprs.is_empty() { + // No side effects, replacement is: + // (0, void 0) + Expr::Seq(SeqExpr { + span: val.span(), + exprs: vec![0.into(), val] + }) + } else { + // Side effects exist, replacement is: + // (x(), y(), void 0) + // Where `x()` and `y()` are side effects. + exprs.push(val); + + Expr::Seq(SeqExpr { + span: *span, + exprs + }) + } + }) + } + + _ => None + } + } + + Expr::Object(ObjectLit { props, span }) => { + // Do nothing if there are invalid keys. + // + // Objects with one or more keys that are not literals or identifiers + // are impossible to optimize as we don't know for certain if a given + // key is actually invalid, e.g. `{[bar()]: 5}`, since we don't know + // what `bar()` returns. + let contains_invalid_key = props + .iter() + .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()))); + + if contains_invalid_key { + return None; + } + + // Get key as Atom + let key = match op { + KnownOp::Index(i) => Atom::from(i.to_string()), + KnownOp::IndexStr(key) if key != *"yield" => key, + _ => { + return None; + } + }; + + // Check if key exists + let exists = does_key_exist(&key, props); + if exists.is_none() || exists.is_some_and(|exists| exists) { + // Valid properties are handled in simplify + return None; + } + + let is_known_symbol = is_object_symbol(&key); + if is_known_symbol { + // Like with arrays, we need to check if this is already optimized + // before returning Some so we don't end up in an infinite loop. + // + // The same logic with arrays applies; read above. + let optimized_len = props + .iter() + .filter(|prop| { + matches!(prop, PropOrSpread::Prop(prop) if matches!(&**prop, Prop::KeyValue(prop) if prop.value.may_have_side_effects(&self.expr_ctx))) + }) + .count(); + + if optimized_len == props.len() { + // Already optimized + return None; + } + } + + // Can be optimized fully or partially + Some(self.expr_ctx.preserve_effects( + *span, + if is_known_symbol { + // Valid key, e.g. "hasOwnProperty". Replacement: + // (foo(), bar(), {}.hasOwnProperty) + Expr::Member(MemberExpr { + span: *span, + obj: Box::new(Expr::Object(ObjectLit { + span: *span, + props: vec![], + })), + prop: MemberProp::Ident(Ident::new(key, *span)), + }) + } else { + // Invalid key. Replace with side effects plus `undefined`. + *Expr::undefined(*span) + }, + props.drain(..).map(|x| match x { + PropOrSpread::Prop(prop) => match *prop { + Prop::KeyValue(kv) => kv.value, + _ => unreachable!(), + }, + _ => unreachable!(), + }), + )) + } + + _ => None, + } + } +} diff --git a/crates/swc_ecma_minifier/src/compress/pure/mod.rs b/crates/swc_ecma_minifier/src/compress/pure/mod.rs index 653683f7e253..4a4dcfc90dbf 100644 --- a/crates/swc_ecma_minifier/src/compress/pure/mod.rs +++ b/crates/swc_ecma_minifier/src/compress/pure/mod.rs @@ -29,6 +29,7 @@ mod drop_console; mod evaluate; mod if_return; mod loops; +mod member_expr; mod misc; mod numbers; mod properties; @@ -305,6 +306,20 @@ impl VisitMut for Pure<'_> { self.drop_arguments_of_symbol_call(e); } + fn visit_mut_opt_call(&mut self, opt_call: &mut OptCall) { + { + let ctx = Ctx { + is_callee: true, + ..self.ctx + }; + opt_call.callee.visit_mut_with(&mut *self.with_ctx(ctx)); + } + + opt_call.args.visit_mut_with(self); + + self.eval_spread_array(&mut opt_call.args); + } + fn visit_mut_class_member(&mut self, m: &mut ClassMember) { m.visit_mut_children_with(self); @@ -578,6 +593,8 @@ impl VisitMut for Pure<'_> { if e.is_seq() { debug_assert_valid(e); } + + self.eval_member_expr(e); } fn visit_mut_expr_or_spreads(&mut self, nodes: &mut Vec) { @@ -685,6 +702,7 @@ impl VisitMut for Pure<'_> { fn visit_mut_member_expr(&mut self, e: &mut MemberExpr) { e.obj.visit_mut_with(self); + if let MemberProp::Computed(c) = &mut e.prop { c.visit_mut_with(self); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/array/input.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/array/input.js new file mode 100644 index 000000000000..d9da2f27b3b2 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/array/input.js @@ -0,0 +1,62 @@ +// Invalid +f([][0]); +f([][1]); +f([][-1]); + +f([].invalid); +f([]["invalid"]); +f([][[]]); +f([][0+[]]); + +// Object symbols +[].constructor; +[].__proto__; +[].__defineGetter__; +[].__defineSetter__; +[].__lookupGetter__; +[].__lookupSetter__; +[].hasOwnProperty; +[].isPrototypeOf; +[].propertyIsEnumerable; +[].toLocaleString; +[].toString; +[].valueOf; + +// Array symbols +[].length; +[].at; +[].concat; +[].copyWithin; +[].entries; +[].every; +[].fill; +[].filter; +[].find; +[].findIndex; +[].findLast; +[].findLastIndex; +[].flat; +[].flatMap; +[].forEach; +[].includes; +[].indexOf; +[].join; +[].keys; +[].lastIndexOf; +[].map; +[].pop; +[].push; +[].reduce; +[].reduceRight; +[].reverse; +[].shift; +[].slice; +[].some; +[].sort; +[].splice; +[].toReversed; +[].toSorted; +[].toSpliced; +[].unshift; +[].values; +[].with; diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/array/output.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/array/output.js new file mode 100644 index 000000000000..525a610dd6d6 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/array/output.js @@ -0,0 +1 @@ +f(void 0), f(void 0), f(void 0), f(void 0), f(void 0), f(void 0), f(void 0), [].constructor, [].__proto__, [].__defineGetter__, [].__defineSetter__, [].__lookupGetter__, [].__lookupSetter__, [].hasOwnProperty, [].isPrototypeOf, [].propertyIsEnumerable, [].toLocaleString, [].toString, [].valueOf, [].at, [].concat, [].copyWithin, [].entries, [].every, [].fill, [].filter, [].find, [].findIndex, [].findLast, [].findLastIndex, [].flat, [].flatMap, [].forEach, [].includes, [].indexOf, [].join, [].keys, [].lastIndexOf, [].map, [].pop, [].push, [].reduce, [].reduceRight, [].reverse, [].shift, [].slice, [].some, [].sort, [].splice, [].toReversed, [].toSorted, [].toSpliced, [].unshift, [].values, [].with; diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/array_side_effects/input.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/array_side_effects/input.js new file mode 100644 index 000000000000..0a5ece99c9a9 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/array_side_effects/input.js @@ -0,0 +1,22 @@ +// Out of bounds +f([][-1]); +f([][1]); +f([][[]]); +f([][0+[]]); + +f([x(), 2, 'a', 1+1, y()][-1]); +f([x(), 2, 'a', 1+1, y()][10]); + +// Invalid property +f([].invalid); +f([]["invalid"]); + +f([x(), 2, 'a', 1+1, y()].invalid); +f([x(), 2, 'a', 1+1, y()]["invalid"]); + +// Valid property +f([].push); +f([]["push"]); + +f([x(), 2, 'a', 1+1, y()].push); +f([x(), 2, 'a', 1+1, y()]["push"]); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/array_side_effects/output.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/array_side_effects/output.js new file mode 100644 index 000000000000..0eb89962e0ab --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/array_side_effects/output.js @@ -0,0 +1,7 @@ +f(void 0), f(void 0), f(void 0), f(void 0), f((x(), void y())), f((x(), void y())), f(void 0), f(void 0), f((x(), void y())), f((x(), void y())), f([].push), f([].push), f([ + x(), + y() +].push), f([ + x(), + y() +].push); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/assignment/input.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/assignment/input.js new file mode 100644 index 000000000000..7e5c43b52f98 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/assignment/input.js @@ -0,0 +1 @@ +f({}.x = 5); \ No newline at end of file diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/assignment/output.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/assignment/output.js new file mode 100644 index 000000000000..cec899c133d3 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/assignment/output.js @@ -0,0 +1 @@ +f({}.x = 5); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/callee/input.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/callee/input.js new file mode 100644 index 000000000000..3042394fa0fd --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/callee/input.js @@ -0,0 +1,6 @@ +try { + const foo = {}; + foo?.bar.baz?.(); +} catch (e) { + console.log('PASS'); +} diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/callee/output.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/callee/output.js new file mode 100644 index 000000000000..089d30f03851 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/callee/output.js @@ -0,0 +1,5 @@ +try { + ({}).bar.baz?.(); +} catch (e) { + console.log('PASS'); +} diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/config.json b/crates/swc_ecma_minifier/tests/fixture/member_expr/config.json new file mode 100644 index 000000000000..f91482cbb1d1 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/config.json @@ -0,0 +1,6 @@ +{ + "defaults": true, + "toplevel": true, + "unused": false, + "dead_code": false +} diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/object/input.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/object/input.js new file mode 100644 index 000000000000..1eee3862fcb7 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/object/input.js @@ -0,0 +1,20 @@ +// Invalid +({}[0]); +({}.invalid); +({}["invalid"]); +({}[[]]); +({}[0+[]]); + +// Object symbols +({}.constructor); +({}.__proto__); +({}.__defineGetter__); +({}.__defineSetter__); +({}.__lookupGetter__); +({}.__lookupSetter__); +({}.hasOwnProperty); +({}.isPrototypeOf); +({}.propertyIsEnumerable); +({}.toLocaleString); +({}.toString); +({}.valueOf); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/object/output.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/object/output.js new file mode 100644 index 000000000000..e82e2350aece --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/object/output.js @@ -0,0 +1 @@ +({}).constructor, ({}).__proto__, ({}).__defineGetter__, ({}).__defineSetter__, ({}).__lookupGetter__, ({}).__lookupSetter__, ({}).hasOwnProperty, ({}).isPrototypeOf, ({}).propertyIsEnumerable, ({}).toLocaleString, ({}).toString, ({}).valueOf; diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/object_side_effects/input.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/object_side_effects/input.js new file mode 100644 index 000000000000..9191ea9f521c --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/object_side_effects/input.js @@ -0,0 +1,17 @@ +// foo(), {}.__proto__ +f({a: foo(), b: 5}.__proto__); + +// foo(), bar(), undefined +f({a: foo(), b: bar()}.invalid); + +// foo1(), bar(), baz(), foo2(), undefined +f({ + a: foo1(), + b: { + a: bar(), + b: { + a: baz() + }, + c: foo2() + } +}.invalid); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/object_side_effects/output.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/object_side_effects/output.js new file mode 100644 index 000000000000..4dcc1c3bc6a1 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/object_side_effects/output.js @@ -0,0 +1 @@ +f((foo(), ({}).__proto__)), f((foo(), void bar())), f((foo1(), bar(), baz(), void foo2())); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/seq/input.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/seq/input.js new file mode 100644 index 000000000000..92a240ed9344 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/seq/input.js @@ -0,0 +1,2 @@ +console.log((f(), [2, 4])[5]); +console.log((f(), {b: 2}).a); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/seq/output.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/seq/output.js new file mode 100644 index 000000000000..883367d7963a --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/seq/output.js @@ -0,0 +1 @@ +console.log(void f()), console.log(void f()); diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/string/input.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/string/input.js new file mode 100644 index 000000000000..95bb389c82e2 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/string/input.js @@ -0,0 +1,72 @@ +// Invalid +''[0]; +''[1]; +''[-1]; + +''.invalid; +''["invalid"]; +''[[]]; +''[0+[]]; + +// Object symbols +''.constructor; +''.__proto__; +''.__defineGetter__; +''.__defineSetter__; +''.__lookupGetter__; +''.__lookupSetter__; +''.hasOwnProperty; +''.isPrototypeOf; +''.propertyIsEnumerable; +''.toLocaleString; +''.toString; +''.valueOf; + +// String symbols +''.length; +''.anchor; +''.at; +''.big; +''.blink; +''.bold; +''.charAt; +''.charCodeAt; +''.codePointAt; +''.concat; +''.endsWith; +''.fixed; +''.fontcolor; +''.fontsize; +''.includes; +''.indexOf; +''.isWellFormed; +''.italics; +''.lastIndexOf; +''.link; +''.localeCompare; +''.match; +''.matchAll; +''.normalize; +''.padEnd; +''.padStart; +''.repeat; +''.replace; +''.replaceAll; +''.search; +''.slice; +''.small; +''.split; +''.startsWith; +''.strike; +''.sub; +''.substr; +''.substring; +''.sup; +''.toLocaleLowerCase; +''.toLocaleUpperCase; +''.toLowerCase; +''.toUpperCase; +''.toWellFormed; +''.trim; +''.trimEnd; +''.trimStart; diff --git a/crates/swc_ecma_minifier/tests/fixture/member_expr/string/output.js b/crates/swc_ecma_minifier/tests/fixture/member_expr/string/output.js new file mode 100644 index 000000000000..d0c8fc31bfac --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/member_expr/string/output.js @@ -0,0 +1 @@ +''.constructor, ''.__proto__, ''.__defineGetter__, ''.__defineSetter__, ''.__lookupGetter__, ''.__lookupSetter__, ''.hasOwnProperty, ''.isPrototypeOf, ''.propertyIsEnumerable, ''.toLocaleString, ''.toString, ''.valueOf, ''.anchor, ''.at, ''.big, ''.blink, ''.bold, ''.charAt, ''.charCodeAt, ''.codePointAt, ''.concat, ''.endsWith, ''.fixed, ''.fontcolor, ''.fontsize, ''.includes, ''.indexOf, ''.isWellFormed, ''.italics, ''.lastIndexOf, ''.link, ''.localeCompare, ''.match, ''.matchAll, ''.normalize, ''.padEnd, ''.padStart, ''.repeat, ''.replace, ''.replaceAll, ''.search, ''.slice, ''.small, ''.split, ''.startsWith, ''.strike, ''.sub, ''.substr, ''.substring, ''.sup, ''.toLocaleLowerCase, ''.toLocaleUpperCase, ''.toLowerCase, ''.toUpperCase, ''.toWellFormed, ''.trim, ''.trimEnd, ''.trimStart; diff --git a/crates/swc_ecma_minifier/tests/full/size/e4dd4373c192c6fe2fc929bc55d1ed625b974338/output.js b/crates/swc_ecma_minifier/tests/full/size/e4dd4373c192c6fe2fc929bc55d1ed625b974338/output.js index 91bdb072f148..e6c92b799ba1 100644 --- a/crates/swc_ecma_minifier/tests/full/size/e4dd4373c192c6fe2fc929bc55d1ed625b974338/output.js +++ b/crates/swc_ecma_minifier/tests/full/size/e4dd4373c192c6fe2fc929bc55d1ed625b974338/output.js @@ -1 +1 @@ -[]({c(){a=({}).b}}); +[]({c(){a=void 0}}); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/arrow/object_parens/output.js b/crates/swc_ecma_minifier/tests/terser/compress/arrow/object_parens/output.js index d321beba44fb..e7333cadee7a 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/arrow/object_parens/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/arrow/object_parens/output.js @@ -1,9 +1,9 @@ -() => ({}); -() => ({}); -() => ({}[0]); -() => 1; -() => 1; -() => 2; -() => { +()=>({}); +()=>({}); +()=>void 0; +()=>1; +()=>1; +()=>2; +()=>{ foo(); }; diff --git a/crates/swc_ecma_minifier/tests/terser/compress/comparing/issue_2857_6/output.js b/crates/swc_ecma_minifier/tests/terser/compress/comparing/issue_2857_6/output.js index dd9f7ac8ecc8..b0fa090b0e35 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/comparing/issue_2857_6/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/comparing/issue_2857_6/output.js @@ -1,11 +1,11 @@ function f(a) { - if (null == {}.b) return void 0 !== a.b && null !== a.b; + if (true) return void 0 !== a.b && null !== a.b; } -console.log( - f({ - a: [null], - get b() { - return this.a.shift(); - }, - }) -); +console.log(f({ + a: [ + null + ], + get b () { + return this.a.shift(); + } +})); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/evaluate/prop_function/output.js b/crates/swc_ecma_minifier/tests/terser/compress/evaluate/prop_function/output.js index 41b7f00b639c..6baf59ba132b 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/evaluate/prop_function/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/evaluate/prop_function/output.js @@ -1,6 +1,8 @@ -console.log( - { a: { b: 1 }, b: function () {} } + 1, - { b: 1 } + 1, - function () {} + 1, - 2 -); +console.log({ + a: { + b: 1 + }, + b: function() {} +} + 1, { + b: 1 +} + 1, function() {} + 1, 2); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_array/output.js b/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_array/output.js index a8167d185605..7509b863c954 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_array/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_array/output.js @@ -1,12 +1,11 @@ -console.log( - void 0, - [1, 2, 3, a] + 1, - "1,2,3,41", - [1, 2, 3, a][0] + 1, +console.log(void 0, [ + 1, 2, 3, - NaN, - "1,21", - 5, - (void 0)[1] + 1 -); + a +] + 1, "1,2,3,41", [ + 1, + 2, + 3, + a +][0] + 1, 2, 3, NaN, "1,21", 5, (void 0)[1] + 1); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_array_bad_index/output.js b/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_array_bad_index/output.js index b3b7416e6105..136efa1f74dd 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_array_bad_index/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_array_bad_index/output.js @@ -1 +1 @@ -console.log([1, 2, 3, 4].a + 1, [1, 2, 3, 4]["a"] + 1, [1, 2, 3, 4][3.14] + 1); +console.log(NaN, NaN, NaN); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_string_bad_index/output.js b/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_string_bad_index/output.js index da5d6acced78..136efa1f74dd 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_string_bad_index/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/evaluate/unsafe_string_bad_index/output.js @@ -1 +1 @@ -console.log("1234".a + 1, "1234"["a"] + 1, "1234"[3.14] + 1); +console.log(NaN, NaN, NaN); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_2a/output.js b/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_2a/output.js index c7119a35b1c9..ccd48d4a1a03 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_2a/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_2a/output.js @@ -1,19 +1,7 @@ -console.log([ - 10, - 20, - 30, - 40, - 50 -]["length"]); +console.log(5); console.log(10); console.log(20); console.log(30); console.log(40); console.log(50); -console.log([ - 10, - 20, - 30, - 40, - 50 -][5]); +console.log(void 0); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_3a/output.js b/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_3a/output.js index 61dd47dc736c..818c44afcbb9 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_3a/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_3a/output.js @@ -1,24 +1,12 @@ console.log(10); console.log(20); -console.log([ - 10, - 20 -][2]); +console.log(void 0); console.log(10); console.log(20); -console.log([ - 10, - 20 -][2]); +console.log(void 0); console.log(10); console.log(20); -console.log([ - 10, - 20 -][2]); +console.log(void 0); console.log(10); console.log(20); -console.log([ - 10, - 20 -][2]); +console.log(void 0); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_3b/output.js b/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_3b/output.js index 7ddb30b53629..450e86d1e6e8 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_3b/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_3b/output.js @@ -1,10 +1,7 @@ var nothing = []; console.log(10); console.log(20); -console.log([ - 10, - 20 -][2]); +console.log(void 0); console.log([ ...nothing, 10, diff --git a/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_4a/output.js b/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_4a/output.js index 0f741d7ea1e4..38247e0dcfd2 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_4a/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/harmony/array_literal_with_spread_4a/output.js @@ -2,15 +2,27 @@ function t(x) { console.log("(" + x + ")"); return 10 * x; } -console.log([t(1), t(2)][0]); +console.log([ + t(1), + t(2) +][0]); console.log((t(1), t(2))); -console.log([t(1), t(2)][2]); -console.log([t(1), t(2)][0]); +console.log((t(1), void t(2))); +console.log([ + t(1), + t(2) +][0]); console.log((t(1), t(2))); -console.log([t(1), t(2)][2]); -console.log([t(1), t(2)][0]); +console.log((t(1), void t(2))); +console.log([ + t(1), + t(2) +][0]); console.log((t(1), t(2))); -console.log([t(1), t(2)][2]); -console.log([t(1), t(2)][0]); +console.log((t(1), void t(2))); +console.log([ + t(1), + t(2) +][0]); console.log((t(1), t(2))); -console.log([t(1), t(2)][2]); +console.log((t(1), void t(2))); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/harmony/issue_2345/output.js b/crates/swc_ecma_minifier/tests/terser/compress/harmony/issue_2345/output.js index d10c79467ad9..f41496e19e7e 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/harmony/issue_2345/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/harmony/issue_2345/output.js @@ -1,3 +1,9 @@ console.log("3-2-1"); -var a = [3, 2, 1]; -console.log([...a].join("-")); +var a = [ + 3, + 2, + 1 +]; +console.log([ + ...a +].join("-")); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50/config.json b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50/config.json index dccb2dcc1f2c..547e3ffc38e5 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50/config.json +++ b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50/config.json @@ -1,4 +1,4 @@ { "defaults": true, - "passes": 2 + "passes": 3 } diff --git a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50/output.js b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50/output.js index 1a566b601381..e76beab2604a 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50/output.js @@ -1,7 +1 @@ -console.log((0, { - a: -1, - b: 5 -}).a, (0, { - a: -10, - b: 5 -}).a); +console.log(-1, -10); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_const/config.json b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_const/config.json index dccb2dcc1f2c..547e3ffc38e5 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_const/config.json +++ b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_const/config.json @@ -1,4 +1,4 @@ { "defaults": true, - "passes": 2 + "passes": 3 } diff --git a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_const/output.js b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_const/output.js index 1a566b601381..e76beab2604a 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_const/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_const/output.js @@ -1,7 +1 @@ -console.log((0, { - a: -1, - b: 5 -}).a, (0, { - a: -10, - b: 5 -}).a); +console.log(-1, -10); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_let/config.json b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_let/config.json index dccb2dcc1f2c..547e3ffc38e5 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_let/config.json +++ b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_let/config.json @@ -1,4 +1,4 @@ { "defaults": true, - "passes": 2 + "passes": 3 } diff --git a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_let/output.js b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_let/output.js index 1a566b601381..e76beab2604a 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_let/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/issue_t50/issue_t50_let/output.js @@ -1,7 +1 @@ -console.log((0, { - a: -1, - b: 5 -}).a, (0, { - a: -10, - b: 5 -}).a); +console.log(-1, -10); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/properties/evaluate_array_length/output.js b/crates/swc_ecma_minifier/tests/terser/compress/properties/evaluate_array_length/output.js index 024fde7d89a3..b7dead595497 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/properties/evaluate_array_length/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/properties/evaluate_array_length/output.js @@ -1,4 +1,12 @@ a = 3; a = 5; -a = [1, 2, b].length; -a = [1, 2, 3].join(b).length; +a = [ + 1, + 2, + b +].length; +a = [ + 1, + 2, + 3 +].join(b).length; diff --git a/crates/swc_ecma_minifier/tests/terser/compress/properties/lhs_prop_1/output.js b/crates/swc_ecma_minifier/tests/terser/compress/properties/lhs_prop_1/output.js index 3b91051ed2d7..bcde4998b254 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/properties/lhs_prop_1/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/properties/lhs_prop_1/output.js @@ -1 +1,3 @@ -console.log(++{ a: 1 }.a); +console.log(++{ + a: 1 +}.a); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_2450_5/output.js b/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_2450_5/output.js index 30bb2374c906..28398ae3857d 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_2450_5/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_2450_5/output.js @@ -1,7 +1,11 @@ var a; function g() {} -[1, 2, 3].forEach(function () { - (function (b) { +[ + 1, + 2, + 3 +].forEach(function() { + (function(b) { console.log(a === b); a = b; })(g); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_3042_1/output.js b/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_3042_1/output.js index dfb06fba21ff..b396675941b2 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_3042_1/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_3042_1/output.js @@ -1,5 +1,8 @@ function f() {} -var a = [1, 2].map(function () { +var a = [ + 1, + 2 +].map(function() { return new f(); }); console.log(a[0].constructor === a[1].constructor); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_3042_2/output.js b/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_3042_2/output.js index 4ae9f4d0452b..e091db1bf17b 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_3042_2/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/issue_3042_2/output.js @@ -1,13 +1,16 @@ function Foo() { - this.isFoo = function (o) { + this.isFoo = function(o) { return o instanceof Foo; }; } -var fooCollection = new (function () { - this.foos = [1, 1].map(function () { +var fooCollection = new function() { + this.foos = [ + 1, + 1 + ].map(function() { return new Foo(); }); -})(); +}(); console.log(fooCollection.foos[0].isFoo(fooCollection.foos[0])); console.log(fooCollection.foos[0].isFoo(fooCollection.foos[1])); console.log(fooCollection.foos[1].isFoo(fooCollection.foos[0])); diff --git a/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/obj_for_1/output.js b/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/obj_for_1/output.js index d6f02bdd7dd8..a1a05cb3848a 100644 --- a/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/obj_for_1/output.js +++ b/crates/swc_ecma_minifier/tests/terser/compress/reduce_vars/obj_for_1/output.js @@ -1 +1,3 @@ -for (var i = { a: 1 }.a--; i; i--) console.log(i); +for(var i = { + a: 1 +}.a--; i; i--)console.log(i); diff --git a/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs b/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs index ed33657634e4..2712e478dc98 100644 --- a/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs +++ b/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, iter, iter::once}; -use swc_atoms::JsWord; +use swc_atoms::{Atom, JsWord}; use swc_common::{ pass::{CompilerPass, Repeated}, util::take::Take, @@ -107,35 +107,54 @@ impl SimplifyExpr { return; } - #[derive(Clone, PartialEq, Eq)] + #[derive(Clone, PartialEq)] enum KnownOp { /// [a, b].length Len, - Index(i64), + /// [a, b][0] + /// + /// {0.5: "bar"}[0.5] + /// Note: callers need to check `v.fract() == 0.0` in some cases. + /// ie non-integer indexes for arrays result in `undefined` + /// but not for objects (because indexing an object + /// returns the value of the key, ie `0.5` will not + /// return `undefined` if a key `0.5` exists + /// and its value is not `undefined`). + Index(f64), /// ({}).foo IndexStr(JsWord), } let op = match prop { - MemberProp::Ident(Ident { sym, .. }) if &**sym == "length" => KnownOp::Len, + MemberProp::Ident(Ident { sym, .. }) if &**sym == "length" && !obj.is_object() => { + KnownOp::Len + } MemberProp::Ident(Ident { sym, .. }) => { - if !self.in_callee { - KnownOp::IndexStr(sym.clone()) - } else { + if self.in_callee { return; } + + KnownOp::IndexStr(sym.clone()) } MemberProp::Computed(ComputedPropName { expr, .. }) => { - if !self.in_callee { - if let Expr::Lit(Lit::Num(Number { value, .. })) = &**expr { - if value.fract() == 0.0 { - KnownOp::Index(*value as _) - } else { - return; - } + if self.in_callee { + return; + } + + if let Expr::Lit(Lit::Num(Number { value, .. })) = &**expr { + // x[5] + KnownOp::Index(*value) + } else if let Known(s) = expr.as_pure_string(&self.expr_ctx) { + if s == "length" && !obj.is_object() { + // Length of non-object type + KnownOp::Len + } else if let Ok(n) = s.parse::() { + // x['0'] is treated as x[0] + KnownOp::Index(n) } else { - return; + // x[''] or x[...] where ... is an expression like [], ie x[[]] + KnownOp::IndexStr(s.into()) } } else { return; @@ -144,9 +163,18 @@ impl SimplifyExpr { _ => return, }; + // Note: pristine_globals refers to the compress config option pristine_globals. + // Any potential cases where globals are not pristine are handled in compress, + // e.g. x[-1] is not changed as the object's prototype may be modified. + // For example, Array.prototype[-1] = "foo" will result in [][-1] returning + // "foo". + match &mut **obj { Expr::Lit(Lit::Str(Str { value, span, .. })) => match op { // 'foo'.length + // + // Prototype changes do not affect .length, so we don't need to worry + // about pristine_globals here. KnownOp::Len => { self.changed = true; @@ -158,23 +186,35 @@ impl SimplifyExpr { } // 'foo'[1] - KnownOp::Index(idx) if (idx as usize) < value.len() => { - if idx < 0 { - self.changed = true; - *expr = *Expr::undefined(*span) - } else if let Some(value) = nth_char(value, idx as _) { - self.changed = true; - *expr = Expr::Lit(Lit::Str(Str { - raw: None, - value: value.into(), - span: *span, - })) + KnownOp::Index(idx) => { + if idx.fract() != 0.0 || idx < 0.0 || idx as usize >= value.len() { + // Prototype changes affect indexing if the index is out of bounds, so we + // don't replace out-of-bound indexes. + return; + } + + let Some(value) = nth_char(value, idx as _) else { + return; }; + + self.changed = true; + + *expr = Expr::Lit(Lit::Str(Str { + raw: None, + value: value.into(), + span: *span, + })) } - _ => {} + + // 'foo'[''] + // + // Handled in compress + KnownOp::IndexStr(..) => {} }, // [1, 2, 3].length + // + // [1, 2, 3][0] Expr::Array(ArrayLit { elems, span }) => { // do nothing if spread exists let has_spread = elems.iter().any(|elem| { @@ -186,140 +226,136 @@ impl SimplifyExpr { if has_spread { return; } - if op == KnownOp::Len - && !elems - .iter() - .filter_map(|e| e.as_ref()) - .any(|e| e.expr.may_have_side_effects(&self.expr_ctx)) - { - self.changed = true; - *expr = Expr::Lit(Lit::Num(Number { - value: elems.len() as _, - span: *span, - raw: None, - })); - } else if matches!(op, KnownOp::Index(..)) { - let idx = match op { - KnownOp::Index(i) => i, - _ => unreachable!(), - }; + match op { + KnownOp::Len => { + // do nothing if replacement will have side effects + let may_have_side_effects = elems + .iter() + .filter_map(|e| e.as_ref()) + .any(|e| e.expr.may_have_side_effects(&self.expr_ctx)); + + if may_have_side_effects { + return; + } + + // Prototype changes do not affect .length + self.changed = true; + + *expr = Expr::Lit(Lit::Num(Number { + value: elems.len() as _, + span: *span, + raw: None, + })); + } + + KnownOp::Index(idx) => { + // If the fraction part is non-zero, or if the index is out of bounds, + // then we handle this in compress as Array's prototype may be modified. + if idx.fract() != 0.0 || idx < 0.0 || idx as usize >= elems.len() { + return; + } - if elems.len() > idx as _ && idx >= 0 { + // Don't change if after has side effects. let after_has_side_effect = - elems.iter().skip((idx + 1) as _).any(|elem| match elem { - Some(elem) => elem.expr.may_have_side_effects(&self.expr_ctx), - None => false, - }); + elems + .iter() + .skip((idx as usize + 1) as _) + .any(|elem| match elem { + Some(elem) => elem.expr.may_have_side_effects(&self.expr_ctx), + None => false, + }); if after_has_side_effect { return; } - } else { - return; - } - self.changed = true; + self.changed = true; - let (before, e, after) = if elems.len() > idx as _ && idx >= 0 { - let before = elems.drain(..(idx as usize)).collect(); + // elements before target element + let before: Vec> = + elems.drain(..(idx as usize)).collect(); let mut iter = elems.take().into_iter(); + // element at idx let e = iter.next().flatten(); - let after = iter.collect(); + // elements after target element + let after: Vec> = iter.collect(); - (before, e, after) - } else { - let before = elems.take(); + // element value + let v = match e { + None => Expr::undefined(*span), + Some(e) => e.expr, + }; - (before, None, vec![]) - }; + // Replacement expressions. + let mut exprs = vec![]; - let v = match e { - None => Expr::undefined(*span), - Some(e) => e.expr, - }; + // Add before side effects. + for elem in before.into_iter().flatten() { + self.expr_ctx + .extract_side_effects_to(&mut exprs, *elem.expr); + } - let mut exprs = vec![]; - for elem in before.into_iter().flatten() { - self.expr_ctx - .extract_side_effects_to(&mut exprs, *elem.expr); - } + // Element value. + let val = v; - let val = v; + // Add after side effects. + for elem in after.into_iter().flatten() { + self.expr_ctx + .extract_side_effects_to(&mut exprs, *elem.expr); + } - for elem in after.into_iter().flatten() { - self.expr_ctx - .extract_side_effects_to(&mut exprs, *elem.expr); - } + // Note: we always replace with a SeqExpr so that + // `this` remains undefined in strict mode. - if exprs.is_empty() { - *expr = Expr::Seq(SeqExpr { - span: val.span(), - exprs: vec![0.into(), val], - }); - return; - } + if exprs.is_empty() { + // No side effects exist, replace with: + // (0, val) + *expr = Expr::Seq(SeqExpr { + span: val.span(), + exprs: vec![0.into(), val], + }); + return; + } - exprs.push(val); + // Add value and replace with SeqExpr + exprs.push(val); + *expr = Expr::Seq(SeqExpr { span: *span, exprs }); + } - *expr = Expr::Seq(SeqExpr { span: *span, exprs }); + // Handled in compress + KnownOp::IndexStr(..) => {} } } // { foo: true }['foo'] - Expr::Object(ObjectLit { props, span }) => match op { - KnownOp::IndexStr(key) if is_literal(props) && key != *"yield" => { - // do nothing if spread exists - let has_spread = props - .iter() - .any(|prop| matches!(prop, PropOrSpread::Spread(..))); - - if has_spread { - return; - } - - let idx = props.iter().rev().position(|p| match p { - PropOrSpread::Prop(p) => match &**p { - Prop::Shorthand(i) => i.sym == key, - Prop::KeyValue(k) => prop_name_eq(&k.key, &key), - Prop::Assign(p) => p.key.sym == key, - Prop::Getter(..) => false, - Prop::Setter(..) => false, - // TODO - Prop::Method(..) => false, - }, - _ => unreachable!(), - }); - let idx = idx.map(|idx| props.len() - 1 - idx); - // + // + // { 0.5: true }[0.5] + Expr::Object(ObjectLit { props, span }) => { + // get key + let key = match op { + KnownOp::Index(i) => Atom::from(i.to_string()), + KnownOp::IndexStr(key) if key != *"yield" && is_literal(props) => key, + _ => return, + }; + + // Get `key`s value. Non-existent keys are handled in compress. + // This also checks if spread exists. + let Some(v) = get_key_value(&key, props) else { + return; + }; - if let Some(i) = idx { - let v = props.remove(i); - self.changed = true; + self.changed = true; - *expr = self.expr_ctx.preserve_effects( - *span, - match v { - PropOrSpread::Prop(p) => match *p { - Prop::Shorthand(i) => Expr::Ident(i), - Prop::KeyValue(p) => *p.value, - Prop::Assign(p) => *p.value, - Prop::Getter(..) => unreachable!(), - Prop::Setter(..) => unreachable!(), - // TODO - Prop::Method(..) => unreachable!(), - }, - _ => unreachable!(), - }, - once(Box::new(Expr::Object(ObjectLit { - props: props.take(), - span: *span, - }))), - ); - } - } - _ => {} - }, + *expr = self.expr_ctx.preserve_effects( + *span, + v, + once(Box::new(Expr::Object(ObjectLit { + props: props.take(), + span: *span, + }))), + ); + } _ => {} } @@ -1697,3 +1733,70 @@ fn nth_char(s: &str, mut idx: usize) -> Option> { fn need_zero_for_this(e: &Expr) -> bool { e.directness_maters() || e.is_seq() } + +/// Gets the value of the given key from the given object properties, if the key +/// exists. If the key does exist, `Some` is returned and the property is +/// removed from the given properties. +fn get_key_value(key: &str, props: &mut Vec) -> Option { + // It's impossible to know the value for certain if a spread property exists. + let has_spread = props.iter().any(|prop| prop.is_spread()); + + if has_spread { + return None; + } + + for (i, prop) in props.iter_mut().enumerate() { + let prop = match prop { + PropOrSpread::Prop(x) => &mut **x, + PropOrSpread::Spread(_) => unreachable!(), + }; + + match prop { + Prop::Shorthand(ident) if ident.sym == key => { + let prop = match props.remove(i) { + PropOrSpread::Prop(x) => *x, + _ => unreachable!(), + }; + let ident = match prop { + Prop::Shorthand(x) => x, + _ => unreachable!(), + }; + return Some(Expr::Ident(ident)); + } + + Prop::KeyValue(prop) => { + if key != "__proto__" && prop_name_eq(&prop.key, "__proto__") { + // If __proto__ is defined, we need to check the contents of it, + // as well as any nested __proto__ objects + let Expr::Object(ObjectLit { props, .. }) = &mut *prop.value else { + // __proto__ is not an ObjectLiteral. It's unsafe to keep trying to find + // a value for this key, since __proto__ might also contain the key. + return None; + }; + + // Get key value from __props__ object. Only return if + // the result is Some. If None, we keep searching in the + // parent object. + let v = get_key_value(key, props); + if v.is_some() { + return v; + } + } else if prop_name_eq(&prop.key, key) { + let prop = match props.remove(i) { + PropOrSpread::Prop(x) => *x, + _ => unreachable!(), + }; + let prop = match prop { + Prop::KeyValue(x) => x, + _ => unreachable!(), + }; + return Some(*prop.value); + } + } + + _ => {} + } + } + + None +} diff --git a/crates/swc_ecma_transforms_optimization/src/simplify/expr/tests.rs b/crates/swc_ecma_transforms_optimization/src/simplify/expr/tests.rs index 9b4b388a6c01..b70c0743a1e9 100644 --- a/crates/swc_ecma_transforms_optimization/src/simplify/expr/tests.rs +++ b/crates/swc_ecma_transforms_optimization/src/simplify/expr/tests.rs @@ -974,15 +974,15 @@ fn test_fold_comparison4() { #[test] fn test_fold_get_elem1() { - fold("x = [,10][0]", "x = (0, void 0)"); - fold("x = [10, 20][0]", "x = (0, 10)"); - fold("x = [10, 20][1]", "x = (0, 20)"); + fold("x = [,10][0]", "x = (0, void 0);"); + fold("x = [10, 20][0]", "x = (0, 10);"); + fold("x = [10, 20][1]", "x = (0, 20);"); // fold("x = [10, 20][-1]", "x = void 0;"); // fold("x = [10, 20][2]", "x = void 0;"); fold("x = [foo(), 0][1]", "x = (foo(), 0);"); - fold("x = [0, foo()][1]", "x = (0, foo())"); + fold("x = [0, foo()][1]", "x = (0, foo());"); // fold("x = [0, foo()][0]", "x = (foo(), 0)"); fold_same("for([1][0] in {});"); } @@ -1008,8 +1008,8 @@ fn test_fold_array_lit_spread_get_elem() { fold("x = [...[0 ]][0]", "x = (0, 0);"); fold("x = [0, 1, ...[2, 3, 4]][3]", "x = (0, 3);"); fold("x = [...[0, 1], 2, ...[3, 4]][3]", "x = (0, 3);"); - fold("x = [...[...[0, 1], 2, 3], 4][0]", "x = (0, 0)"); - fold("x = [...[...[0, 1], 2, 3], 4][3]", "x = (0, 3)"); + fold("x = [...[...[0, 1], 2, 3], 4][0]", "x = (0, 0);"); + fold("x = [...[...[0, 1], 2, 3], 4][3]", "x = (0, 3);"); // fold("x = [...[]][100]", "x = void 0;"); // fold("x = [...[0]][100]", "x = void 0;"); } @@ -1575,3 +1575,53 @@ fn test_export_default_paren_expr() { fold_same("import fn from './b'; export default (function fn1 () {});"); fold("export default ((foo));", "export default foo;"); } + +#[test] +fn test_issue_8747() { + // Index with a valid index. + fold("'a'[0]", "\"a\";"); + fold("'a'['0']", "\"a\";"); + + // Index with an invalid index. + // An invalid index is an out-of-bound index. These are not replaced as + // prototype changes could cause undefined behaviour. Refer to + // pristine_globals in compress. + fold_same("'a'[0.5]"); + fold_same("'a'[-1]"); + + fold_same("[1][0.5]"); + fold_same("[1][-1]"); + + // Index with an expression. + fold("'a'[0 + []]", "\"a\";"); + fold("[1][0 + []]", "0, 1;"); + + // Don't replace if side effects exist. + fold_same("[f(), f()][0]"); + fold("[x(), 'x', 5][2]", "x(), 5;"); + fold_same("({foo: f()}).foo"); + + // Index with length, resulting in replacement. + // Prototype changes don't affect .length in String and Array, + // but it is affected in Object. + fold("[].length", "0"); + fold("[]['length']", "0"); + + fold("''.length", "0"); + fold("''['length']", "0"); + + fold_same("({}).length"); + fold_same("({})['length']"); + fold("({length: 'foo'}).length", "'foo'"); + fold("({length: 'foo'})['length']", "'foo'"); + + // Indexing objects has a few special cases that were broken that we test here. + fold("({0.5: 'a'})[0.5]", "'a';"); + fold("({'0.5': 'a'})[0.5]", "'a';"); + fold("({0.5: 'a'})['0.5']", "'a';"); + // Indexing objects that have a spread operator in `__proto__` can still be + // optimized if the key comes before the `__proto__` object. + fold("({1: 'bar', __proto__: {...[1]}})[1]", "({...[1]}), 'bar';"); + // Spread operator comes first, can't be evaluated. + fold_same("({...[1], 1: 'bar'})[1]"); +} diff --git a/crates/swc_ecma_utils/src/lib.rs b/crates/swc_ecma_utils/src/lib.rs index f1511dc9dac4..074b38ac41d0 100644 --- a/crates/swc_ecma_utils/src/lib.rs +++ b/crates/swc_ecma_utils/src/lib.rs @@ -1885,13 +1885,9 @@ impl Visit for LiteralVisitor { match node { PropName::Str(ref s) => self.cost += 2 + s.value.len(), PropName::Ident(ref id) => self.cost += 2 + id.sym.len(), - PropName::Num(n) => { - if n.value.fract() < 1e-10 { - // TODO: Count digits - self.cost += 5; - } else { - self.is_lit = false - } + PropName::Num(..) => { + // TODO: Count digits + self.cost += 5; } PropName::BigInt(_) => self.is_lit = false, PropName::Computed(..) => self.is_lit = false, @@ -2675,7 +2671,7 @@ pub fn prop_name_eq(p: &PropName, key: &str) -> bool { match p { PropName::Ident(i) => i.sym == *key, PropName::Str(s) => s.value == *key, - PropName::Num(_) => false, + PropName::Num(n) => n.value.to_string() == *key, PropName::BigInt(_) => false, PropName::Computed(e) => match &*e.expr { Expr::Lit(Lit::Str(Str { value, .. })) => *value == *key,