ソースを参照

LibJS: Handle non-decimal integer literals in Value::to_number

Implements support for parsing binary and octal literals, and fixes
instances where a hex literal is parsed in ways the spec doesn't
allow.
Slappy826 2 年 前
コミット
f4b3bb5

+ 88 - 19
Userland/Libraries/LibJS/Runtime/Value.cpp

@@ -471,6 +471,92 @@ FLATTEN ThrowCompletionOr<Value> Value::to_numeric(VM& vm) const
     return primitive.to_number(vm);
 }
 
+constexpr bool is_ascii_number(u32 code_point)
+{
+    return is_ascii_digit(code_point) || code_point == '.' || (code_point == 'e' || code_point == 'E') || code_point == '+' || code_point == '-';
+}
+
+struct NumberParseResult {
+    StringView literal;
+    u8 base;
+};
+
+static Optional<NumberParseResult> parse_number_text(StringView text)
+{
+    NumberParseResult result {};
+
+    auto check_prefix = [&](auto lower_prefix, auto upper_prefix) {
+        if (text.length() <= 2)
+            return false;
+        if (!text.starts_with(lower_prefix) && !text.starts_with(upper_prefix))
+            return false;
+        return true;
+    };
+
+    // https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type
+    if (check_prefix("0b"sv, "0B"sv)) {
+        if (!all_of(text.substring_view(2), is_ascii_binary_digit))
+            return {};
+
+        result.literal = text.substring_view(2);
+        result.base = 2;
+    } else if (check_prefix("0o"sv, "0O"sv)) {
+        if (!all_of(text.substring_view(2), is_ascii_octal_digit))
+            return {};
+
+        result.literal = text.substring_view(2);
+        result.base = 8;
+    } else if (check_prefix("0x"sv, "0X"sv)) {
+        if (!all_of(text.substring_view(2), is_ascii_hex_digit))
+            return {};
+
+        result.literal = text.substring_view(2);
+        result.base = 16;
+    } else {
+        if (!all_of(text, is_ascii_number))
+            return {};
+
+        result.literal = text;
+        result.base = 10;
+    }
+
+    return result;
+}
+
+// 7.1.4.1.1 StringToNumber ( str ), https://tc39.es/ecma262/#sec-stringtonumber
+static Optional<Value> string_to_number(StringView string)
+{
+    // 1. Let text be StringToCodePoints(str).
+    String text = string.trim_whitespace();
+
+    // 2. Let literal be ParseText(text, StringNumericLiteral).
+    if (text.is_empty())
+        return Value(0);
+    if (text == "Infinity" || text == "+Infinity")
+        return js_infinity();
+    if (text == "-Infinity")
+        return js_negative_infinity();
+
+    auto result = parse_number_text(text);
+
+    // 3. If literal is a List of errors, return NaN.
+    if (!result.has_value())
+        return js_nan();
+
+    // 4. Return StringNumericValue of literal.
+    if (result->base != 10) {
+        auto bigint = Crypto::UnsignedBigInteger::from_base(result->base, result->literal);
+        return Value(bigint.to_double());
+    }
+
+    char* endptr;
+    auto parsed_double = strtod(text.characters(), &endptr);
+    if (*endptr)
+        return js_nan();
+
+    return Value(parsed_double);
+}
+
 // 7.1.4 ToNumber ( argument ), https://tc39.es/ecma262/#sec-tonumber
 ThrowCompletionOr<Value> Value::to_number(VM& vm) const
 {
@@ -485,25 +571,8 @@ ThrowCompletionOr<Value> Value::to_number(VM& vm) const
         return Value(0);
     case BOOLEAN_TAG:
         return Value(as_bool() ? 1 : 0);
-    case STRING_TAG: {
-        String string = Utf8View(as_string().string()).trim(whitespace_characters, AK::TrimMode::Both).as_string();
-        if (string.is_empty())
-            return Value(0);
-        if (string == "Infinity" || string == "+Infinity")
-            return js_infinity();
-        if (string == "-Infinity")
-            return js_negative_infinity();
-        char* endptr;
-        auto parsed_double = strtod(string.characters(), &endptr);
-        if (*endptr)
-            return js_nan();
-        // NOTE: Per the spec only exactly [+-]Infinity should result in infinity
-        //       but strtod gives infinity for any case-insensitive 'infinity' or 'inf' string.
-        if (isinf(parsed_double) && string.contains('i', AK::CaseSensitivity::CaseInsensitive))
-            return js_nan();
-
-        return Value(parsed_double);
-    }
+    case STRING_TAG:
+        return string_to_number(as_string().string().view());
     case SYMBOL_TAG:
         return vm.throw_completion<TypeError>(ErrorType::Convert, "symbol", "number");
     case BIGINT_TAG:

+ 84 - 0
Userland/Libraries/LibJS/Tests/builtins/Number/Number.js

@@ -11,6 +11,9 @@ test("constructor without new", () => {
     expect(Number(-123)).toBe(-123);
     expect(Number(123n)).toBe(123);
     expect(Number(-123n)).toBe(-123);
+    expect(Number("1_23")).toBeNaN();
+    expect(Number("00123")).toBe(123);
+    expect(Number("123n")).toBeNaN();
     expect(Number("42")).toBe(42);
     expect(Number(null)).toBe(0);
     expect(Number(true)).toBe(1);
@@ -29,6 +32,45 @@ test("constructor without new", () => {
     expect(Number("foo")).toBeNaN();
     expect(Number("10e10000")).toBe(Infinity);
     expect(Number("-10e10000")).toBe(-Infinity);
+    expect(Number("0b1")).toBe(1);
+    expect(Number("0B1")).toBe(1);
+    expect(Number("0b01")).toBe(1);
+    expect(Number("0b11")).toBe(3);
+    expect(Number("0b")).toBeNaN();
+    expect(Number("0B")).toBeNaN();
+    expect(Number("-0b1")).toBeNaN();
+    expect(Number("+0b1")).toBeNaN();
+    expect(Number("0b1.1")).toBeNaN();
+    expect(Number("0b1e10")).toBeNaN();
+    expect(Number("0b1e+10")).toBeNaN();
+    expect(Number("0b1e-10")).toBeNaN();
+    expect(Number("0b1_1")).toBeNaN();
+    expect(Number("0o7")).toBe(7);
+    expect(Number("0O7")).toBe(7);
+    expect(Number("0o07")).toBe(7);
+    expect(Number("0o77")).toBe(63);
+    expect(Number("0o")).toBeNaN();
+    expect(Number("0O")).toBeNaN();
+    expect(Number("-0o1")).toBeNaN();
+    expect(Number("+0o1")).toBeNaN();
+    expect(Number("0o1.1")).toBeNaN();
+    expect(Number("0o1e10")).toBeNaN();
+    expect(Number("0o1e+10")).toBeNaN();
+    expect(Number("0o1e-10")).toBeNaN();
+    expect(Number("0o1_1")).toBeNaN();
+    expect(Number("0x1")).toBe(1);
+    expect(Number("0X1")).toBe(1);
+    expect(Number("0x01")).toBe(1);
+    expect(Number("0x11")).toBe(17);
+    expect(Number("0x")).toBeNaN();
+    expect(Number("0X")).toBeNaN();
+    expect(Number("-0x1")).toBeNaN();
+    expect(Number("+0x1")).toBeNaN();
+    expect(Number("0x1.1")).toBeNaN();
+    expect(Number("0x1e10")).toBe(7696);
+    expect(Number("0x1e+10")).toBeNaN();
+    expect(Number("0x1e-10")).toBeNaN();
+    expect(Number("0x1_1")).toBeNaN();
 });
 
 test("constructor with new", () => {
@@ -39,6 +81,9 @@ test("constructor with new", () => {
     expect(new Number(-123).valueOf()).toBe(-123);
     expect(new Number(123n).valueOf()).toBe(123);
     expect(new Number(-123n).valueOf()).toBe(-123);
+    expect(new Number("1_23").valueOf()).toBeNaN();
+    expect(new Number("00123").valueOf()).toBe(123);
+    expect(new Number("123n").valueOf()).toBeNaN();
     expect(new Number("42").valueOf()).toBe(42);
     expect(new Number(null).valueOf()).toBe(0);
     expect(new Number(true).valueOf()).toBe(1);
@@ -57,4 +102,43 @@ test("constructor with new", () => {
     expect(new Number("foo").valueOf()).toBeNaN();
     expect(new Number("10e10000").valueOf()).toBe(Infinity);
     expect(new Number("-10e10000").valueOf()).toBe(-Infinity);
+    expect(new Number("0b1").valueOf()).toBe(1);
+    expect(new Number("0B1").valueOf()).toBe(1);
+    expect(new Number("0b01").valueOf()).toBe(1);
+    expect(new Number("0b11").valueOf()).toBe(3);
+    expect(new Number("0b").valueOf()).toBeNaN();
+    expect(new Number("0B").valueOf()).toBeNaN();
+    expect(new Number("-0b1").valueOf()).toBeNaN();
+    expect(new Number("+0b1").valueOf()).toBeNaN();
+    expect(new Number("0b1.1").valueOf()).toBeNaN();
+    expect(new Number("0b1e10").valueOf()).toBeNaN();
+    expect(new Number("0b1e+10").valueOf()).toBeNaN();
+    expect(new Number("0b1e-10").valueOf()).toBeNaN();
+    expect(new Number("0b1_1").valueOf()).toBeNaN();
+    expect(new Number("0o7").valueOf()).toBe(7);
+    expect(new Number("0O7").valueOf()).toBe(7);
+    expect(new Number("0o07").valueOf()).toBe(7);
+    expect(new Number("0o77").valueOf()).toBe(63);
+    expect(new Number("0o").valueOf()).toBeNaN();
+    expect(new Number("0O").valueOf()).toBeNaN();
+    expect(new Number("-0o1").valueOf()).toBeNaN();
+    expect(new Number("+0o1").valueOf()).toBeNaN();
+    expect(new Number("0o1.1").valueOf()).toBeNaN();
+    expect(new Number("0o1e7").valueOf()).toBeNaN();
+    expect(new Number("0o1e+10").valueOf()).toBeNaN();
+    expect(new Number("0o1e-10").valueOf()).toBeNaN();
+    expect(new Number("0o1_1").valueOf()).toBeNaN();
+    expect(new Number("0x1").valueOf()).toBe(1);
+    expect(new Number("0X1").valueOf()).toBe(1);
+    expect(new Number("0x01").valueOf()).toBe(1);
+    expect(new Number("0x11").valueOf()).toBe(17);
+    expect(new Number("0x").valueOf()).toBeNaN();
+    expect(new Number("0X").valueOf()).toBeNaN();
+    expect(new Number("-0x1").valueOf()).toBeNaN();
+    expect(new Number("+0x1").valueOf()).toBeNaN();
+    expect(new Number("0x1.1").valueOf()).toBeNaN();
+    expect(new Number("0x1e10").valueOf()).toBe(7696);
+    expect(new Number("0x1e+10").valueOf()).toBeNaN();
+    expect(new Number("0x1e-10").valueOf()).toBeNaN();
+    expect(new Number("0x1_1").valueOf()).toBeNaN();
 });