Skip to content

Commit 434cfc9

Browse files
committed
feat(ecmascript): support integer index access for array and string in may_have_side_effects
1 parent 0b210f1 commit 434cfc9

4 files changed

Lines changed: 113 additions & 2 deletions

File tree

crates/oxc_ecmascript/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ mod to_numeric;
2424
mod to_primitive;
2525
mod to_string;
2626

27+
// other
28+
mod to_integer_index;
29+
2730
pub mod constant_evaluation;
2831
pub mod is_global_reference;
2932
pub mod side_effects;
@@ -44,6 +47,7 @@ pub use self::{
4447
to_big_int::ToBigInt,
4548
to_boolean::ToBoolean,
4649
to_int_32::ToInt32,
50+
to_integer_index::ToIntegerIndex,
4751
to_number::ToNumber,
4852
to_primitive::ToPrimitive,
4953
to_string::ToJsString,

crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use oxc_ast::ast::*;
22

33
use crate::{
4-
constant_evaluation::DetermineValueType, is_global_reference::IsGlobalReference,
5-
to_numeric::ToNumeric, to_primitive::ToPrimitive,
4+
ToBigInt, ToIntegerIndex, constant_evaluation::DetermineValueType,
5+
is_global_reference::IsGlobalReference, to_numeric::ToNumeric, to_primitive::ToPrimitive,
66
};
77

88
/// Returns true if subtree changes application state.
@@ -448,6 +448,27 @@ impl MayHaveSideEffects for ComputedMemberExpression<'_> {
448448
is_global_reference,
449449
)
450450
}
451+
Expression::NumericLiteral(n) => !n.value.to_integer_index().is_some_and(|n| {
452+
!integer_index_property_access_may_have_side_effects(
453+
&self.object,
454+
n,
455+
is_global_reference,
456+
)
457+
}),
458+
Expression::BigIntLiteral(b) => {
459+
if b.is_negative() {
460+
return true;
461+
}
462+
!b.to_big_int(is_global_reference)
463+
.and_then(ToIntegerIndex::to_integer_index)
464+
.is_some_and(|b| {
465+
!integer_index_property_access_may_have_side_effects(
466+
&self.object,
467+
b,
468+
is_global_reference,
469+
)
470+
})
471+
}
451472
_ => true,
452473
}
453474
}
@@ -467,6 +488,35 @@ fn property_access_may_have_side_effects(
467488
}
468489
}
469490

491+
fn integer_index_property_access_may_have_side_effects(
492+
object: &Expression,
493+
property: u32,
494+
is_global_reference: &impl IsGlobalReference,
495+
) -> bool {
496+
if object.may_have_side_effects(is_global_reference) {
497+
return true;
498+
}
499+
match object {
500+
Expression::StringLiteral(s) => property as usize >= s.value.encode_utf16().count(),
501+
Expression::ArrayExpression(arr) => property as usize >= get_array_minimum_length(arr),
502+
_ => true,
503+
}
504+
}
505+
506+
fn get_array_minimum_length(arr: &ArrayExpression) -> usize {
507+
arr.elements
508+
.iter()
509+
.map(|e| match e {
510+
ArrayExpressionElement::SpreadElement(spread) => match &spread.argument {
511+
Expression::ArrayExpression(arr) => get_array_minimum_length(arr),
512+
Expression::StringLiteral(str) => str.value.chars().count(),
513+
_ => 0,
514+
},
515+
_ => 1,
516+
})
517+
.sum()
518+
}
519+
470520
impl MayHaveSideEffects for CallExpression<'_> {
471521
fn may_have_side_effects(&self, is_global_reference: &impl IsGlobalReference) -> bool {
472522
if self.pure {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use num_bigint::BigInt;
2+
use num_traits::{ToPrimitive, Zero};
3+
4+
/// Convert a value to u32 if the value can be an integer index and can be represented with u32.
5+
///
6+
/// Note that the max value of u32 is `2^32 - 1`, which is smaller than the max value of
7+
/// safe integer `2^53 - 1`.
8+
///
9+
/// If the value is a non-negative safe integer, it can be an integer index.
10+
/// <https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-object-type>
11+
pub trait ToIntegerIndex {
12+
fn to_integer_index(self) -> Option<u32>;
13+
}
14+
15+
impl ToIntegerIndex for f64 {
16+
fn to_integer_index(self) -> Option<u32> {
17+
if self.fract() != 0.0 || self < 0.0 {
18+
return None;
19+
}
20+
self.to_u32()
21+
}
22+
}
23+
24+
impl ToIntegerIndex for BigInt {
25+
fn to_integer_index(self) -> Option<u32> {
26+
if self < BigInt::zero() {
27+
return None;
28+
}
29+
self.to_u32()
30+
}
31+
}

crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,32 @@ fn test_property_access() {
641641
test("[][`length`]", false);
642642

643643
test("a[0]", true);
644+
test("''[-1]", true); // String.prototype[-1] may be overridden
645+
test("''[0.3]", true); // String.prototype[0.3] may be overridden
646+
test("''[0]", true); // String.prototype[0] may be overridden
647+
test("'a'[0]", false);
648+
test("'a'[0n]", false);
649+
test("'a'[1]", true); // String.prototype[1] may be overridden
650+
test("'あ'[0]", false);
651+
test("'あ'[1]", true); // the length of "あ" is 1
652+
test("'😀'[0]", false);
653+
test("'😀'[1]", false);
654+
test("'😀'[2]", true); // the length of "😀" is 2
655+
656+
test("[][-1]", true); // Array.prototype[-1] may be overridden
657+
test("[][0.3]", true); // Array.prototype[0.3] may be overridden
658+
test("[][0]", true); // Array.prototype[0] may be overridden
659+
test("[1][0]", false);
660+
test("[1][0n]", false);
661+
test("[1][1]", true); // Array.prototype[1] may be overridden
662+
test("[,][0]", false);
663+
test("[...[], 1][0]", false);
664+
test("[...[1]][0]", false);
665+
test("[...'a'][0]", false);
666+
test("[...'a'][1]", true);
667+
test("[...'😀'][0]", false);
668+
test("[...'😀'][1]", true);
669+
test("[...a, 1][0]", true); // "...a" may have a sideeffect
644670
}
645671

646672
#[test]

0 commit comments

Comments
 (0)