diff --git a/crates/oxc_ecmascript/src/string_char_at.rs b/crates/oxc_ecmascript/src/string_char_at.rs index fc018f1ee4129..c155fed42b516 100644 --- a/crates/oxc_ecmascript/src/string_char_at.rs +++ b/crates/oxc_ecmascript/src/string_char_at.rs @@ -9,26 +9,32 @@ pub trait StringCharAt { 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); + let index = index.unwrap_or(0.0); + if index.fract() != 0.0 || index.is_nan() || index.is_infinite() { + return None; + } + let index = index.to_int_32() as isize; if index < 0 { None } else { - self.chars().nth(index as usize) + self.encode_utf16().nth(index as usize).and_then(|n| char::from_u32(u32::from(n))) } } } #[cfg(test)] mod test { + use super::StringCharAt; #[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); + let s = "test"; + assert_eq!(s.char_at(Some(0.0)), Some('t')); + assert_eq!(s.char_at(Some(1.0)), Some('e')); + assert_eq!(s.char_at(Some(2.0)), Some('s')); + assert_eq!(s.char_at(Some(0.5)), None); + assert_eq!(s.char_at(Some(-1.0)), None); + assert_eq!(s.char_at(Some(-1.1)), None); + assert_eq!(s.char_at(Some(-1_073_741_825.0)), None); } } diff --git a/crates/oxc_ecmascript/src/string_char_code_at.rs b/crates/oxc_ecmascript/src/string_char_code_at.rs index 59f5eb36d33d8..5f110e2daf625 100644 --- a/crates/oxc_ecmascript/src/string_char_code_at.rs +++ b/crates/oxc_ecmascript/src/string_char_code_at.rs @@ -14,19 +14,21 @@ impl StringCharCodeAt for &str { #[cfg(test)] mod test { + use super::StringCharCodeAt; #[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)); + let s = "abcde"; + assert_eq!(s.char_code_at(Some(0.0)), Some(97)); + assert_eq!(s.char_code_at(Some(1.0)), Some(98)); + assert_eq!(s.char_code_at(Some(2.0)), Some(99)); + assert_eq!(s.char_code_at(Some(3.0)), Some(100)); + assert_eq!(s.char_code_at(Some(4.0)), Some(101)); + assert_eq!(s.char_code_at(Some(5.0)), None); + assert_eq!(s.char_code_at(Some(-1.0)), None); + assert_eq!(s.char_code_at(None), Some(97)); + assert_eq!(s.char_code_at(Some(0.0)), Some(97)); + assert_eq!(s.char_code_at(Some(f64::NAN)), None); + assert_eq!(s.char_code_at(Some(f64::INFINITY)), 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 435534cbf3ec9..93628ddbd70ab 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 @@ -185,6 +185,7 @@ impl<'a> PeepholeReplaceKnownMethods { Some(ctx.ast.expression_string_literal(ce.span, result, None)) } + #[expect(clippy::cast_lossless)] fn try_fold_string_char_code_at( ce: &CallExpression<'a>, object: &Expression<'a>, @@ -196,10 +197,14 @@ impl<'a> PeepholeReplaceKnownMethods { Some(Argument::SpreadElement(_)) => None, Some(e) => Ctx(ctx).get_side_free_number_value(e.to_expression()), }?; - // TODO: if `result` is `None`, return `NaN` instead of skipping the optimization - let result = s.value.as_str().char_code_at(Some(char_at_index))?; - #[expect(clippy::cast_lossless)] - Some(ctx.ast.expression_numeric_literal(ce.span, result as f64, None, NumberBase::Decimal)) + let value = if (0.0..65536.0).contains(&char_at_index) { + s.value.as_str().char_code_at(Some(char_at_index))? as f64 + } else if char_at_index.is_nan() || char_at_index.is_infinite() { + return None; + } else { + f64::NAN + }; + Some(ctx.ast.expression_numeric_literal(ce.span, value, None, NumberBase::Decimal)) } fn try_fold_string_replace( @@ -606,18 +611,15 @@ mod test { fold("x = 'abcde'.charCodeAt(2)", "x = 99"); fold("x = 'abcde'.charCodeAt(3)", "x = 100"); fold("x = 'abcde'.charCodeAt(4)", "x = 101"); - 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(5)"); + fold("x = 'abcde'.charCodeAt(-1)", "x = NaN"); fold_same("x = 'abcde'.charCodeAt(y)"); - // Seems that it does not handle this case - // fold("x = 'abcde'.charCodeAt()", "x = 97"); + 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)"); fold_same("x = `abcdef ${abc}`.charCodeAt(0)"); }