Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3239,7 +3239,7 @@ pub mod formatter {
value.get_class_name(global_this, &mut name_str)?;
if !name_str.eql_comptime(b"Object") {
return Ok(Some(name_str));
} else if value.get_prototype(global_this).eql_value(JSValue::NULL) {
} else if value.get_prototype(global_this)?.eql_value(JSValue::NULL) {
return Ok(Some(ZigString::static_("[Object: null prototype]")));
}
Ok(None)
Expand Down Expand Up @@ -3969,7 +3969,7 @@ pub mod formatter {
// (i.e. `class Foo extends Bar`). Built-in and DOM constructors
// have `Function.prototype` as their prototype, which would
// render as `[class X extends Function]` and is noise.
let proto = value.get_prototype(self.global_this);
let proto = value.get_prototype(self.global_this)?;
let proto_is_class = !proto.is_empty_or_undefined_or_null()
&& proto.is_cell()
&& proto.is_class(self.global_this);
Expand Down Expand Up @@ -4035,7 +4035,7 @@ pub mod formatter {
}
let printable = OwnedString::new(value.get_name(self.global_this)?);

let proto = value.get_prototype(self.global_this);
let proto = value.get_prototype(self.global_this)?;
// "Function" | "AsyncFunction" | "GeneratorFunction" | "AsyncGeneratorFunction"
let func_name = OwnedString::new(proto.get_name(self.global_this)?);

Expand Down
6 changes: 3 additions & 3 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2334,9 +2334,9 @@ impl JSValue {
pub fn unwrap_boxed_primitive(self, global: &JSGlobalObject) -> JsResult<JSValue> {
host_fn::from_js_host_call(global, || JSC__JSValue__unwrapBoxedPrimitive(global, self))
}
/// `JSValue.getPrototype`.
pub fn get_prototype(self, global: &JSGlobalObject) -> JSValue {
JSC__JSValue__getPrototype(self, global)
/// `JSValue.getPrototype`; runs a Proxy's `getPrototypeOf` trap, so it may throw.
pub fn get_prototype(self, global: &JSGlobalObject) -> JsResult<JSValue> {
host_fn::from_js_host_call(global, || JSC__JSValue__getPrototype(self, global))
}

// ── Reflection / naming. ───────────────
Expand Down
11 changes: 10 additions & 1 deletion src/runtime/napi/napi_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,16 @@ extern "C" fn napi_get_prototype(
return NapiEnv::set_last_error(Some(env), NapiStatus::object_expected);
}

result.set(env, object.get_prototype(env.to_js()));
// Like V8's Object::GetPrototype: a Proxy yields null and its trap never runs.
if object.js_type() == jsc::JSType::ProxyObject {
result.set(env, JSValue::NULL);
return env.ok();
}
let prototype = match object.get_prototype(env.to_js()) {
Ok(prototype) => prototype,
Err(_) => return env.pending_exception(),
};
result.set(env, prototype);
env.ok()
}

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/test_runner/ScopeFunctions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ fn bind(value: JSValue, global: &JSGlobalObject, name: BunString) -> JsResult<JS
// `JSHostFn` shape, not the safe Rust signature.
let call_fn = bun_jsc::JSFunction::create(global, name.clone(), __jsc_host_call_as_function, 1, Default::default());
let bound = JSValueTestExt::bind(call_fn, global, value, &name, 1.0, &[])?;
set_prototype_direct(bound, value.get_prototype(global), global)?;
set_prototype_direct(bound, value.get_prototype(global)?, global)?;
Ok(bound)
}

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/test_runner/pretty_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ impl<'a, 'f, W: bun_io::Write, const ENABLE_ANSI_COLORS: bool>
writer.print(format_args!("{} ", name_str));
} else {
value
.get_prototype(global_this)
.get_prototype(global_this)?
.get_name_property(global_this, &mut name_str)?;
if name_str.len > 0 && !name_str.eql_comptime(b"Object") {
writer.print(format_args!("{} ", name_str));
Expand Down
45 changes: 45 additions & 0 deletions test/napi/napi-app/js_test_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,50 @@ static napi_value perform_instanceof(const Napi::CallbackInfo &info) {
return out;
}

// perform_get_prototype(object) -> { status, result, pending, exception }
//
// `result` is the string "untouched" if napi_get_prototype did not write to
// *result, the string "null handle" if it wrote a NULL napi_value, and
// otherwise the prototype it returned.
static napi_value perform_get_prototype(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value object = info[0];

napi_value untouched;
NODE_API_CALL(env, napi_create_string_utf8(env, "untouched", NAPI_AUTO_LENGTH,
&untouched));
napi_value result = untouched;
napi_status status = napi_get_prototype(env, object, &result);

bool pending = false;
NODE_API_CALL(env, napi_is_exception_pending(env, &pending));

napi_value exception;
if (pending) {
NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception));
} else {
NODE_API_CALL(env, napi_get_undefined(env, &exception));
}

if (result == nullptr) {
NODE_API_CALL(env, napi_create_string_utf8(env, "null handle",
NAPI_AUTO_LENGTH, &result));
}

napi_value out;
NODE_API_CALL(env, napi_create_object(env, &out));

napi_value status_val, pending_val;
NODE_API_CALL(env, napi_create_uint32(env, status, &status_val));
NODE_API_CALL(env, napi_get_boolean(env, pending, &pending_val));

NODE_API_CALL(env, napi_set_named_property(env, out, "status", status_val));
NODE_API_CALL(env, napi_set_named_property(env, out, "result", result));
NODE_API_CALL(env, napi_set_named_property(env, out, "pending", pending_val));
NODE_API_CALL(env, napi_set_named_property(env, out, "exception", exception));
return out;
}

// create_latin1_string(byte_length): returns a JS string created via
// napi_create_string_latin1. Used by the leak test in napi.test.ts.
static napi_value create_latin1_string(const Napi::CallbackInfo &info) {
Expand Down Expand Up @@ -615,6 +659,7 @@ void register_js_test_helpers(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, perform_set);
REGISTER_FUNCTION(env, exports, define_properties);
REGISTER_FUNCTION(env, exports, perform_instanceof);
REGISTER_FUNCTION(env, exports, perform_get_prototype);
REGISTER_FUNCTION(env, exports, throw_error);
REGISTER_FUNCTION(env, exports, create_and_throw_error);
REGISTER_FUNCTION(env, exports, call_fatal_exception);
Expand Down
82 changes: 82 additions & 0 deletions test/napi/napi-app/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,88 @@ nativeTests.test_napi_instanceof = () => {
dump("undefined ctor", nativeTests.perform_instanceof({}, undefined));
};

// V8's Object::GetPrototype (what napi_get_prototype wraps in Node) returns null
// for a Proxy without running its getPrototypeOf trap, so the proxy lines below
// all read "result=null" with nothing pending, whatever the trap would do.
// result= is "untouched" if *result was not written and "null handle" if a NULL
// napi_value was written (see perform_get_prototype in js_test_helpers.cpp).
nativeTests.test_napi_get_prototype_proxy = () => {
const trapError = new RangeError("from getPrototypeOf trap");
let trapCalls = 0;
const chainProxy = new Proxy({}, {});
const names = [
[Object.prototype, "Object.prototype"],
[Array.prototype, "Array.prototype"],
[Function.prototype, "Function.prototype"],
[chainProxy, "the proxy"],
];

function dump(label, object) {
const r = nativeTests.perform_get_prototype(object);
const named = names.find(([value]) => value === r.result);
const result = typeof r.result === "string" ? r.result : named ? named[1] : String(r.result);
const exception =
r.exception === undefined
? "none"
: r.exception === trapError
? "the trap's error"
: r.exception instanceof TypeError
? "TypeError"
: String(r.exception);
console.log(`${label}: status=${r.status} pending=${r.pending} result=${result} exception=${exception}`);
}

dump("plain object", {});
dump("null prototype", Object.create(null));

dump("proxy without traps", new Proxy([], {}));
dump("callable proxy", new Proxy(function () {}, {}));
dump(
"trap returns Array.prototype",
new Proxy(
{},
{
getPrototypeOf() {
trapCalls++;
return Array.prototype;
},
},
),
);
console.log(`getPrototypeOf trap calls: ${trapCalls}`);
dump(
"trap throws",
new Proxy(
{},
{
getPrototypeOf() {
throw trapError;
},
},
),
);
dump(
"trap returns a number",
new Proxy(
{},
{
getPrototypeOf() {
return 42;
},
},
),
);
const revocable = Proxy.revocable({}, {});
revocable.revoke();
dump("revoked proxy", revocable.proxy);

// Only the object itself is special-cased; a proxy further up the chain is
// returned like any other prototype.
dump("object whose prototype is a proxy", Object.create(chainProxy));

dump("plain object again", {});
};

nativeTests.test_get_value_string = () => {
function to16Bit(string) {
if (typeof Bun != "object") return string;
Expand Down
24 changes: 24 additions & 0 deletions test/napi/napi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,30 @@ describe.skipIf(!canBuildNodeAddons())("cleanup hooks", () => {
});
});

describe("napi_get_prototype", () => {
it("returns null for a Proxy without running its getPrototypeOf trap, like Node", async () => {
// Before this was special-cased, Bun ran the trap: a proxy without traps
// reported its target's prototype, and a throwing trap reported napi_ok
// with a NULL handle written to *result and the exception left pending.
const output = await checkSameOutput("test_napi_get_prototype_proxy", []);
// checkSameOutput already asserted parity with Node; pin the values so a
// shared failure cannot pass.
expect(output.split(/\r?\n/)).toEqual([
"plain object: status=0 pending=false result=Object.prototype exception=none",
"null prototype: status=0 pending=false result=null exception=none",
"proxy without traps: status=0 pending=false result=null exception=none",
"callable proxy: status=0 pending=false result=null exception=none",
"trap returns Array.prototype: status=0 pending=false result=null exception=none",
"getPrototypeOf trap calls: 0",
"trap throws: status=0 pending=false result=null exception=none",
"trap returns a number: status=0 pending=false result=null exception=none",
"revoked proxy: status=0 pending=false result=null exception=none",
"object whose prototype is a proxy: status=0 pending=false result=the proxy exception=none",
"plain object again: status=0 pending=false result=Object.prototype exception=none",
]);
});
});

describe("napi_object_freeze and napi_object_seal", () => {
it("should handle arrays with indexed properties", async () => {
const output = await checkSameOutput("test_napi_freeze_seal_indexed", []);
Expand Down
Loading