mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
1 parent
aa7642ad31
commit
4c0fa90abe
6 files changed
+58
-3
No files matched your search
@@ -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(
|
||||
"
|
||||
|
||||
Reference in new issue
Block a user