LibJS: Implement rawJSON and isRawJSON functions
Some checks are pending
CI / Lagom (arm64, Sanitizer_CI, false, macos-15, macOS, Clang) (push) Waiting to run
CI / Lagom (x86_64, Fuzzers_CI, false, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, false, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, true, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (arm64, macos-15, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (x86_64, ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

This commit is contained in:
aplefull 2025-04-23 19:43:00 +02:00 committed by Tim Flynn
commit 223c9c91e6
Notes: github-actions[bot] 2025-04-24 13:34:52 +00:00
10 changed files with 209 additions and 4 deletions

View file

@ -177,6 +177,7 @@ set(SOURCES
Runtime/PropertyDescriptor.cpp
Runtime/ProxyConstructor.cpp
Runtime/ProxyObject.cpp
Runtime/RawJSONObject.cpp
Runtime/Realm.cpp
Runtime/Reference.cpp
Runtime/ReflectObject.cpp

View file

@ -285,6 +285,7 @@ namespace JS {
P(isLockFree) \
P(isNaN) \
P(isPrototypeOf) \
P(isRawJSON) \
P(isSafeInteger) \
P(isSealed) \
P(isSubsetOf) \
@ -403,6 +404,7 @@ namespace JS {
P(race) \
P(random) \
P(raw) \
P(rawJSON) \
P(read) \
P(reason) \
P(reduce) \

View file

@ -96,6 +96,7 @@
M(JsonBigInt, "Cannot serialize BigInt value to JSON") \
M(JsonCircular, "Cannot stringify circular object") \
M(JsonMalformed, "Malformed JSON string") \
M(JsonRawJSONNonPrimitive, "JSON.rawJSON cannot accept object or array as outermost value") \
M(MathSumPreciseOverflow, "Overflow in Math.sumPrecise") \
M(MissingRequiredProperty, "Required property {} is missing or undefined") \
M(ModuleNoEnvironment, "Cannot find module environment for imported binding") \

View file

@ -22,6 +22,7 @@
#include <LibJS/Runtime/JSONObject.h>
#include <LibJS/Runtime/NumberObject.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/RawJSONObject.h>
#include <LibJS/Runtime/StringObject.h>
#include <LibJS/Runtime/ValueInlines.h>
@ -41,6 +42,8 @@ void JSONObject::initialize(Realm& realm)
u8 attr = Attribute::Writable | Attribute::Configurable;
define_native_function(realm, vm.names.stringify, stringify, 3, attr);
define_native_function(realm, vm.names.parse, parse, 2, attr);
define_native_function(realm, vm.names.rawJSON, raw_json, 1, attr);
define_native_function(realm, vm.names.isRawJSON, is_raw_json, 1, attr);
// 25.5.3 JSON [ @@toStringTag ], https://tc39.es/ecma262/#sec-json-@@tostringtag
define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "JSON"_string), Attribute::Configurable);
@ -128,6 +131,7 @@ JS_DEFINE_NATIVE_FUNCTION(JSONObject::stringify)
}
// 25.5.2.1 SerializeJSONProperty ( state, key, holder ), https://tc39.es/ecma262/#sec-serializejsonproperty
// 1.4.1 SerializeJSONProperty ( state, key, holder ), https://tc39.es/proposal-json-parse-with-source/#sec-serializejsonproperty
ThrowCompletionOr<Optional<String>> JSONObject::serialize_json_property(VM& vm, StringifyState& state, PropertyKey const& key, Object* holder)
{
// 1. Let value be ? Get(holder, key).
@ -155,22 +159,27 @@ ThrowCompletionOr<Optional<String>> JSONObject::serialize_json_property(VM& vm,
if (value.is_object()) {
auto& value_object = value.as_object();
// a. If value has a [[NumberData]] internal slot, then
// a. If value has an [[IsRawJSON]] internal slot, then
if (is<RawJSONObject>(value_object)) {
// i. Return ! Get(value, "rawJSON").
return MUST(value_object.get(vm.names.rawJSON)).as_string().utf8_string();
}
// b. If value has a [[NumberData]] internal slot, then
if (is<NumberObject>(value_object)) {
// i. Set value to ? ToNumber(value).
value = TRY(value.to_number(vm));
}
// b. Else if value has a [[StringData]] internal slot, then
// c. Else if value has a [[StringData]] internal slot, then
else if (is<StringObject>(value_object)) {
// i. Set value to ? ToString(value).
value = TRY(value.to_primitive_string(vm));
}
// c. Else if value has a [[BooleanData]] internal slot, then
// d. Else if value has a [[BooleanData]] internal slot, then
else if (is<BooleanObject>(value_object)) {
// i. Set value to value.[[BooleanData]].
value = Value(static_cast<BooleanObject&>(value_object).boolean());
}
// d. Else if value has a [[BigIntData]] internal slot, then
// e. Else if value has a [[BigIntData]] internal slot, then
else if (is<BigIntObject>(value_object)) {
// i. Set value to value.[[BigIntData]].
value = Value(&static_cast<BigIntObject&>(value_object).bigint());
@ -495,4 +504,61 @@ ThrowCompletionOr<Value> JSONObject::internalize_json_property(VM& vm, Object* h
return TRY(call(vm, reviver, holder, PrimitiveString::create(vm, name.to_string()), value));
}
// 1.3 JSON.rawJSON ( text ), https://tc39.es/proposal-json-parse-with-source/#sec-json.rawjson
JS_DEFINE_NATIVE_FUNCTION(JSONObject::raw_json)
{
auto& realm = *vm.current_realm();
// 1. Let jsonString be ? ToString(text).
auto json_string = TRY(vm.argument(0).to_string(vm));
// 2. Throw a SyntaxError exception if jsonString is the empty String, or if either the first or last code unit of
// jsonString is any of 0x0009 (CHARACTER TABULATION), 0x000A (LINE FEED), 0x000D (CARRIAGE RETURN), or
// 0x0020 (SPACE).
auto bytes = json_string.bytes_as_string_view();
if (bytes.is_empty())
return vm.throw_completion<SyntaxError>(ErrorType::JsonMalformed);
static constexpr AK::Array invalid_code_points { 0x09, 0x0A, 0x0D, 0x20 };
auto first_char = bytes[0];
auto last_char = bytes[bytes.length() - 1];
if (invalid_code_points.contains_slow(first_char) || invalid_code_points.contains_slow(last_char))
return vm.throw_completion<SyntaxError>(ErrorType::JsonMalformed);
// 3. Parse StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404. Throw a SyntaxError exception
// if it is not a valid JSON text as defined in that specification, or if its outermost value is an object or
// array as defined in that specification.
auto json = JsonValue::from_string(json_string);
if (json.is_error())
return vm.throw_completion<SyntaxError>(ErrorType::JsonMalformed);
if (json.value().is_object() || json.value().is_array())
return vm.throw_completion<SyntaxError>(ErrorType::JsonRawJSONNonPrimitive);
// 4. Let internalSlotsList be « [[IsRawJSON]] ».
// 5. Let obj be OrdinaryObjectCreate(null, internalSlotsList).
auto object = RawJSONObject::create(realm, nullptr);
// 6. Perform ! CreateDataPropertyOrThrow(obj, "rawJSON", jsonString).
MUST(object->create_data_property_or_throw(vm.names.rawJSON, PrimitiveString::create(vm, json_string)));
// 7. Perform ! SetIntegrityLevel(obj, frozen).
MUST(object->set_integrity_level(Object::IntegrityLevel::Frozen));
// 8. Return obj.
return object;
}
// 1.1 JSON.isRawJSON ( O ), https://tc39.es/proposal-json-parse-with-source/#sec-json.israwjson
JS_DEFINE_NATIVE_FUNCTION(JSONObject::is_raw_json)
{
// 1. If Type(O) is Object and O has an [[IsRawJSON]] internal slot, return true.
if (vm.argument(0).is_object() && is<RawJSONObject>(vm.argument(0).as_object()))
return Value(true);
// 2. Return false.
return Value(false);
}
}

View file

@ -48,6 +48,8 @@ private:
JS_DECLARE_NATIVE_FUNCTION(stringify);
JS_DECLARE_NATIVE_FUNCTION(parse);
JS_DECLARE_NATIVE_FUNCTION(raw_json);
JS_DECLARE_NATIVE_FUNCTION(is_raw_json);
};
}

View file

@ -210,6 +210,7 @@ public:
virtual bool is_native_function() const { return false; }
virtual bool is_ecmascript_function_object() const { return false; }
virtual bool is_array_iterator() const { return false; }
virtual bool is_raw_json_object() const { return false; }
// B.3.7 The [[IsHTMLDDA]] Internal Slot, https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot
virtual bool is_htmldda() const { return false; }

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2025, Artsiom Yafremau <aplefull@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/RawJSONObject.h>
namespace JS {
GC_DEFINE_ALLOCATOR(RawJSONObject);
GC::Ref<RawJSONObject> RawJSONObject::create(Realm& realm, Object* prototype)
{
if (!prototype)
return realm.create<RawJSONObject>(realm.intrinsics().empty_object_shape());
return realm.create<RawJSONObject>(ConstructWithPrototypeTag::Tag, *prototype);
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2025, Artsiom Yafremau <aplefull@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Object.h>
namespace JS {
class RawJSONObject final : public Object {
JS_OBJECT(RawJSONObject, Object);
GC_DECLARE_ALLOCATOR(RawJSONObject);
public:
static GC::Ref<RawJSONObject> create(Realm& realm, Object* prototype);
virtual ~RawJSONObject() override = default;
private:
explicit RawJSONObject(Shape& shape)
: Object(shape)
{
}
RawJSONObject(ConstructWithPrototypeTag tag, Object& prototype)
: Object(tag, prototype)
{
}
virtual bool is_raw_json_object() const final { return true; }
};
template<>
inline bool Object::fast_is<RawJSONObject>() const { return is_raw_json_object(); }
}

View file

@ -0,0 +1,14 @@
test("JSON.isRawJSON basic functionality", () => {
const values = [1, 1.1, null, false, true, "123"];
for (const value of values) {
expect(JSON.isRawJSON(value)).toBeFalse();
expect(JSON.isRawJSON(JSON.rawJSON(value))).toBeTrue();
}
expect(JSON.isRawJSON(undefined)).toBeFalse();
expect(JSON.isRawJSON(Symbol("123"))).toBeFalse();
expect(JSON.isRawJSON([])).toBeFalse();
expect(JSON.isRawJSON({})).toBeFalse();
expect(JSON.isRawJSON({ rawJSON: "123" })).toBeFalse();
});

View file

@ -0,0 +1,57 @@
test("JSON.rawJSON basic functionality", () => {
expect(Object.isExtensible(JSON.rawJSON)).toBeTrue();
expect(typeof JSON.rawJSON).toBe("function");
expect(Object.getPrototypeOf(JSON.rawJSON)).toBe(Function.prototype);
expect(Object.getOwnPropertyDescriptor(JSON.rawJSON, "prototype")).toBeUndefined();
expect(JSON.stringify(JSON.rawJSON(1))).toBe("1");
expect(JSON.stringify(JSON.rawJSON(1.1))).toBe("1.1");
expect(JSON.stringify(JSON.rawJSON(-1))).toBe("-1");
expect(JSON.stringify(JSON.rawJSON(-1.1))).toBe("-1.1");
expect(JSON.stringify(JSON.rawJSON(1.1e1))).toBe("11");
expect(JSON.stringify(JSON.rawJSON(1.1e-1))).toBe("0.11");
expect(JSON.stringify(JSON.rawJSON(null))).toBe("null");
expect(JSON.stringify(JSON.rawJSON(true))).toBe("true");
expect(JSON.stringify(JSON.rawJSON(false))).toBe("false");
expect(JSON.stringify(JSON.rawJSON('"foo"'))).toBe('"foo"');
expect(JSON.stringify({ 42: JSON.rawJSON(37) })).toBe('{"42":37}');
expect(JSON.stringify({ x: JSON.rawJSON(1), y: JSON.rawJSON(2) })).toBe('{"x":1,"y":2}');
expect(JSON.stringify({ x: { x: JSON.rawJSON(1), y: JSON.rawJSON(2) } })).toBe(
'{"x":{"x":1,"y":2}}'
);
expect(JSON.stringify([JSON.rawJSON(1), JSON.rawJSON(1.1)])).toBe("[1,1.1]");
expect(
JSON.stringify([
JSON.rawJSON('"1"'),
JSON.rawJSON(true),
JSON.rawJSON(null),
JSON.rawJSON(false),
])
).toBe('["1",true,null,false]');
expect(JSON.stringify([{ x: JSON.rawJSON(1), y: JSON.rawJSON(1) }])).toBe('[{"x":1,"y":1}]');
});
test("JSON.rawJSON error cases", () => {
expect(() => JSON.rawJSON(Symbol("123"))).toThrow(TypeError);
expect(() => JSON.rawJSON(undefined)).toThrow(SyntaxError);
expect(() => JSON.rawJSON({})).toThrow(SyntaxError);
expect(() => JSON.rawJSON([])).toThrow(SyntaxError);
const illegalChars = ["\n", "\t", "\r", " "];
illegalChars.forEach(char => {
expect(() => {
JSON.rawJSON(`${char}123`);
}).toThrow(SyntaxError);
expect(() => {
JSON.rawJSON(`123${char}`);
}).toThrow(SyntaxError);
});
expect(() => {
JSON.rawJSON("");
}).toThrow(SyntaxError);
});