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 df7fc773325f6..17eba2fcd6fe6 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 @@ -1,8 +1,11 @@ +use std::borrow::Cow; + use cow_utils::CowUtils; + use oxc_ast::ast::*; use oxc_ecmascript::{ constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf, - StringLastIndexOf, StringSubstring, + StringLastIndexOf, StringSubstring, ToInt32, }; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; @@ -27,77 +30,53 @@ impl<'a> Traverse<'a> for PeepholeReplaceKnownMethods { } } -impl PeepholeReplaceKnownMethods { +impl<'a> PeepholeReplaceKnownMethods { pub fn new() -> Self { Self { changed: false } } - fn try_fold_known_string_methods<'a>( + fn try_fold_known_string_methods( &mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>, ) { - let Expression::CallExpression(call_expr) = node else { return }; - - let Expression::StaticMemberExpression(member) = &call_expr.callee else { return }; - if let Expression::StringLiteral(string_lit) = &member.object { - let replacement = match member.property.name.as_str() { - "toLowerCase" | "toUpperCase" | "trim" => match member.property.name.as_str() { - "toLowerCase" => Some(ctx.ast.expression_string_literal( - call_expr.span, - string_lit.value.cow_to_lowercase(), - None, - )), - "toUpperCase" => Some(ctx.ast.expression_string_literal( - call_expr.span, - string_lit.value.cow_to_uppercase(), - None, - )), - "trim" => Some(ctx.ast.expression_string_literal( - call_expr.span, - string_lit.value.trim(), - None, - )), - _ => None, - }, - "indexOf" | "lastIndexOf" => Self::try_fold_string_index_of( - call_expr.span, - call_expr, - member, - string_lit, - ctx, - ), - "substring" | "slice" => Self::try_fold_string_substring_or_slice( - call_expr.span, - call_expr, - string_lit, - ctx, - ), - "charAt" => { - Self::try_fold_string_char_at(call_expr.span, call_expr, string_lit, ctx) - } - "charCodeAt" => { - Self::try_fold_string_char_code_at(call_expr.span, call_expr, string_lit, ctx) + let Expression::CallExpression(ce) = node else { return }; + let Expression::StaticMemberExpression(member) = &ce.callee else { return }; + let replacement = match &member.object { + Expression::StringLiteral(s) => match member.property.name.as_str() { + "toLowerCase" | "toUpperCase" | "trim" => { + let value = match member.property.name.as_str() { + "toLowerCase" => s.value.cow_to_lowercase(), + "toUpperCase" => s.value.cow_to_uppercase(), + "trim" => Cow::Borrowed(s.value.trim()), + _ => return, + }; + Some(ctx.ast.expression_string_literal(ce.span, value, None)) } - "replace" | "replaceAll" => Self::try_fold_string_replace_or_string_replace_all( - call_expr.span, - call_expr, - member, - string_lit, - ctx, - ), + "indexOf" | "lastIndexOf" => Self::try_fold_string_index_of(ce, member, s, ctx), + "substring" | "slice" => Self::try_fold_string_substring_or_slice(ce, s, ctx), + "charAt" => Self::try_fold_string_char_at(ce, s, ctx), + "charCodeAt" => Self::try_fold_string_char_code_at(ce, s, ctx), + "replace" | "replaceAll" => Self::try_fold_string_replace(ce, member, s, ctx), _ => None, - }; - - if let Some(replacement) = replacement { - self.changed = true; - *node = replacement; + }, + Expression::Identifier(ident) + if ident.name == "String" + && member.property.name == "fromCharCode" + && Ctx(ctx).is_global_reference(ident) => + { + Self::try_fold_string_from_char_code(ce, ctx) } + _ => None, + }; + + if let Some(replacement) = replacement { + self.changed = true; + *node = replacement; } } - fn try_fold_string_index_of<'a>( - span: Span, + fn try_fold_string_index_of( call_expr: &CallExpression<'a>, member: &StaticMemberExpression<'a>, string_lit: &StringLiteral<'a>, @@ -108,13 +87,11 @@ impl PeepholeReplaceKnownMethods { None => None, _ => return None, }; - let search_start_index = match call_expr.arguments.get(1) { Some(Argument::NumericLiteral(numeric_lit)) => Some(numeric_lit.value), None => None, _ => return None, }; - let result = match member.property.name.as_str() { "indexOf" => string_lit.value.as_str().index_of(search_value, search_start_index), "lastIndexOf" => { @@ -122,13 +99,12 @@ impl PeepholeReplaceKnownMethods { } _ => unreachable!(), }; - + let span = call_expr.span; #[expect(clippy::cast_precision_loss)] Some(ctx.ast.expression_numeric_literal(span, result as f64, None, NumberBase::Decimal)) } - fn try_fold_string_substring_or_slice<'a>( - span: Span, + fn try_fold_string_substring_or_slice( call_expr: &CallExpression<'a>, string_lit: &StringLiteral<'a>, ctx: &mut TraverseCtx<'a>, @@ -136,7 +112,7 @@ impl PeepholeReplaceKnownMethods { if call_expr.arguments.len() > 2 { return None; } - + let span = call_expr.span; let start_idx = call_expr.arguments.first().and_then(|arg| match arg { Argument::SpreadElement(_) => None, _ => Ctx(ctx).get_side_free_number_value(arg.to_expression()), @@ -145,20 +121,17 @@ impl PeepholeReplaceKnownMethods { Argument::SpreadElement(_) => None, _ => Ctx(ctx).get_side_free_number_value(arg.to_expression()), }); - #[expect(clippy::cast_precision_loss)] if start_idx.is_some_and(|start| start > string_lit.value.len() as f64 || start < 0.0) || end_idx.is_some_and(|end| end > string_lit.value.len() as f64 || end < 0.0) { return None; } - if let (Some(start), Some(end)) = (start_idx, end_idx) { if start > end { return None; } }; - Some(ctx.ast.expression_string_literal( span, string_lit.value.as_str().substring(start_idx, end_idx), @@ -166,8 +139,7 @@ impl PeepholeReplaceKnownMethods { )) } - fn try_fold_string_char_at<'a>( - span: Span, + fn try_fold_string_char_at( call_expr: &CallExpression<'a>, string_lit: &StringLiteral<'a>, ctx: &mut TraverseCtx<'a>, @@ -175,7 +147,7 @@ impl PeepholeReplaceKnownMethods { if call_expr.arguments.len() > 1 { return None; } - + let span = call_expr.span; let char_at_index: Option = match call_expr.arguments.first() { Some(Argument::NumericLiteral(numeric_lit)) => Some(numeric_lit.value), Some(Argument::UnaryExpression(unary_expr)) @@ -199,8 +171,7 @@ impl PeepholeReplaceKnownMethods { Some(ctx.ast.expression_string_literal(span, result, None)) } - fn try_fold_string_char_code_at<'a>( - span: Span, + fn try_fold_string_char_code_at( call_expr: &CallExpression<'a>, string_lit: &StringLiteral<'a>, ctx: &mut TraverseCtx<'a>, @@ -209,16 +180,14 @@ impl PeepholeReplaceKnownMethods { Argument::SpreadElement(_) => None, _ => Ctx(ctx).get_side_free_number_value(arg.to_expression()), })?; - + let span = call_expr.span; // TODO: if `result` is `None`, return `NaN` instead of skipping the optimization let result = string_lit.value.as_str().char_code_at(Some(char_at_index))?; - #[expect(clippy::cast_lossless)] Some(ctx.ast.expression_numeric_literal(span, result as f64, None, NumberBase::Decimal)) } - fn try_fold_string_replace_or_string_replace_all<'a>( - span: Span, + fn try_fold_string_replace( call_expr: &CallExpression<'a>, member: &StaticMemberExpression<'a>, string_lit: &StringLiteral<'a>, @@ -227,7 +196,7 @@ impl PeepholeReplaceKnownMethods { if call_expr.arguments.len() != 2 { return None; } - + let span = call_expr.span; let search_value = call_expr.arguments.first().unwrap(); let search_value = match search_value { Argument::SpreadElement(_) => return None, @@ -242,11 +211,9 @@ impl PeepholeReplaceKnownMethods { Ctx(ctx).get_side_free_string_value(replace_value.to_expression())? } }; - if replace_value.contains('$') { return None; } - let result = match member.property.name.as_str() { "replace" => { string_lit.value.as_str().cow_replacen(search_value.as_ref(), &replace_value, 1) @@ -256,9 +223,33 @@ impl PeepholeReplaceKnownMethods { } _ => unreachable!(), }; - Some(ctx.ast.expression_string_literal(span, result, None)) } + + fn try_fold_string_from_char_code( + ce: &CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let args = &ce.arguments; + if args.iter().any(|arg| !matches!(arg, Argument::NumericLiteral(_))) { + return None; + } + let mut s = String::with_capacity(args.len()); + for arg in args { + let Argument::NumericLiteral(lit) = arg else { unreachable!() }; + if lit.value.is_nan() || lit.value.is_infinite() { + return None; + } + let v = lit.value.to_int_32(); + if v >= 65535 { + return None; + } + let Ok(v) = u32::try_from(v) else { return None }; + let Ok(c) = char::try_from(v) else { return None }; + s.push(c); + } + Some(ctx.ast.expression_string_literal(ce.span, s, None)) + } } /// Port from: @@ -1100,4 +1091,25 @@ mod test { let right = "function f(/** string */ a) {".to_string() + expected + "}"; test(left.as_str(), right.as_str()); } + + #[test] + fn test_fold_string_from_char_code() { + test("String.fromCharCode()", "''"); + test("String.fromCharCode(0)", "'\\0'"); + test("String.fromCharCode(120)", "'x'"); + test("String.fromCharCode(120, 121)", "'xy'"); + test_same("String.fromCharCode(55358, 56768)"); + test("String.fromCharCode(0x10000)", "String.fromCharCode(65536)"); + test("String.fromCharCode(0x10078, 0x10079)", "String.fromCharCode(0x10078, 0x10079)"); + test("String.fromCharCode(0x1_0000_FFFF)", "String.fromCharCode(4295032831)"); + test_same("String.fromCharCode(NaN)"); + test_same("String.fromCharCode(-Infinity)"); + test_same("String.fromCharCode(Infinity)"); + test_same("String.fromCharCode(null)"); + test_same("String.fromCharCode(undefined)"); + test_same("String.fromCharCode('123')"); + test_same("String.fromCharCode(x)"); + test_same("String.fromCharCode('x')"); + test_same("String.fromCharCode('0.5')"); + } }