mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(ecmascript): add global isNaN, isFinite, parseFloat, parseInt functions support to constant evaluation (#12954)
This commit is contained in:
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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user