Skip to content

Commit dd6cb2b

Browse files
committed
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.
1 parent eacffcc commit dd6cb2b

3 files changed

Lines changed: 17 additions & 7 deletions

File tree

β€Žcrates/oxc_minifier/src/peephole/replace_known_methods.rsβ€Ž

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,11 @@ impl<'a> PeepholeOptimizations {
729729
let unit = match s.value.as_str() {
730730
Some(value) => value.encode_utf16().nth(index),
731731
None => s.value.encode_utf16().nth(index),
732-
}?;
732+
};
733+
// An index past the end reads `String.prototype`, which has no index properties.
734+
let Some(unit) = unit else {
735+
return Some(Expression::new_void_0(span, ctx));
736+
};
733737
let mut builder = JSStrBuilder::new_in(ctx);
734738
builder.push_code_unit(unit);
735739
s.span = span;

β€Žcrates/oxc_minifier/tests/peephole/esbuild.rsβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,8 @@ fn js_parser_test() {
371371
test("a = 'abc'[-0]", "a = 'a';");
372372
test("a = 'abc'[0]", "a = 'a';");
373373
test("a = 'abc'[2]", "a = 'c';");
374-
test("a = 'abc'[3]", "a = 'abc'[3];");
374+
// esbuild keeps this; oxc assumes `String.prototype` has no index properties.
375+
test("a = 'abc'[3]", "a = void 0;");
375376
test("a = 'abc'[NaN]", "a = 'abc'[NaN];");
376377
test("a = 'abc'[-1e100]", "a = 'abc'[-1e100];");
377378
test("a = 'abc'[1e100]", "a = 'abc'[1e100];");

β€Žcrates/oxc_minifier/tests/peephole/replace_known_methods.rsβ€Ž

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,16 +1193,20 @@ fn test_number_constants() {
11931193

11941194
#[test]
11951195
fn test_fold_integer_index_access() {
1196-
test_same("v = ''[0]");
1196+
// An index past the end reads `String.prototype`, which has no index properties.
1197+
test("v = ''[0]", "v = void 0");
11971198
test_same("v = 'a'[-1]");
11981199
test_same("v = 'a'[0.3]");
11991200
test("v = 'a'[0]", "v = 'a'");
1200-
test_same("v = 'a'[1]");
1201+
test("v = 'a'[1]", "v = void 0");
1202+
test("v = 'a'['1']", "v = void 0");
1203+
test_same("v = 'a'['01']");
12011204
test("v = 'あ'[0]", "v = 'あ'");
1202-
test_same("v = 'あ'[1]");
1205+
test("v = 'あ'[1]", "v = void 0");
1206+
test("v = 'Γ©'[1]", "v = void 0");
12031207
test("v = 'πŸ˜€'[0]", "v = '\\uD83D'");
12041208
test("v = 'πŸ˜€'[1]", "v = '\\uDE00'");
1205-
test_same("v = 'πŸ˜€'[2]");
1209+
test("v = 'πŸ˜€'[2]", "v = void 0");
12061210
test("v = 'πŸ˜€x'[2]", "v = 'x'");
12071211
test_same("v = 'πŸ˜€'[0.5]");
12081212
test_same("v = 'πŸ˜€'['01']");
@@ -1214,7 +1218,8 @@ fn test_fold_integer_index_access() {
12141218
test("v = '\\uD800πŸ˜€'[2]", "v = '\\uDE00'");
12151219
test("v = 'πŸ˜€\\uD800'[2]", "v = '\\uD800'");
12161220
test("v = '\\uDC00'[0]", "v = '\\uDC00'");
1217-
test_same("v = 'a\\uD800b'[3]");
1221+
test("v = 'a\\uD800b'[3]", "v = void 0");
1222+
test_same("v = 'a'[1]()");
12181223
test_same("v = (foo(), 'a')[1]"); // can be fold into `v = (foo(), 'a')`
12191224

12201225
test_same("v = [][0]");

0 commit comments

Comments
Β (0)