diff --git a/crates/oxc_ecmascript/src/lib.rs b/crates/oxc_ecmascript/src/lib.rs index 86fb5aed24f483..1f4f711d5674b4 100644 --- a/crates/oxc_ecmascript/src/lib.rs +++ b/crates/oxc_ecmascript/src/lib.rs @@ -7,6 +7,7 @@ mod private_bound_identifiers; mod prop_name; // Abstract Operations +mod string_char_at; mod string_index_of; mod string_last_index_of; mod to_int_32; @@ -14,5 +15,6 @@ mod to_int_32; pub use self::{ bound_names::BoundNames, is_simple_parameter_list::IsSimpleParameterList, private_bound_identifiers::PrivateBoundIdentifiers, prop_name::PropName, - string_index_of::StringIndexOf, string_last_index_of::StringLastIndexOf, to_int_32::ToInt32, + string_char_at::StringCharAt, string_index_of::StringIndexOf, + string_last_index_of::StringLastIndexOf, to_int_32::ToInt32, }; diff --git a/crates/oxc_ecmascript/src/string_char_at.rs b/crates/oxc_ecmascript/src/string_char_at.rs new file mode 100644 index 00000000000000..fc018f1ee4129f --- /dev/null +++ b/crates/oxc_ecmascript/src/string_char_at.rs @@ -0,0 +1,34 @@ +use crate::ToInt32; + +pub trait StringCharAt { + /// `String.prototype.charAt ( pos )` + /// + fn char_at(&self, index: Option) -> Option; +} + +impl StringCharAt for &str { + #[expect(clippy::cast_sign_loss)] + fn char_at(&self, index: Option) -> Option { + let index = index.map_or(0, |x| x.to_int_32() as isize); + if index < 0 { + None + } else { + self.chars().nth(index as usize) + } + } +} + +#[cfg(test)] +mod test { + + #[test] + fn test_evaluate_string_char_at() { + use crate::string_char_at::StringCharAt; + assert_eq!("test".char_at(Some(0.0)), Some('t')); + assert_eq!("test".char_at(Some(1.0)), Some('e')); + assert_eq!("test".char_at(Some(2.0)), Some('s')); + assert_eq!("test".char_at(Some(-1.0)), None); + assert_eq!("test".char_at(Some(-1.1)), None); + assert_eq!("test".char_at(Some(-1_073_741_825.0)), None); + } +} 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 286f8a22d1071d..27f0022527c945 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,7 +1,7 @@ use cow_utils::CowUtils; - use oxc_ast::ast::*; -use oxc_ecmascript::{StringIndexOf, StringLastIndexOf}; +use oxc_ast::ast::*; +use oxc_ecmascript::{StringCharAt, StringIndexOf, StringLastIndexOf}; use oxc_traverse::{Traverse, TraverseCtx}; use crate::CompressorPass; @@ -76,7 +76,9 @@ impl PeepholeReplaceKnownMethods { // TODO: Implement the rest of the string methods "substr" => None, "substring" | "slice" => None, - "charAt" => None, + "charAt" => { + Self::try_fold_string_char_at(call_expr.span, call_expr, string_lit, ctx) + } "charCodeAt" => None, "replace" => None, "replaceAll" => None, @@ -125,6 +127,35 @@ impl PeepholeReplaceKnownMethods { NumberBase::Decimal, ))); } + + fn try_fold_string_char_at<'a>( + span: Span, + call_expr: &CallExpression<'a>, + string_lit: &StringLiteral<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let char_at_index: Option = match call_expr.arguments.first() { + Some(Argument::NumericLiteral(numeric_lit)) => Some(numeric_lit.value), + Some(Argument::UnaryExpression(unary_expr)) + if unary_expr.operator == UnaryOperator::UnaryNegation => + { + let Expression::NumericLiteral(numeric_lit) = &unary_expr.argument else { + return None; + }; + Some(-(numeric_lit.value)) + } + None => None, + _ => return None, + }; + + let result = &string_lit + .value + .as_str() + .char_at(char_at_index) + .map_or(String::new(), |v| v.to_string()); + + return Some(ctx.ast.expression_from_string_literal(ctx.ast.string_literal(span, result))); + } } /// Port from: @@ -382,18 +413,19 @@ mod test { } #[test] - #[ignore] fn test_fold_string_char_at() { fold("x = 'abcde'.charAt(0)", "x = 'a'"); fold("x = 'abcde'.charAt(1)", "x = 'b'"); fold("x = 'abcde'.charAt(2)", "x = 'c'"); fold("x = 'abcde'.charAt(3)", "x = 'd'"); fold("x = 'abcde'.charAt(4)", "x = 'e'"); - fold_same("x = 'abcde'.charAt(5)"); // or x = '' - fold_same("x = 'abcde'.charAt(-1)"); // or x = '' + // 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(y)"); - fold_same("x = 'abcde'.charAt()"); // or x = 'a' - fold_same("x = 'abcde'.charAt(0, ++z)"); // or (++z, 'a') fold_same("x = 'abcde'.charAt(null)"); // or x = 'a' fold_same("x = 'abcde'.charAt(true)"); // or x = 'b' // fold("x = '\\ud834\udd1e'.charAt(0)", "x = '\\ud834'");