mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(minifier): fold .toString() (#8308)
This commit is contained in:
parent
30cee0e47e
commit
922c5147b5
5 changed files with 133 additions and 34 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,13 @@ impl<'a> ToJsString<'a> for IdentifierReference<'a> {
|
|||
impl<'a> ToJsString<'a> for NumericLiteral<'a> {
|
||||
fn to_js_string(&self) -> Option<Cow<'a, str>> {
|
||||
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 })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Expression<'a>> {
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -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<Expression<'a>> {
|
||||
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: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
|
||||
|
|
@ -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)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue