fix(ecmascript): trim trailing whitespace in string to number (#26148)

## Summary

`StringToNumber` previously removed ECMAScript whitespace only from the beginning of a string. Legal trailing whitespace therefore remained in the value passed to Rust’s numeric parsers, causing valid JavaScript numeric strings to produce `NaN`.

For example:

```js
"1 " * 2       // incorrectly folded to NaN
"0x10 " - 0    // incorrectly folded to NaN
```

This changes `trim_start_matches` to `trim_matches`, applying the existing ECMAScript whitespace predicate to both ends of the string.

After the change:

```js
"1 " * 2       // folds to 2
"0x10 " - 0    // folds to 16
```

The custom predicate remains important because ECMAScript and Rust define whitespace differently. Tests cover every ECMAScript whitespace character and verify that non-ECMAScript whitespace such as U+0085 is not accepted.
This commit is contained in:
camc314 committed 2026-08-28 14:07:56 +00:00
1 parent 243b685c3d
commit dc7398beae
2 files changed
+27 -1

No files matched your search

+25 -1
View File
@@ -9,7 +9,7 @@ pub trait StringToNumber {
/// <https://tc39.es/ecma262/#sec-stringtonumber>
impl StringToNumber for &str {
fn string_to_number(&self) -> f64 {
let s = self.trim_start_matches(is_str_white_space_char);
let s = self.trim_matches(is_str_white_space_char);
match s {
"" => return 0.0,
"-Infinity" => return f64::NEG_INFINITY,
@@ -74,3 +74,27 @@ impl StringToNumber for &str {
fn is_str_white_space_char(c: char) -> bool {
is_white_space(c) || is_line_terminator(c)
}
#[cfg(test)]
mod tests {
use super::StringToNumber;
#[expect(clippy::float_cmp)]
#[test]
fn trims_ecmascript_whitespace_from_both_ends() {
const WHITESPACE: &str = "\u{0009}\u{000B}\u{000C}\u{0020}\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{202F}\u{205F}\u{3000}\u{FEFF}\u{000A}\u{000D}\u{2028}\u{2029}";
assert_eq!(format!("{WHITESPACE}1{WHITESPACE}").as_str().string_to_number(), 1.0);
assert_eq!(format!("{WHITESPACE}0x10{WHITESPACE}").as_str().string_to_number(), 16.0);
assert_eq!(
format!("{WHITESPACE}Infinity{WHITESPACE}").as_str().string_to_number(),
f64::INFINITY
);
assert_eq!(format!("{WHITESPACE}{WHITESPACE}").as_str().string_to_number(), 0.0);
}
#[test]
fn does_not_trim_non_ecmascript_whitespace() {
assert!("1\u{0085}".string_to_number().is_nan());
}
}
@@ -1039,11 +1039,13 @@ fn test_fold_numeric_expression_only_if_shorter() {
#[test]
fn test_fold_sub() {
fold("x = 10 - 20", "x = -10");
fold("x = '0x10 ' - 0", "x = 16");
}
#[test]
fn test_fold_multiply() {
fold("x = 2.25 * 3", "x = 6.75");
fold("x = '1 ' * 2", "x = 2");
fold_same("z = x * y");
fold_same("x = f() * 2");
fold_same("x = y * 5");