js: support [LegacyUnforgeable] accessors; make Event.isTrusted one

Fixes the failing test in WPT /dom/events/Event-isTrusted.any.html (and
its .worker variant): Object.getOwnPropertyDescriptor(event,
"isTrusted") returned undefined because the accessor lived on
Event.prototype. Per Web IDL, isTrusted is [LegacyUnforgeable]: an own,
non-configurable accessor property of every Event instance, with the
same getter function shared between instances.

- bridge.accessor gains an `unforgeable` option. Such accessors are
  installed on the class's v8 instance template instead of the
  prototype template, so every instance gets an own accessor backed by
  one getter function per context.
- Since v8's Inherit only chains prototype templates, attachClass now
  walks the ancestor chain (via the _proto fields) and re-installs
  ancestors' unforgeable accessors on the subclass's instance template,
  so MouseEvent instances & co. get their own isTrusted as well. The
  accessor installation code is extracted into attachAccessorProperty
  for reuse.
- Event.isTrusted is declared unforgeable and non-deletable.

Coverage: /dom/events/Event-isTrusted.any.html 0/1 -> 1/1,
/dom/events/Event-isTrusted.any.worker.html 0/1 -> 1/1. No regressions
across /dom/events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Francis Bouvier
2026-07-10 19:25:40 +02:00
committed by Karl Seguin
parent 2623be4493
commit 0b2cfc35f4
4 changed files with 79 additions and 44 deletions

View File

