diff --git a/crates/oxc_ecmascript/src/to_boolean.rs b/crates/oxc_ecmascript/src/to_boolean.rs index b210291f94285..d1e051a5de2ad 100644 --- a/crates/oxc_ecmascript/src/to_boolean.rs +++ b/crates/oxc_ecmascript/src/to_boolean.rs @@ -28,7 +28,13 @@ impl<'a> ToBoolean<'a> for Expression<'a> { | Expression::ObjectExpression(_) => Some(true), Expression::NullLiteral(_) => Some(false), Expression::BooleanLiteral(boolean_literal) => Some(boolean_literal.value), - Expression::NumericLiteral(number_literal) => Some(number_literal.value != 0.0), + Expression::NumericLiteral(lit) => Some({ + if lit.value.is_nan() { + false + } else { + lit.value != 0.0 + } + }), Expression::BigIntLiteral(big_int_literal) => Some(!big_int_literal.is_zero()), Expression::StringLiteral(string_literal) => Some(!string_literal.value.is_empty()), Expression::TemplateLiteral(template_literal) => { diff --git a/crates/oxc_ecmascript/src/to_string.rs b/crates/oxc_ecmascript/src/to_string.rs index f3f23817fffc0..c2beb2fea1f84 100644 --- a/crates/oxc_ecmascript/src/to_string.rs +++ b/crates/oxc_ecmascript/src/to_string.rs @@ -79,7 +79,13 @@ impl<'a> ToJsString<'a> for IdentifierReference<'a> { impl<'a> ToJsString<'a> for NumericLiteral<'a> { fn to_js_string(&self) -> Option> { use oxc_syntax::number::ToJsString; - Some(Cow::Owned(self.value.to_js_string())) + let value = self.value; + let s = value.to_js_string(); + Some(if value == 0.0 { + Cow::Borrowed("0") + } else { + Cow::Owned(if value.is_sign_negative() && value != 0.0 { format!("-{s}") } else { s }) + }) } } diff --git a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs index 1827cb3cdeeb6..63e5c02f54c6e 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs @@ -2,7 +2,6 @@ use oxc_ast::ast::*; use oxc_ecmascript::{ constant_evaluation::{ConstantEvaluation, ConstantValue, ValueType}, side_effects::MayHaveSideEffects, - ToJsString, }; use oxc_span::{GetSpan, SPAN}; use oxc_syntax::{ @@ -37,8 +36,7 @@ impl<'a> Traverse<'a> for PeepholeFoldConstants { Expression::StaticMemberExpression(e) => Self::try_fold_static_member_expr(e, ctx), Expression::LogicalExpression(e) => Self::try_fold_logical_expr(e, ctx), Expression::ChainExpression(e) => Self::try_fold_optional_chain(e, ctx), - Expression::CallExpression(e) => Self::try_fold_number_constructor(e, ctx) - .or_else(|| Self::try_fold_to_string(e, ctx)), + Expression::CallExpression(e) => Self::try_fold_number_constructor(e, ctx), _ => None, } { *expr = folded_expr; @@ -606,26 +604,6 @@ impl<'a, 'b> PeepholeFoldConstants { )) } - fn try_fold_to_string(e: &CallExpression<'a>, ctx: Ctx<'a, 'b>) -> Option> { - let Expression::StaticMemberExpression(member_expr) = &e.callee else { return None }; - if member_expr.property.name != "toString" { - return None; - } - if !e.arguments.is_empty() { - return None; - } - let object = &member_expr.object; - if !matches!( - ValueType::from(object), - ValueType::String | ValueType::Boolean | ValueType::Number - ) { - return None; - } - object - .to_js_string() - .map(|value| ctx.ast.expression_string_literal(object.span(), value, None)) - } - // `typeof a === typeof b` -> `typeof a == typeof b`, `typeof a != typeof b` -> `typeof a != typeof b`, // `typeof a == typeof a` -> `true`, `typeof a != typeof a` -> `false` fn try_fold_binary_typeof_comparison( @@ -1829,14 +1807,6 @@ mod test { test_same("var Number; Number(1)"); } - #[test] - fn test_fold_to_string() { - test("'x'.toString()", "'x'"); - test("1 .toString()", "'1'"); - test("true.toString()", "'true'"); - test("false.toString()", "'false'"); - } - #[test] fn test_fold_typeof_addition_string() { test_same("typeof foo"); diff --git a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs index 12739ab2b9ded..7bd23a51fc14d 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs @@ -53,6 +53,7 @@ impl<'a> PeepholeReplaceKnownMethods { "charCodeAt" => Self::try_fold_string_char_code_at(ce, member, ctx), "replace" | "replaceAll" => Self::try_fold_string_replace(ce, member, ctx), "fromCharCode" => Self::try_fold_string_from_char_code(ce, member, ctx), + "toString" => Self::try_fold_to_string(ce, member, ctx), _ => None, }; if let Some(replacement) = replacement { @@ -246,6 +247,81 @@ impl<'a> PeepholeReplaceKnownMethods { } Some(ctx.ast.expression_string_literal(ce.span, s, None)) } + + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_lossless, + clippy::float_cmp + )] + fn try_fold_to_string( + ce: &CallExpression<'a>, + member: &StaticMemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let args = &ce.arguments; + match &member.object { + // Number.prototype.toString() + // Number.prototype.toString(radix) + Expression::NumericLiteral(lit) if args.len() <= 1 => { + let mut radix: u32 = 0; + if args.is_empty() { + radix = 10; + } + if let Some(Argument::NumericLiteral(n)) = args.first() { + if n.value >= 2.0 && n.value <= 36.0 && n.value.fract() == 0.0 { + radix = n.value as u32; + } + } + if radix == 0 { + return None; + } + if lit.value.is_nan() { + return Some(ctx.ast.expression_string_literal(ce.span, "NaN", None)); + } + if lit.value.is_infinite() { + return Some(ctx.ast.expression_string_literal(ce.span, "Infinity", None)); + } + if radix == 10 { + use oxc_syntax::number::ToJsString; + return Some(ctx.ast.expression_string_literal( + ce.span, + lit.value.to_js_string(), + None, + )); + } + // Only convert integers for other radix values. + let value = lit.value; + if value >= 0.0 && value.fract() != 0.0 { + return None; + } + let i = value as u32; + if i as f64 != value { + return None; + } + Some(ctx.ast.expression_string_literal(ce.span, Self::format_radix(i, radix), None)) + } + e if e.is_literal() && args.is_empty() => { + use oxc_ecmascript::ToJsString; + e.to_js_string().map(|s| ctx.ast.expression_string_literal(ce.span, s, None)) + } + _ => None, + } + } + + fn format_radix(mut x: u32, radix: u32) -> String { + debug_assert!((2..=36).contains(&radix)); + let mut result = vec![]; + loop { + let m = x % radix; + x /= radix; + result.push(std::char::from_digit(m, radix).unwrap()); + if x == 0 { + break; + } + } + result.into_iter().rev().collect() + } } /// Port from: @@ -1109,4 +1185,45 @@ mod test { test("String.fromCharCode('x')", "'\\0'"); test("String.fromCharCode('0.5')", "'\\0'"); } + + #[test] + fn test_to_string() { + test("false.toString()", "'false';"); + test("true.toString()", "'true';"); + test("'xy'.toString()", "'xy';"); + test("0 .toString()", "'0';"); + test("123 .toString()", "'123';"); + test("NaN.toString()", "'NaN';"); + test("Infinity.toString()", "'Infinity';"); + // test("/a\\\\b/ig.toString()", "'/a\\\\\\\\b/ig';"); + + test("100 .toString(0)", "100 .toString(0)"); + test("100 .toString(1)", "100 .toString(1)"); + test("100 .toString(2)", "'1100100'"); + test("100 .toString(5)", "'400'"); + test("100 .toString(8)", "'144'"); + test("100 .toString(13)", "'79'"); + test("100 .toString(16)", "'64'"); + test("10000 .toString(19)", "'18d6'"); + test("10000 .toString(23)", "'iki'"); + test("1000000 .toString(29)", "'1c01m'"); + test("1000000 .toString(31)", "'12hi2'"); + test("1000000 .toString(36)", "'lfls'"); + test("0 .toString(36)", "'0'"); + test("0.5.toString()", "'0.5'"); + + test("false.toString(b)", "false.toString(b)"); + test("true.toString(b)", "true.toString(b)"); + test("'xy'.toString(b)", "'xy'.toString(b)"); + test("123 .toString(b)", "123 .toString(b)"); + test("1e99.toString(b)", "1e99.toString(b)"); + test("/./.toString(b)", "/./.toString(b)"); + + // Will get constant folded into positive values + test_same("(-0).toString()"); + test_same("(-123).toString()"); + test_same("(-Infinity).toString()"); + test_same("(-1000000).toString(36)"); + test_same("(-0).toString(36)"); + } } diff --git a/crates/oxc_minifier/src/node_util/mod.rs b/crates/oxc_minifier/src/node_util/mod.rs index 04ea6dd1aa077..d1fea52b3ff51 100644 --- a/crates/oxc_minifier/src/node_util/mod.rs +++ b/crates/oxc_minifier/src/node_util/mod.rs @@ -77,7 +77,7 @@ impl<'a> Ctx<'a, '_> { } pub fn is_identifier_nan(self, ident: &IdentifierReference) -> bool { - if ident.name == "Infinity" && ident.is_global_reference(self.symbols()) { + if ident.name == "NaN" && ident.is_global_reference(self.symbols()) { return true; } false