feat(minifier): fold out-of-bounds string indices to void 0 (#28004)

`"a"[1]`, `"a"["1"]`, `"é"[1]` and `"😀"[2]` read an index past the end of the string, which finds no own property and falls through to `String.prototype`. Under the [no-monkey-patching assumption](https://github.com/oxc-project/oxc/blob/main/crates/oxc_minifier/docs/ASSUMPTIONS.md#no-monkey-patching-built-ins) that prototype has no index properties, so the access is `undefined` and now folds to `void 0`. Later folds can then use the value: `"a"[1] === void 0` becomes `!0`.

The operand of `delete` and call targets keep the access. Non-canonical keys such as `"a"["01"]` and negative indices are not index reads, so they stay as they are. esbuild keeps `"abc"[3]`, and the ported esbuild test is updated with a note.

Closes #27992.

Implemented with AI assistance.
This commit is contained in:
Dunqing committed 2026-10-21 20:59:04 +00:00
1 parent eacffcccdf
commit dd6cb2be00
3 files changed
+17 -7

No files matched your search

@@ -729,7 +729,11 @@ impl<'a> PeepholeOptimizations {
let unit = match s.value.as_str() {
Some(value) => value.encode_utf16().nth(index),
None => s.value.encode_utf16().nth(index),
}?;
};
// An index past the end reads `String.prototype`, which has no index properties.
let Some(unit) = unit else {
return Some(Expression::new_void_0(span, ctx));
};
let mut builder = JSStrBuilder::new_in(ctx);
builder.push_code_unit(unit);
s.span = span;
@@ -371,7 +371,8 @@ fn js_parser_test() {
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];");
// esbuild keeps this; oxc assumes `String.prototype` has no index properties.
test("a = 'abc'[3]", "a = void 0;");
test("a = 'abc'[NaN]", "a = 'abc'[NaN];");
test("a = 'abc'[-1e100]", "a = 'abc'[-1e100];");
test("a = 'abc'[1e100]", "a = 'abc'[1e100];");
@@ -1193,16 +1193,20 @@ fn test_number_constants() {
#[test]
fn test_fold_integer_index_access() {
test_same("v = ''[0]");
// An index past the end reads `String.prototype`, which has no index properties.
test("v = ''[0]", "v = void 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 = 'a'[1]", "v = void 0");
test("v = 'a'['1']", "v = void 0");
test_same("v = 'a'['01']");
test("v = 'あ'[0]", "v = 'あ'");
test_same("v = 'あ'[1]");
test("v = 'あ'[1]", "v = void 0");
test("v = 'é'[1]", "v = void 0");
test("v = '😀'[0]", "v = '\\uD83D'");
test("v = '😀'[1]", "v = '\\uDE00'");
test_same("v = '😀'[2]");
test("v = '😀'[2]", "v = void 0");
test("v = '😀x'[2]", "v = 'x'");
test_same("v = '😀'[0.5]");
test_same("v = '😀'['01']");
@@ -1214,7 +1218,8 @@ fn test_fold_integer_index_access() {
test("v = '\\uD800😀'[2]", "v = '\\uDE00'");
test("v = '😀\\uD800'[2]", "v = '\\uD800'");
test("v = '\\uDC00'[0]", "v = '\\uDC00'");
test_same("v = 'a\\uD800b'[3]");
test("v = 'a\\uD800b'[3]", "v = void 0");
test_same("v = 'a'[1]()");
test_same("v = (foo(), 'a')[1]"); // can be fold into `v = (foo(), 'a')`
test_same("v = [][0]");