@@ -719,6 +719,10 @@ pub const Function = struct {
exposed: Exposed = .both,
ce_reactions: bool = false,
js_name: ?[:0]const u8 = null,
// Web IDL [LegacyUnforgeable]: the accessor is an own property of
// every instance (including subclass instances) instead of living on
// the interface prototype object.
unforgeable: bool = false,
pub const Exposed = enum { both, window, worker };

View File

@@ -757,6 +757,52 @@ fn inheritsFromHtmlElement(comptime JsApi: type) bool {
// and define_on != null. This is the "flattening" pass, and it defines all of
// the functions/accessors on directly on the global instance. Thus, globals have
// it defined on both their prototype (first pass) and their own instance (2nd pass).
fn attachAccessorProperty(comptime name: [:0]const u8, value: bridge.Accessor, isolate: *v8.Isolate, template: *const v8.FunctionTemplate, signature: anytype, target: anytype) void {
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
const getter_signature = if (value.static) null else signature;
const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = value.getter,
.signature = getter_signature,
}).?;
// WebIDL: getter function's .name should be "get X"
const getter_name_str = "get " ++ name;
const getter_name_v8 = v8.v8__String__NewFromUtf8(isolate, getter_name_str.ptr, v8.kNormal, @intCast(getter_name_str.len));
v8.v8__FunctionTemplate__SetClassName(getter_callback, getter_name_v8);
const setter_callback = if (value.setter) |setter| blk: {
const cb = v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = setter,
.signature = getter_signature,
.length = 1,
}).?;
const setter_name_str = "set " ++ name;
const setter_name_v8 = v8.v8__String__NewFromUtf8(isolate, setter_name_str.ptr, v8.kNormal, @intCast(setter_name_str.len));
v8.v8__FunctionTemplate__SetClassName(cb, setter_name_v8);
break :blk cb;
} else null;
var attribute: v8.PropertyAttribute = 0;
if (value.setter == null) {
attribute |= v8.ReadOnly;
}
if (value.deletable == false) {
attribute |= v8.DontDelete;
}
if (value.static) {
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
} else {
// Web IDL: attributes on the interface prototype object
// (and mirrored onto [Global] instances) are enumerable.
v8.v8__ObjectTemplate__SetAccessorProperty__Config(target, &.{
.key = js_name,
.getter = getter_callback,
.setter = setter_callback,
.attribute = attribute,
});
}
}
fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolate, template: *const v8.FunctionTemplate, define_on: ?*const v8.ObjectTemplate) void {
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
@@ -792,49 +838,10 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
continue;
}
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
const getter_signature = if (value.static) null else signature;
const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = value.getter,
.signature = getter_signature,
}).?;
// WebIDL: getter function's .name should be "get X"
const getter_name_str = "get " ++ name;
const getter_name_v8 = v8.v8__String__NewFromUtf8(isolate, getter_name_str.ptr, v8.kNormal, @intCast(getter_name_str.len));
v8.v8__FunctionTemplate__SetClassName(getter_callback, getter_name_v8);
const setter_callback = if (value.setter) |setter| blk: {
const cb = v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = setter,
.signature = getter_signature,
.length = 1,
}).?;
const setter_name_str = "set " ++ name;
const setter_name_v8 = v8.v8__String__NewFromUtf8(isolate, setter_name_str.ptr, v8.kNormal, @intCast(setter_name_str.len));
v8.v8__FunctionTemplate__SetClassName(cb, setter_name_v8);
break :blk cb;
} else null;
var attribute: v8.PropertyAttribute = 0;
if (value.setter == null) {
attribute |= v8.ReadOnly;
}
if (value.deletable == false) {
attribute |= v8.DontDelete;
}
if (value.static) {
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
} else {
// Web IDL: attributes on the interface prototype object
// (and mirrored onto [Global] instances) are enumerable.
v8.v8__ObjectTemplate__SetAccessorProperty__Config(define_on orelse prototype, &.{
.key = js_name,
.getter = getter_callback,
.setter = setter_callback,
.attribute = attribute,
});
}
// [LegacyUnforgeable] accessors live on the instances, not on
// the interface prototype object.
const target = if (value.unforgeable) instance else (define_on orelse prototype);
attachAccessorProperty(name, value, isolate, template, signature, target);
},
bridge.Function => {
if (value.wpt_only and wpt_extensions_enabled == false) {
@@ -920,6 +927,26 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
}
}
// Web IDL [LegacyUnforgeable]: unforgeable accessors of ancestor
// interfaces are own properties of every subclass instance too. v8's
// Inherit only chains prototype templates, not instance templates, so
// re-install them on this class's instance template.
if (comptime !flatten) {
comptime var T = JsApi.bridge.type;
inline while (comptime @hasField(T, "_proto")) {
const P = @typeInfo(std.meta.fieldInfo(T, ._proto).type).pointer.child;
inline for (@typeInfo(P.JsApi).@"struct".decls) |pd| {
const pvalue = @field(P.JsApi, pd.name);
if (comptime @TypeOf(pvalue) == bridge.Accessor) {
if (comptime (pvalue.unforgeable and !pvalue.static)) {
attachAccessorProperty(pd.name, pvalue, isolate, template, signature, instance);
}
}
}
T = P;
}
}
// The remaining per-class setup targets the class's own instance template;
// in [Global] flattening mode the global already has these (or doesn't need
// them), so skip it.

View File

@@ -230,6 +230,7 @@ pub const Accessor = struct {
static: bool = false,
deletable: bool = true,
wpt_only: bool = false,
unforgeable: bool = false,
exposed: Caller.Function.Opts.Exposed = .both,
cache: ?Caller.Function.Opts.Caching = null,
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
@@ -241,6 +242,7 @@ pub const Accessor = struct {
.static = opts.static,
.wpt_only = opts.wpt_only,
.deletable = opts.deletable,
.unforgeable = opts.unforgeable,
.exposed = opts.exposed,
};

View File

@@ -481,7 +481,9 @@ pub const JsApi = struct {
pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{});
pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{});
pub const timeStamp = bridge.accessor(Event.getTimeStamp, null, .{});
pub const isTrusted = bridge.accessor(Event.getIsTrusted, null, .{});
// [LegacyUnforgeable]: an own, non-configurable accessor of every
// Event (and subclass) instance.
pub const isTrusted = bridge.accessor(Event.getIsTrusted, null, .{ .unforgeable = true, .deletable = false });
pub const preventDefault = bridge.function(Event.preventDefault, .{});
pub const stopPropagation = bridge.function(Event.stopPropagation, .{});
pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});