mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
fix(ecmascript): derive ToNumber of !x from ToBoolean (#25595)
Found by the semantic fuzzer added in #25594 (seed `15979`). ## What `ToNumber(!x)` was computed from `ToNumber(x)` instead of `ToBoolean(x)`: ```rust Expression::UnaryExpression(unary) if unary.operator.is_not() => { let number = unary.argument.to_number(ctx)?; Some(if number == 0.0 { 1.0 } else { 0.0 }) } ``` `!x` is `ToBoolean(x)` negated, so the two predicates must agree — and they don't. `NaN` is falsy but does not number to `0`, while `"0"`, `" "`, `"0.0"` and `[]` all number to `0` yet are truthy. ## Why it was not caught `minimize_conditional_expression` rewrites `a ? 1 : 0` into `+!!a`. That rewrite is correct, but when a parent expression evaluated the freshly built `+!!a` in the same pass, the broken branch flipped the result: ```js (NaN ? 1 : 0) || 2 // 2, minified to 0 ('0' ? 1 : 0) || 2 // 1, minified to 2 ([] ? 1 : 0) || 2 // 1, minified to 2 ``` A `!!x` written in the source is folded correctly by `fold_unary_expr` before any parent observes it, so only pass-generated nodes were affected. That is why existing tests and the conformance suites pass. refs https://github.com/oxc-project/oxc/pull/8267 ## How Derive the value from `to_boolean`. This is strictly more conservative: when `to_boolean` returns `None` the fold is skipped rather than answered wrongly. ## Testing - New regression test `test_conditional_to_unary_plus_uses_to_boolean` covering `NaN`, `"0"`, `" "`, `"0.0"` and `[]`. - `cargo test -p oxc_minifier -p oxc_ecmascript`, `cargo clippy --all-targets`, `cargo fmt --check` all pass. - `just minsize`: no snapshot change, so no size regression. ## AI disclosure This change was produced with AI assistance (Claude Code). I have reviewed and understand every line; the root cause and each minimal reproduction were verified by hand against Node.js. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
757f3d4113
commit
e82495ba54
2 files changed
+19
-3
No files matched your search
@@ -1,7 +1,7 @@
|
||||
use oxc_ast::ast::*;
|
||||
|
||||
use crate::{
|
||||
GlobalContext, StringToNumber, ToJsString,
|
||||
GlobalContext, StringToNumber, ToBoolean, ToJsString,
|
||||
to_primitive::maybe_object_with_to_primitive_related_properties_overridden,
|
||||
};
|
||||
|
||||
@@ -31,8 +31,8 @@ impl<'a> ToNumber<'a> for Expression<'a> {
|
||||
},
|
||||
Expression::StringLiteral(lit) => Some(lit.value.as_str().string_to_number()),
|
||||
Expression::UnaryExpression(unary) if unary.operator.is_not() => {
|
||||
let number = unary.argument.to_number(ctx)?;
|
||||
Some(if number == 0.0 { 1.0 } else { 0.0 })
|
||||
let boolean = unary.argument.to_boolean(ctx)?;
|
||||
Some(if boolean { 0.0 } else { 1.0 })
|
||||
}
|
||||
Expression::ObjectExpression(obj) => {
|
||||
// If `toString` / `valueOf` / `Symbol.toPrimitive` is not overridden,
|
||||
|
||||
@@ -163,3 +163,19 @@ fn test_minimize_conditional_boolean_value_context() {
|
||||
test("use(flag ? false : (value = touch()))", "use(flag ? !1 : value = touch())");
|
||||
test("use(a === b || c === d ? value : false)", "use(a === b || c === d ? value : !1)");
|
||||
}
|
||||
|
||||
/// `a ? 1 : 0` becomes `+!!a`, whose value is `ToBoolean(a)` coerced to a
|
||||
/// number. Deriving it from `ToNumber(a)` instead flips the result for every
|
||||
/// value where `ToNumber(a) === 0` and `ToBoolean(a)` disagree.
|
||||
#[test]
|
||||
fn test_conditional_to_unary_plus_uses_to_boolean() {
|
||||
// `ToNumber(NaN)` is not `0`, but `NaN` is falsy.
|
||||
test("x = (NaN ? 1 : 0) || 2", "x = 2");
|
||||
test("x = (NaN ? 1 : 0) && 2", "x = 0");
|
||||
// `ToNumber("0")` is `0`, but a non-empty string is truthy.
|
||||
test("x = ('0' ? 1 : 0) || 2", "x = 1");
|
||||
test("x = (' ' ? 1 : 0) || 2", "x = 1");
|
||||
test("x = ('0.0' ? 1 : 0) || 2", "x = 1");
|
||||
// `ToNumber([])` is `0`, but every object is truthy.
|
||||
test("x = ([] ? 1 : 0) || 2", "x = 1");
|
||||
}
|
||||
Reference in new issue
Block a user