diff --git a/crates/oxc_ecmascript/src/lib.rs b/crates/oxc_ecmascript/src/lib.rs index 770bc716eda59..ebc3612fa1d13 100644 --- a/crates/oxc_ecmascript/src/lib.rs +++ b/crates/oxc_ecmascript/src/lib.rs @@ -8,6 +8,7 @@ mod prop_name; // Abstract Operations mod string_char_at; +mod string_char_code_at; mod string_index_of; mod string_last_index_of; mod string_to_big_int; @@ -27,8 +28,8 @@ pub mod side_effects; pub use self::{ bound_names::BoundNames, is_simple_parameter_list::IsSimpleParameterList, private_bound_identifiers::PrivateBoundIdentifiers, prop_name::PropName, - string_char_at::StringCharAt, string_index_of::StringIndexOf, - string_last_index_of::StringLastIndexOf, string_to_big_int::StringToBigInt, - string_to_number::StringToNumber, to_big_int::ToBigInt, to_boolean::ToBoolean, - to_int_32::ToInt32, to_number::ToNumber, to_string::ToJsString, + string_char_at::StringCharAt, string_char_code_at::StringCharCodeAt, + string_index_of::StringIndexOf, string_last_index_of::StringLastIndexOf, + string_to_big_int::StringToBigInt, string_to_number::StringToNumber, to_big_int::ToBigInt, + to_boolean::ToBoolean, to_int_32::ToInt32, to_number::ToNumber, to_string::ToJsString, }; diff --git a/crates/oxc_ecmascript/src/string_char_code_at.rs b/crates/oxc_ecmascript/src/string_char_code_at.rs new file mode 100644 index 0000000000000..59f5eb36d33d8 --- /dev/null +++ b/crates/oxc_ecmascript/src/string_char_code_at.rs @@ -0,0 +1,32 @@ +use crate::StringCharAt; + +pub trait StringCharCodeAt { + /// `String.prototype.charCodeAt ( pos )` + /// + fn char_code_at(&self, index: Option) -> Option; +} + +impl StringCharCodeAt for &str { + fn char_code_at(&self, index: Option) -> Option { + self.char_at(index).map(|c| c as u32) + } +} + +#[cfg(test)] +mod test { + + #[test] + fn test_evaluate_char_code_at() { + use crate::StringCharCodeAt; + + assert_eq!("abcde".char_code_at(Some(0.0)), Some(97)); + assert_eq!("abcde".char_code_at(Some(1.0)), Some(98)); + assert_eq!("abcde".char_code_at(Some(2.0)), Some(99)); + assert_eq!("abcde".char_code_at(Some(3.0)), Some(100)); + assert_eq!("abcde".char_code_at(Some(4.0)), Some(101)); + assert_eq!("abcde".char_code_at(Some(5.0)), None); + assert_eq!("abcde".char_code_at(Some(-1.0)), None); + assert_eq!("abcde".char_code_at(None), Some(97)); + assert_eq!("abcde".char_code_at(Some(0.0)), Some(97)); + } +} 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 c89667e708e7a..9274c1fe8c0c9 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,9 +1,12 @@ use cow_utils::CowUtils; use oxc_ast::ast::*; -use oxc_ecmascript::{StringCharAt, StringIndexOf, StringLastIndexOf}; +use oxc_ecmascript::{ + constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf, + StringLastIndexOf, +}; use oxc_traverse::{Traverse, TraverseCtx}; -use crate::CompressorPass; +use crate::{node_util::Ctx, CompressorPass}; /// Minimize With Known Methods /// @@ -78,7 +81,9 @@ impl PeepholeReplaceKnownMethods { "charAt" => { Self::try_fold_string_char_at(call_expr.span, call_expr, string_lit, ctx) } - "charCodeAt" => None, + "charCodeAt" => { + Self::try_fold_string_char_code_at(call_expr.span, call_expr, string_lit, ctx) + } "replace" => None, "replaceAll" => None, _ => None, @@ -133,6 +138,10 @@ impl PeepholeReplaceKnownMethods { string_lit: &StringLiteral<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { + if call_expr.arguments.len() > 1 { + return None; + } + let char_at_index: Option = match call_expr.arguments.first() { Some(Argument::NumericLiteral(numeric_lit)) => Some(numeric_lit.value), Some(Argument::UnaryExpression(unary_expr)) @@ -155,6 +164,35 @@ impl PeepholeReplaceKnownMethods { return Some(ctx.ast.expression_from_string_literal(ctx.ast.string_literal(span, result))); } + + fn try_fold_string_char_code_at<'a>( + span: Span, + call_expr: &CallExpression<'a>, + string_lit: &StringLiteral<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let char_at_index = call_expr.arguments.first(); + let char_at_index = if let Some(v) = char_at_index { + let val = match v { + Argument::SpreadElement(_) => None, + _ => Ctx(ctx).get_side_free_number_value(v.to_expression()), + }?; + Some(val) + } else { + None + }; + + // TODO: if `result` is `None`, return `NaN` instead of skipping the optimization + let result = string_lit.value.as_str().char_code_at(char_at_index)?; + + #[expect(clippy::cast_lossless)] + Some(ctx.ast.expression_from_numeric_literal(ctx.ast.numeric_literal( + span, + result as f64, + result.to_string(), + NumberBase::Decimal, + ))) + } } /// Port from: @@ -418,12 +456,10 @@ mod test { fold("x = 'abcde'.charAt(2)", "x = 'c'"); fold("x = 'abcde'.charAt(3)", "x = 'd'"); fold("x = 'abcde'.charAt(4)", "x = 'e'"); - // START: note, the following test cases outputs differ from Google's fold("x = 'abcde'.charAt(5)", "x = ''"); fold("x = 'abcde'.charAt(-1)", "x = ''"); fold("x = 'abcde'.charAt()", "x = 'a'"); - fold("x = 'abcde'.charAt(0, ++z)", "x = 'a'"); - // END + fold_same("x = 'abcde'.charAt(0, ++z)"); fold_same("x = 'abcde'.charAt(y)"); fold_same("x = 'abcde'.charAt(null)"); // or x = 'a' fold_same("x = 'abcde'.charAt(true)"); // or x = 'b' @@ -436,7 +472,6 @@ mod test { } #[test] - #[ignore] fn test_fold_string_char_code_at() { fold("x = 'abcde'.charCodeAt(0)", "x = 97"); fold("x = 'abcde'.charCodeAt(1)", "x = 98"); @@ -446,12 +481,12 @@ mod test { fold_same("x = 'abcde'.charCodeAt(5)"); // or x = (0/0) fold_same("x = 'abcde'.charCodeAt(-1)"); // or x = (0/0) fold_same("x = 'abcde'.charCodeAt(y)"); - fold_same("x = 'abcde'.charCodeAt()"); // or x = 97 - fold_same("x = 'abcde'.charCodeAt(0, ++z)"); // or (++z, 97) - fold_same("x = 'abcde'.charCodeAt(null)"); // or x = 97 - fold_same("x = 'abcde'.charCodeAt(true)"); // or x = 98 - // fold("x = '\\ud834\udd1e'.charCodeAt(0)", "x = 55348"); - // fold("x = '\\ud834\udd1e'.charCodeAt(1)", "x = 56606"); + fold("x = 'abcde'.charCodeAt()", "x = 97"); + fold("x = 'abcde'.charCodeAt(0, ++z)", "x = 97"); + fold("x = 'abcde'.charCodeAt(null)", "x = 97"); + fold("x = 'abcde'.charCodeAt(true)", "x = 98"); + // fold("x = '\\ud834\udd1e'.charCodeAt(0)", "x = 55348"); + // fold("x = '\\ud834\udd1e'.charCodeAt(1)", "x = 56606"); // Template strings fold_same("x = `abcdef`.charCodeAt(0)");