fix(minifier,linter): keep delete of constant members and later JSX keys (#27990)

`delete "a"[0]`, `delete "a".length`, `delete [1].length` and `delete Number.NaN` delete a non-configurable property, so they are `false`, or a `TypeError` in strict mode. The minifier folded the member to its value first, and deleting a value is `true`:

```js
console.log(delete "😀"[0]); // false; minified to `console.log(!0)`
```

#27096 extended the string index fold to every string, so `delete "😀"[0]` became `!0` in 0.155.0. `delete "abc"[0]` and `delete "abc".length` were already wrong in 0.154.0. The member folds, the `Number.*` constant replacement and the `Number.NaN` normalization now skip the operand of `delete`. `delete obj["x"]` still becomes `delete obj.x`, and `delete Number["NaN"]` now minifies the same way every time.

`react/jsx-key` with `warnOnDuplicates` stopped at a template key with a substitution, `` key={`${a}`} ``, instead of reading the element's next `key`. Since #26717 the duplicate in `[<li key={`${a}`} key="dup" />, <li key={`${a}`} key="dup" />]` went unreported. The rule now falls through to the next `key` as 0.154.0 did.

Found by comparing 0.154.0 and 0.155.0 builds. Part of #26242.

Implemented with AI assistance.
This commit is contained in:
Dunqing committed 2026-10-20 07:49:47 +00:00
1 parent aa7642ad31
commit 4c0fa90abe
6 files changed
+58 -3

No files matched your search

+14 -1
View File
@@ -419,7 +419,9 @@ fn get_jsx_element_key_value<'a>(
return Some((value, attr.span));
}
JSXExpression::TemplateLiteral(lit) => {
return lit.single_quasi().map(|value| (value, attr.span));
if let Some(value) = lit.single_quasi() {
return Some((value, attr.span));
}
}
_ => {}
}
@@ -678,6 +680,17 @@ fn test() {
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
None,
),
// A template key with a substitution leaves the element's later `key` to compare.
(
r#"const spans = [<span key={`${a}`} key="dup"/>, <span key={`${a}`} key="dup"/>];"#,
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
None,
),
(
r#"const div = <div><span key={`${a}`} key="dup"/><span key="dup"/></div>;"#,
Some(serde_json::json!([{ "warnOnDuplicates": true }])),
None,
),
// Keys containing lone surrogates are compared by their JavaScript value.
(
r#"const spans = [<span key={"\uD800"}/>, <span key={"\uD800"}/>];"#,
@@ -171,6 +171,20 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Each child in a list should have a unique 'key' prop
⚠ react(jsx-key): Duplicate key 'dup' found in JSX elements
╭─[jsx_key.tsx:1:67]
1 │ const spans = [<span key={`${a}`} key="dup"/>, <span key={`${a}`} key="dup"/>];
· ─────────
╰────
help: Each child in a list should have a unique 'key' prop
⚠ react(jsx-key): Duplicate key 'dup' found in JSX elements
╭─[jsx_key.tsx:1:54]
1 │ const div = <div><span key={`${a}`} key="dup"/><span key="dup"/></div>;
· ─────────
╰────
help: Each child in a list should have a unique 'key' prop
⚠ react(jsx-key): Duplicate key '\ud800' found in JSX elements
╭─[jsx_key.tsx:1:46]
1 │ const spans = [<span key={"\uD800"}/>, <span key={"\uD800"}/>];
@@ -71,8 +71,18 @@ impl<'a> PeepholeOptimizations {
}
}
/// Whether the current node is the operand of `delete`. `delete "a".length` deletes a
/// non-configurable property, so it is false (or throws in strict mode), while deleting the
/// folded value would be true.
pub(super) fn is_delete_argument(ctx: &TraverseCtx<'a>) -> bool {
matches!(ctx.parent(), Ancestor::UnaryExpressionArgument(unary) if unary.operator().is_delete())
}
pub fn fold_static_member_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::StaticMemberExpression(e) = expr else { return };
if Self::is_delete_argument(ctx) {
return;
}
// TODO: tryFoldObjectPropAccess(n, left, name)
// `evaluate_value` only folds a narrow set of member accesses (currently
// `.length` on constant strings/arrays) and bails cheaply otherwise.
@@ -89,6 +99,9 @@ impl<'a> PeepholeOptimizations {
pub fn fold_computed_member_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::ComputedMemberExpression(e) = expr else { return };
if Self::is_delete_argument(ctx) {
return;
}
// TODO: tryFoldObjectPropAccess(n, left, name)
// See `fold_static_member_expr`: bail via the cheap `evaluate_value`
// check before walking the object and key for side effects.
@@ -311,7 +324,7 @@ impl<'a> PeepholeOptimizations {
if e.operator == BinaryOperator::Division
&& matches!(&e.right, Expression::NumericLiteral(r) if r.value == 0.0 && r.value.is_sign_positive())
&& matches!(&e.left, Expression::NumericLiteral(l) if l.value.abs() == 1.0)
&& !matches!(&ctx.parent(), Ancestor::UnaryExpressionArgument(unary) if unary.operator().is_delete())
&& !Self::is_delete_argument(ctx)
{
return;
}
@@ -377,6 +377,10 @@ impl<'a> Normalize {
if !ctx.is_global_reference(ident) {
return None;
}
// `delete Number.NaN` is false (or throws in strict mode), but `delete NaN` is true.
if Self::is_unary_delete_ancestor(ctx.ancestors()) {
return None;
}
Some(Expression::new_nan(ident.span, ctx))
}
@@ -544,7 +544,8 @@ impl<'a> PeepholeOptimizations {
if matches!(
ctx.parent(),
Ancestor::CallExpressionCallee(_) | Ancestor::TaggedTemplateExpressionTag(_)
) {
) || Self::is_delete_argument(ctx)
{
return;
}
@@ -1225,6 +1225,16 @@ fn test_fold_integer_index_access() {
test_same_value("'abc'[-1n]");
test_same_value("'abc'[9007199254740992n]");
// `delete` of a non-configurable property is false, or a TypeError in strict mode
test_same("v = delete 'a'[0]");
test_same("v = delete '😀'[0]");
test_same("v = delete 'abc'.length");
test("v = delete 'abc'['length']", "v = delete 'abc'.length");
test_same("v = delete [1, 2].length");
test_same("v = delete Number.MAX_SAFE_INTEGER");
test_same("v = delete Number.NaN");
test("v = delete Number['NaN']", "v = delete Number.NaN");
// property access should be kept to keep `this` value
test_same(
"