feat(ecmascript): add global isNaN, isFinite, parseFloat, parseInt functions support to constant evaluation (#12954)

This commit is contained in:
Copilot authored and GitHub committed 2025-08-11 14:01:01 +08:00
1 parent e2bda360ef
commit 33c0e9f1e2
4 files changed
+359 -33

No files matched your search

@@ -25,7 +25,7 @@ use crate::{
use super::{ConstantEvaluation, ConstantEvaluationCtx, ConstantValue};
fn try_fold_url_related_function<'a>(
fn try_fold_global_functions<'a>(
ident: &IdentifierReference<'a>,
arguments: &Vec<'a, Argument<'a>>,
ctx: &impl ConstantEvaluationCtx<'a>,
@@ -39,6 +39,12 @@ fn try_fold_url_related_function<'a>(
"decodeURIComponent" if ctx.is_global_reference(ident) => {
try_fold_decode_uri_component(arguments, ctx)
}
"isNaN" if ctx.is_global_reference(ident) => try_fold_global_is_nan(arguments, ctx),
"isFinite" if ctx.is_global_reference(ident) => try_fold_global_is_finite(arguments, ctx),
"parseFloat" if ctx.is_global_reference(ident) => {
try_fold_global_parse_float(arguments, ctx)
}
"parseInt" if ctx.is_global_reference(ident) => try_fold_global_parse_int(arguments, ctx),
_ => None,
}
}
@@ -49,7 +55,7 @@ pub fn try_fold_known_global_methods<'a>(
ctx: &impl ConstantEvaluationCtx<'a>,
) -> Option<ConstantValue<'a>> {
if let Expression::Identifier(ident) = callee {
if let Some(result) = try_fold_url_related_function(ident, arguments, ctx) {
if let Some(result) = try_fold_global_functions(ident, arguments, ctx) {
return Some(result);
}
return None;
@@ -617,3 +623,259 @@ fn try_fold_decode_uri_component<'a>(
)?;
Some(ConstantValue::String(decoded))
}
fn try_fold_global_is_nan<'a>(
args: &Vec<'a, Argument<'a>>,
ctx: &impl ConstantEvaluationCtx<'a>,
) -> Option<ConstantValue<'a>> {
if args.is_empty() {
return Some(ConstantValue::Boolean(true));
}
if args.len() != 1 {
return None;
}
let arg = args.first().unwrap();
let expr = arg.as_expression()?;
let num = expr.get_side_free_number_value(ctx)?;
Some(ConstantValue::Boolean(num.is_nan()))
}
fn try_fold_global_is_finite<'a>(
args: &Vec<'a, Argument<'a>>,
ctx: &impl ConstantEvaluationCtx<'a>,
) -> Option<ConstantValue<'a>> {
if args.is_empty() {
return Some(ConstantValue::Boolean(false));
}
if args.len() != 1 {
return None;
}
let arg = args.first().unwrap();
let expr = arg.as_expression()?;
let num = expr.get_side_free_number_value(ctx)?;
Some(ConstantValue::Boolean(num.is_finite()))
}
fn try_fold_global_parse_float<'a>(
args: &Vec<'a, Argument<'a>>,
ctx: &impl ConstantEvaluationCtx<'a>,
) -> Option<ConstantValue<'a>> {
if args.is_empty() {
return Some(ConstantValue::Number(f64::NAN));
}
if args.len() != 1 {
return None;
}
let arg = args.first().unwrap();
let expr = arg.as_expression()?;
let input_string = expr.get_side_free_string_value(ctx)?;
let trimmed = input_string.trim_start();
let Some(trimmed_prefix) = find_str_decimal_literal_prefix(trimmed) else {
return Some(ConstantValue::Number(f64::NAN));
};
let parsed = trimmed_prefix.cow_replace('_', "").parse::<f64>().unwrap_or_else(|_| {
unreachable!(
"StrDecimalLiteral should be parse-able with Rust FromStr for f64: {trimmed_prefix}"
)
});
Some(ConstantValue::Number(parsed))
}
/// Find the longest prefix of a string that satisfies the syntax of a `StrDecimalLiteral`.
/// Returns None when not found.
///
/// This function implements step 4 of `parseFloat`.
/// <https://tc39.es/ecma262/2025/multipage/global-object.html#sec-parsefloat-string>
fn find_str_decimal_literal_prefix(input: &str) -> Option<&str> {
fn match_decimal_digits(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
if bytes.first().is_none_or(|b| !b.is_ascii_digit()) {
// must have at least one digit
return None;
}
let mut iter = bytes.iter().enumerate().skip(1);
while let Some((i, &b)) = iter.next() {
match b {
b'0'..=b'9' => {}
b'_' => {
let Some((i, &b)) = iter.next() else {
// must have at least one digit after _
return Some(i); // without _
};
if !b.is_ascii_digit() {
// must have at least one digit after _
return Some(i); // without _
}
}
_ => return Some(i),
}
}
Some(s.len())
}
fn match_exponent_part(mut s: &str) -> Option<usize> {
if !s.starts_with(['e', 'E']) {
return None;
}
let mut last_index = 1;
s = &s[1..];
if s.starts_with(['+', '-']) {
last_index += 1;
s = &s[1..];
}
let end_of_decimal_digits = match_decimal_digits(s)?;
last_index += end_of_decimal_digits;
Some(last_index)
}
let mut s = input;
let mut last_index: usize = 0;
if s.starts_with(['+', '-']) {
s = &s[1..];
last_index += 1;
}
if s.starts_with("Infinity") {
last_index += "Infinity".len();
return Some(&input[..last_index]);
}
// . DecimalDigits ExponentPart
if s.starts_with('.') {
last_index += 1;
s = &s[1..];
let end_of_decimal_digits = match_decimal_digits(s)?;
last_index += end_of_decimal_digits;
s = &s[end_of_decimal_digits..];
let Some(end_of_exponent_part) = match_exponent_part(s) else {
return Some(&input[..last_index]);
};
last_index += end_of_exponent_part;
return Some(&input[..last_index]);
}
let end_of_decimal_digits = match_decimal_digits(s)?;
last_index += end_of_decimal_digits;
s = &s[end_of_decimal_digits..];
// DecimalDigits . DecimalDigits ExponentPart
if s.starts_with('.') {
last_index += 1;
s = &s[1..];
let Some(end_of_decimal_digits) = match_decimal_digits(s) else {
return Some(&input[..last_index - 1]); // without .
};
last_index += end_of_decimal_digits;
s = &s[end_of_decimal_digits..];
let Some(end_of_exponent_part) = match_exponent_part(s) else {
return Some(&input[..last_index]);
};
last_index += end_of_exponent_part;
return Some(&input[..last_index]);
}
// DecimalDigits ExponentPart
let Some(end_of_exponent_part) = match_exponent_part(s) else {
return Some(&input[..last_index]);
};
last_index += end_of_exponent_part;
Some(&input[..last_index])
}
fn try_fold_global_parse_int<'a>(
args: &Vec<'a, Argument<'a>>,
ctx: &impl ConstantEvaluationCtx<'a>,
) -> Option<ConstantValue<'a>> {
if args.is_empty() {
return Some(ConstantValue::Number(f64::NAN));
}
if args.len() > 2
|| args
.iter()
.any(|arg| arg.as_expression().is_none_or(|arg| arg.may_have_side_effects(ctx)))
{
return None;
}
let string_arg = args.first().unwrap();
let string_expr = string_arg.as_expression()?;
let string_value = string_expr.evaluate_value_to_string(ctx)?;
let mut string_value = string_value.trim_start();
let mut sign = 1;
if string_value.starts_with('-') {
sign = -1;
}
if string_value.starts_with(['+', '-']) {
string_value = &string_value[1..];
}
let mut strip_prefix = true;
let mut radix = if let Some(arg) = args.get(1) {
let expr = arg.as_expression()?;
let mut radix = expr.evaluate_value_to_number(ctx)?.to_int_32();
if radix == 0 {
radix = 10;
} else if !(2..=36).contains(&radix) {
return Some(ConstantValue::Number(f64::NAN));
} else if radix != 16 {
strip_prefix = false;
}
radix as u32
} else {
10
};
if !matches!(radix, 2 | 4 | 8 | 10 | 16 | 32) {
// implementation can approximate the values. bail out to be safe
return None;
}
if strip_prefix && (string_value.starts_with("0x") || string_value.starts_with("0X")) {
string_value = &string_value[2..];
radix = 16;
}
if let Some(non_radix_digit_pos) = string_value.chars().position(|c| !c.is_digit(radix)) {
string_value = &string_value[..non_radix_digit_pos];
}
if string_value.is_empty() {
return Some(ConstantValue::Number(f64::NAN));
}
if radix == 10 && string_value.len() > 20 {
// implementation can approximate the values. bail out to be safe
return None;
}
let Ok(math_int) = i32::from_str_radix(string_value, radix) else {
// ignore values that cannot be represented as i32 to avoid precision issues
return None;
};
if math_int == 0 {
return Some(ConstantValue::Number(if sign == -1 { -0.0 } else { 0.0 }));
}
Some(ConstantValue::Number((math_int as f64) * sign as f64))
}
#[test]
fn test_find_str_decimal_literal_prefix() {
assert_eq!(find_str_decimal_literal_prefix("Infinitya"), Some("Infinity"));
assert_eq!(find_str_decimal_literal_prefix("+Infinitya"), Some("+Infinity"));
assert_eq!(find_str_decimal_literal_prefix("-Infinitya"), Some("-Infinity"));
assert_eq!(find_str_decimal_literal_prefix("0a"), Some("0"));
assert_eq!(find_str_decimal_literal_prefix("+0a"), Some("+0"));
assert_eq!(find_str_decimal_literal_prefix("-0a"), Some("-0"));
assert_eq!(find_str_decimal_literal_prefix("0."), Some("0"));
assert_eq!(find_str_decimal_literal_prefix("0.e"), Some("0"));
assert_eq!(find_str_decimal_literal_prefix("0.e1"), Some("0"));
assert_eq!(find_str_decimal_literal_prefix("0.1"), Some("0.1"));
assert_eq!(find_str_decimal_literal_prefix("0.1."), Some("0.1"));
assert_eq!(find_str_decimal_literal_prefix("0.1e"), Some("0.1"));
assert_eq!(find_str_decimal_literal_prefix("0.1e1"), Some("0.1e1"));
assert_eq!(find_str_decimal_literal_prefix(".1"), Some(".1"));
assert_eq!(find_str_decimal_literal_prefix(".1."), Some(".1"));
assert_eq!(find_str_decimal_literal_prefix(".1e"), Some(".1"));
assert_eq!(find_str_decimal_literal_prefix(".1e1"), Some(".1e1"));
assert_eq!(find_str_decimal_literal_prefix("1_"), Some("1"));
assert_eq!(find_str_decimal_literal_prefix("1_1"), Some("1_1"));
assert_eq!(find_str_decimal_literal_prefix("1_1_"), Some("1_1"));
}
@@ -1,3 +1,5 @@
use oxc_syntax::identifier::{is_line_terminator, is_white_space};
pub trait StringToNumber {
fn string_to_number(&self) -> f64;
}
@@ -7,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;
let s = self.trim_start_matches(is_str_white_space_char);
match s {
"" => return 0.0,
"-Infinity" => return f64::NEG_INFINITY,
@@ -66,3 +68,9 @@ impl StringToNumber for &str {
s.parse::<f64>().unwrap_or(f64::NAN)
}
}
/// whether the char is a StrWhiteSpaceChar
/// <https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type>
fn is_str_white_space_char(c: char) -> bool {
is_white_space(c) || is_line_terminator(c)
}
@@ -1215,15 +1215,14 @@ mod test {
}
#[test]
#[ignore]
fn test_fold_parse_numbers() {
// Template Strings
test_same("x = parseInt(`123`)");
test_same("x = parseInt(` 123`)");
test("x = parseInt('123')", "x = 123");
test("x = parseInt(`123`)", "x = 123");
test("x = parseInt(` 123`)", "x = 123");
test_same("x = parseInt(`12 ${a}`)");
test_same("x = parseFloat(`1.23`)");
// setAcceptedLanguage(LanguageMode.ECMASCRIPT5);
test("x = parseFloat('1.23')", "x = 1.23");
test("x = parseFloat(`1.23`)", "x = 1.23");
test_same("x = parseFloat(`1.${a}`)");
test("x = parseInt('123')", "x = 123");
test("x = parseInt(' 123')", "x = 123");
@@ -1248,46 +1247,58 @@ mod test {
test("x = parseInt('17', 8)", "x = 15");
test("x = parseInt('015', 10)", "x = 15");
test("x = parseInt('1111', 2)", "x = 15");
test("x = parseInt('12', 13)", "x = 15");
test_same("x = parseInt('12', 13)");
test("x = parseInt(15.99, 10)", "x = 15");
test("x = parseInt(-15.99, 10)", "x = -15");
// Java's Integer.parseInt("-15.99", 10) throws an exception, because of the decimal point.
test_same("x = parseInt('-15.99', 10)");
test("x = parseInt('-15.99', 10)", "x = -15");
test("x = parseFloat('3.14')", "x = 3.14");
test("x = parseFloat(3.14)", "x = 3.14");
test("x = parseFloat(-3.14)", "x = -3.14");
test("x = parseFloat('-3.14')", "x = -3.14");
test("x = parseFloat('-0')", "x = -0");
// Valid calls - unable to fold
test_same("x = parseInt('FXX123', 16)");
test_same("x = parseInt('15*3', 10)");
test_same("x = parseInt('15e2', 10)");
test_same("x = parseInt('15px', 10)");
test_same("x = parseInt('-0x08')");
test_same("x = parseInt('1', -1)");
test_same("x = parseFloat('3.14more non-digit characters')");
test_same("x = parseFloat('314e-2')");
test_same("x = parseFloat('0.0314E+2')");
test_same("x = parseFloat('3.333333333333333333333333')");
test("x = parseInt('FXX123', 16)", "x = 15"); // Parses 'F' (15 in hex)
test("x = parseInt('15*3', 10)", "x = 15"); // Parses '15', stops at '*'
test("x = parseInt('15e2', 10)", "x = 15"); // Parses '15', stops at 'e'
test("x = parseInt('15px', 10)", "x = 15"); // Parses '15', stops at 'p'
test("x = parseInt('-0x08')", "x = -8");
test("x = parseInt('1', -1)", "x = NaN");
test("x = parseFloat('3.14more non-digit characters')", "x = 3.14"); // Parses '3.14', stops at 'm'
test("x = parseFloat('314e-2')", "x = 3.14");
test("x = parseFloat('0.0314E+2')", "x = 3.14");
test("x = parseFloat('3.333333333333333333333333')", "x = 3.3333333333333335");
// Invalid calls
test_same("x = parseInt('0xa', 10)");
test_same("x = parseInt('')");
// setAcceptedLanguage(LanguageMode.ECMASCRIPT3);
test_same("x = parseInt('08')");
test("x = parseInt('0xa', 10)", "x = 0"); // Parses '0' in base 10, stops at 'x'
test("x = parseInt('')", "x = NaN");
}
#[test]
#[ignore]
fn test_fold_parse_octal_numbers() {
// setAcceptedLanguage(LanguageMode.ECMASCRIPT5);
test("x = parseInt('021', 8)", "x = 17");
test("x = parseInt('-021', 8)", "x = -17");
}
#[test]
fn test_fold_parse_numbers_additional() {
test_value("parseInt('+1')", "1");
test_value("parseFloat('+1')", "1");
test_value("parseInt('10', 0)", "10");
test_value("parseInt('0x10', 16)", "16");
test_value("parseInt('')", "NaN");
test_value("parseInt(' ')", "NaN");
test_value("parseInt('abc')", "NaN");
test_value("parseFloat('')", "NaN");
test_value("parseFloat(' ')", "NaN");
test_value("parseFloat('abc')", "NaN");
test_value("parseFloat('Infinity')", "Infinity");
test_value("parseFloat('-Infinity')", "-Infinity");
test_value("parseFloat('+Infinity')", "Infinity");
test_same_value("parseInt(unknown)");
test_same_value("parseInt((foo, '0'))"); // foo may have side effects
test_same_value("parseFloat(unknown)");
test_same_value("parseFloat((foo, '0'))"); // foo may have side effects
}
#[test]
#[ignore]
fn test_replace_with_char_at() {
@@ -1728,4 +1739,38 @@ mod test {
test("x = decodeURI(encodeURI('café'))", "x = 'café'");
test("x = decodeURIComponent(encodeURIComponent('测试'))", "x = '测试'");
}
#[test]
fn test_fold_global_is_nan() {
test_value("isNaN()", "!0");
test_value("isNaN(NaN)", "!0");
test_value("isNaN(123)", "!1");
test_value("isNaN('123')", "!1");
test_value("isNaN('abc')", "!0");
test_value("isNaN('')", "!1");
test_value("isNaN(' ')", "!1");
test_value("isNaN(null)", "!1");
test_value("isNaN(Infinity)", "!1");
test_value("isNaN(-Infinity)", "!1");
test_same_value("isNaN(unknown)");
test_same_value("isNaN((foo, 0))"); // foo may have sideeffect
}
#[test]
fn test_fold_global_is_finite() {
test_value("isFinite()", "!1");
test_value("isFinite(123)", "!0");
test_value("isFinite(123.45)", "!0");
test_value("isFinite('123')", "!0");
test_value("isFinite('')", "!0");
test_value("isFinite(' ')", "!0");
test_value("isFinite(null)", "!0");
test_value("isFinite(NaN)", "!1");
test_value("isFinite(Infinity)", "!1");
test_value("isFinite(-Infinity)", "!1");
test_value("isFinite('abc')", "!1");
test_same_value("isFinite(unknown)");
test_same_value("isFinite((foo, 0))"); // foo may have sideeffect
}
}
+11
View File
@@ -54,6 +54,17 @@ const MMSP: char = '\u{205f}';
const IDEOGRAPHIC_SPACE: char = '\u{3000}';
fn is_unicode_space_separator(c: char) -> bool {
// is_whitespace matches Unicode `White_Space` property
// exclude the characters that are included in `White_Space`, but not `Space_Separator`
// <https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5Cp%7BWhite_Space%7D%26%5CP%7BGeneral_Category%3DSpace_Separator%7D>
c.is_whitespace() && !matches!(c, TAB | LF | VT | FF | CR | NEL | LS | PS)
}
pub fn is_white_space(c: char) -> bool {
matches!(c, TAB | VT | FF | ZWNBSP) || is_unicode_space_separator(c)
}
// https://eslint.org/docs/latest/rules/no-irregular-whitespace#rule-details
#[rustfmt::skip]
pub fn is_irregular_whitespace(c: char) -> bool {