diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs index ab6ffa643de5c..834d2be004c3a 100644 --- a/crates/oxc_ast/src/ast_impl/js.rs +++ b/crates/oxc_ast/src/ast_impl/js.rs @@ -396,6 +396,12 @@ impl ArrayExpressionElement<'_> { pub fn is_elision(&self) -> bool { matches!(self, Self::Elision(_)) } + + /// Returns `true` if this array expression element is a [spread](SpreadElement). + #[inline] + pub fn is_spread(&self) -> bool { + matches!(self, Self::SpreadElement(_)) + } } impl<'a> From> for ArrayExpressionElement<'a> { diff --git a/crates/oxc_minifier/src/peephole/replace_known_methods.rs b/crates/oxc_minifier/src/peephole/replace_known_methods.rs index 6755c8bac52b0..ec1f945b31f31 100644 --- a/crates/oxc_minifier/src/peephole/replace_known_methods.rs +++ b/crates/oxc_minifier/src/peephole/replace_known_methods.rs @@ -5,8 +5,9 @@ use oxc_allocator::IntoIn; use oxc_ast::ast::*; use oxc_ecmascript::{ StringCharAt, StringCharAtResult, StringCharCodeAt, StringIndexOf, StringLastIndexOf, - StringSubstring, ToInt32, + StringSubstring, ToBigInt, ToInt32, ToIntegerIndex, constant_evaluation::{ConstantEvaluation, DetermineValueType}, + side_effects::MayHaveSideEffects, }; use oxc_span::SPAN; use oxc_syntax::es_target::ESTarget; @@ -771,13 +772,47 @@ impl<'a> PeepholeOptimizations { } fn try_fold_known_property_access(&mut self, node: &mut Expression<'a>, ctx: Ctx<'a, '_>) { - let (name, object, span) = match &node { + let (name, object, span) = match node { Expression::StaticMemberExpression(member) if !member.optional => { (member.property.name.as_str(), &member.object, member.span) } Expression::ComputedMemberExpression(member) if !member.optional => { match &member.expression { Expression::StringLiteral(s) => (s.value.as_str(), &member.object, member.span), + Expression::NumericLiteral(n) => { + if let Some(integer_index) = n.value.to_integer_index() { + let span = member.span; + if let Some(replacement) = Self::try_fold_integer_index_access( + &mut member.object, + integer_index, + span, + ctx, + ) { + self.mark_current_function_as_changed(); + *node = replacement; + } + } + return; + } + Expression::BigIntLiteral(b) => { + if !b.is_negative() { + if let Some(integer_index) = + b.to_big_int(&ctx).and_then(ToIntegerIndex::to_integer_index) + { + let span = member.span; + if let Some(replacement) = Self::try_fold_integer_index_access( + &mut member.object, + integer_index, + span, + ctx, + ) { + self.mark_current_function_as_changed(); + *node = replacement; + } + } + } + return; + } _ => return, } } @@ -884,6 +919,50 @@ impl<'a> PeepholeOptimizations { None, )) } + + /// Compress `"abc"[0]` to `"a"` and `[0,1,2][1]` to `1` + fn try_fold_integer_index_access( + object: &mut Expression<'a>, + property: u32, + span: Span, + ctx: Ctx<'a, '_>, + ) -> Option> { + if object.may_have_side_effects(&ctx) { + return None; + } + + match object { + Expression::StringLiteral(s) => { + if let StringCharAtResult::Value(c) = + s.value.as_str().char_at(Some(property.into())) + { + s.span = span; + s.value = ctx.ast.atom(&c.to_string()); + s.raw = None; + Some(ctx.ast.move_expression(object)) + } else { + None + } + } + Expression::ArrayExpression(array_expr) => { + let length_until_spread = + array_expr.elements.iter().take_while(|el| !el.is_spread()).count(); + if (property as usize) < length_until_spread { + match &array_expr.elements[property as usize] { + ArrayExpressionElement::SpreadElement(_) => unreachable!(), + ArrayExpressionElement::Elision(_) => Some(ctx.ast.void_0(span)), + match_expression!(ArrayExpressionElement) => { + let element = array_expr.elements.swap_remove(property as usize); + Some(element.into_expression()) + } + } + } else { + None + } + } + _ => None, + } + } } /// Port from: @@ -1881,4 +1960,33 @@ mod test { test_es2015("v = Number.MIN_SAFE_INTEGER", "v = -9007199254740991"); test_es2015("v = Number.EPSILON", "v = Number.EPSILON"); } + + #[test] + fn test_fold_integer_index_access() { + test_same("v = ''[0]"); + test_same("v = 'a'[-1]"); + test_same("v = 'a'[0.3]"); + test("v = 'a'[0]", "v = 'a'"); + test_same("v = 'a'[1]"); + test("v = 'ใ‚'[0]", "v = 'ใ‚'"); + test_same("v = 'ใ‚'[1]"); + test_same("v = '๐Ÿ˜€'[0]"); // surrogate pairs cannot be represented by rust string + test_same("v = '๐Ÿ˜€'[1]"); // surrogate pairs cannot be represented by rust string + test_same("v = '๐Ÿ˜€'[2]"); + test_same("v = (foo(), 'a')[1]"); // can be fold into `v = (foo(), 'a')` + + test_same("v = [][0]"); + test_same("v = [1][-1]"); + test_same("v = [1][0.3]"); + test("v = [1][0]", "v = 1"); + test_same("v = [1][1]"); + test("v = [,][0]", "v = void 0"); + // test("v = [...'a'][0]", "v = 'a'"); + // test_same("v = [...'a'][1]"); + // test("v = [...'๐Ÿ˜€'][0]", "v = '๐Ÿ˜€'"); + // test_same("v = [...'๐Ÿ˜€'][1]"); + test_same("v = [...a, 1][1]"); + test_same("v = [1, ...a][0]"); + test("v = [1, ...[1,2]][0]", "v = 1"); + } } diff --git a/crates/oxc_minifier/tests/peephole/esbuild.rs b/crates/oxc_minifier/tests/peephole/esbuild.rs index 47059c62c680f..fb92b0ee57554 100644 --- a/crates/oxc_minifier/tests/peephole/esbuild.rs +++ b/crates/oxc_minifier/tests/peephole/esbuild.rs @@ -359,9 +359,9 @@ fn js_parser_test() { test("a = 'ศงแธƒฤ‹'.length", "a = 3;"); test("a = '๐Ÿ‘ฏโ€โ™‚๏ธ'.length", "a = 5;"); test("a = 'abc'[-1]", "a = 'abc'[-1];"); - // test("a = 'abc'[-0]", "a = 'a';"); - // test("a = 'abc'[0]", "a = 'a';"); - // test("a = 'abc'[2]", "a = 'c';"); + test("a = 'abc'[-0]", "a = 'a';"); + test("a = 'abc'[0]", "a = 'a';"); + test("a = 'abc'[2]", "a = 'c';"); test("a = 'abc'[3]", "a = 'abc'[3];"); test("a = 'abc'[NaN]", "a = 'abc'[NaN];"); test("a = 'abc'[-1e100]", "a = 'abc'[-1e100];");