mirror of
https://github.com/lexmount/moli.git
synced 2026-10-25 00:01:45 +00:00
feat(webidl): derive native interface brand checks
Add interface and brand_check field attributes to argument and dictionary derives, including variadic object arguments. Preserve native object identity and validate brands at each field conversion position. Cover nullability, conversion order, cross-realm errors, registered native proxies, and invalid attribute combinations.
This commit is contained in:
Generated
+1
@@ -3446,6 +3446,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"moli-v8-test-util",
|
||||
"moli-v8-util",
|
||||
"moli-webapi-declare",
|
||||
"moli-webidl-callback",
|
||||
"moli-webidl-derive",
|
||||
"v8",
|
||||
|
||||
@@ -56,6 +56,8 @@ pub(crate) struct FieldAttrs {
|
||||
pub(crate) name: Option<LitStr>,
|
||||
pub(crate) default: Option<Expr>,
|
||||
pub(crate) converter: Option<crate::converter::ConverterKind>,
|
||||
pub(crate) interface: Option<Path>,
|
||||
pub(crate) brand_check: Option<Path>,
|
||||
pub(crate) missing_message: Option<LitStr>,
|
||||
pub(crate) index: Option<usize>,
|
||||
pub(crate) legacy_nullish: bool,
|
||||
@@ -119,9 +121,45 @@ pub(crate) fn parse_field_attrs(field: &Field) -> Result<FieldAttrs, Error> {
|
||||
parsed.converter = Some(crate::converter::ConverterKind::from_lit(&converter)?);
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("interface") {
|
||||
if parsed.interface.is_some() {
|
||||
return Err(meta.error("duplicate interface attribute"));
|
||||
}
|
||||
parsed.interface = Some(meta.value()?.parse()?);
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("brand_check") {
|
||||
if parsed.brand_check.is_some() {
|
||||
return Err(meta.error("duplicate brand_check attribute"));
|
||||
}
|
||||
parsed.brand_check = Some(meta.value()?.parse()?);
|
||||
return Ok(());
|
||||
}
|
||||
Err(meta.error("unsupported #[webidl(...)] field attribute"))
|
||||
})?;
|
||||
}
|
||||
if let Some(brand_check) = &parsed.brand_check
|
||||
&& parsed.interface.is_none()
|
||||
{
|
||||
return Err(Error::new(
|
||||
brand_check.span(),
|
||||
"brand_check requires #[webidl(interface = Type)]",
|
||||
));
|
||||
}
|
||||
if let Some(interface) = &parsed.interface {
|
||||
if parsed.converter.is_some() || parsed.with.is_some() {
|
||||
return Err(Error::new(
|
||||
interface.span(),
|
||||
"interface cannot be combined with converter or with",
|
||||
));
|
||||
}
|
||||
if parsed.treat_null_as_empty_string {
|
||||
return Err(Error::new(
|
||||
interface.span(),
|
||||
"treat_null_as_empty_string only applies to string converters",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(crate) enum ConverterKind {
|
||||
UnrestrictedDouble,
|
||||
Boolean,
|
||||
Enum,
|
||||
Interface,
|
||||
Raw,
|
||||
}
|
||||
|
||||
@@ -92,6 +93,7 @@ impl ConverterKind {
|
||||
Self::UnrestrictedDouble => quote!(::moli_webidl::UnrestrictedDouble),
|
||||
Self::Boolean => quote!(::moli_webidl::Boolean),
|
||||
Self::Enum => quote!(::moli_webidl::EnumValue<#ty>),
|
||||
Self::Interface => quote!(::moli_webidl::InterfaceObject<'_>),
|
||||
Self::Raw => quote!(#ty),
|
||||
}
|
||||
}
|
||||
@@ -131,6 +133,7 @@ impl ConverterKind {
|
||||
Self::UnrestrictedDouble => quote!(::moli_webidl::UnrestrictedDouble(#expr)),
|
||||
Self::Boolean => quote!(::moli_webidl::Boolean(#expr)),
|
||||
Self::Enum => quote!(::moli_webidl::EnumValue(#expr)),
|
||||
Self::Interface => quote!(::moli_webidl::InterfaceObject(#expr)),
|
||||
Self::Raw => quote!(#expr),
|
||||
}
|
||||
}
|
||||
@@ -156,20 +159,33 @@ impl ConverterKind {
|
||||
| Self::Double
|
||||
| Self::UnrestrictedDouble
|
||||
| Self::Boolean
|
||||
| Self::Enum => quote!(#value.0),
|
||||
| Self::Enum
|
||||
| Self::Interface => quote!(#value.0),
|
||||
Self::Raw => value,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the options value passed to `WebIdlConverter::convert`.
|
||||
///
|
||||
/// Today only string converters accept options. Rejecting
|
||||
/// String and interface converters accept options. Rejecting
|
||||
/// `treat_null_as_empty_string` on non-string converters keeps invalid
|
||||
/// derive usage as a compile-time error instead of silently ignoring it.
|
||||
pub(crate) fn options_expr(
|
||||
self,
|
||||
attrs: &FieldAttrs,
|
||||
) -> Result<proc_macro2::TokenStream, Error> {
|
||||
if self == Self::Interface {
|
||||
let interface = attrs.interface.as_ref().expect("interface converter path");
|
||||
let brand_check = attrs
|
||||
.brand_check
|
||||
.as_ref()
|
||||
.map(|path| quote!(#path))
|
||||
.unwrap_or_else(|| quote!(<#interface>::is_instance));
|
||||
return Ok(quote!(::moli_webidl::InterfaceOptions {
|
||||
name: <#interface>::NAME,
|
||||
brand_check: #brand_check,
|
||||
}));
|
||||
}
|
||||
if attrs.treat_null_as_empty_string {
|
||||
match self {
|
||||
Self::DomString | Self::UsvString | Self::ByteString => {
|
||||
@@ -190,10 +206,27 @@ impl ConverterKind {
|
||||
}
|
||||
|
||||
pub(crate) fn converter_kind(field: &Field, attrs: &FieldAttrs) -> Result<ConverterKind, Error> {
|
||||
converter_kind_for_field_type(field, attrs, inner_type_for_field(field))
|
||||
}
|
||||
|
||||
pub(crate) fn converter_kind_for_field_type(
|
||||
field: &Field,
|
||||
attrs: &FieldAttrs,
|
||||
ty: &Type,
|
||||
) -> Result<ConverterKind, Error> {
|
||||
if attrs.interface.is_some() {
|
||||
if !is_v8_object_type(ty) {
|
||||
return Err(Error::new(
|
||||
ty.span(),
|
||||
"interface fields must use v8::Local<'_, v8::Object> (or Option/variadic Vec of it)",
|
||||
));
|
||||
}
|
||||
return Ok(ConverterKind::Interface);
|
||||
}
|
||||
if let Some(converter) = attrs.converter {
|
||||
return Ok(converter);
|
||||
}
|
||||
converter_kind_for_type(inner_type_for_field(field)).ok_or_else(|| {
|
||||
converter_kind_for_type(ty).ok_or_else(|| {
|
||||
Error::new(
|
||||
field.span(),
|
||||
"could not infer converter; add #[webidl(converter = \"...\")]",
|
||||
@@ -305,6 +338,23 @@ fn is_v8_local_type(ty: &Type) -> bool {
|
||||
.is_some_and(|segment| segment.ident == "Local")
|
||||
}
|
||||
|
||||
fn is_v8_object_type(ty: &Type) -> bool {
|
||||
let Type::Path(type_path) = ty else {
|
||||
return false;
|
||||
};
|
||||
let Some(segment) = type_path.path.segments.last() else {
|
||||
return false;
|
||||
};
|
||||
if segment.ident != "Local" {
|
||||
return false;
|
||||
}
|
||||
let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
|
||||
return false;
|
||||
};
|
||||
matches!(arguments.args.last(), Some(GenericArgument::Type(Type::Path(path)))
|
||||
if path.path.segments.last().is_some_and(|segment| segment.ident == "Object"))
|
||||
}
|
||||
|
||||
fn option_inner_type(ty: &Type) -> Option<&Type> {
|
||||
let Type::Path(type_path) = ty else {
|
||||
return None;
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::attrs::{
|
||||
parse_container_attrs, parse_enum_attrs, parse_field_attrs, parse_variant_attrs,
|
||||
};
|
||||
use crate::converter::{
|
||||
converter_kind, converter_kind_for_type, inner_type_for_field, is_option_type, vec_inner_type,
|
||||
converter_kind, converter_kind_for_field_type, inner_type_for_field, is_option_type,
|
||||
vec_inner_type,
|
||||
};
|
||||
|
||||
pub(crate) fn expand_webidl_args(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
|
||||
@@ -295,16 +296,7 @@ fn expand_args_field(
|
||||
"variadic WebIdlArgs fields must use Vec<T>",
|
||||
));
|
||||
};
|
||||
let converter = if let Some(converter) = attrs.converter {
|
||||
converter
|
||||
} else {
|
||||
converter_kind_for_type(item_type).ok_or_else(|| {
|
||||
Error::new(
|
||||
field.span(),
|
||||
"could not infer variadic item converter; add #[webidl(converter = \"...\")]",
|
||||
)
|
||||
})?
|
||||
};
|
||||
let converter = converter_kind_for_field_type(field, &attrs, item_type)?;
|
||||
let arg_index = attrs.index.unwrap_or(index) as i32;
|
||||
let converter_ty = converter.wrapper_type(item_type);
|
||||
let unwrap_value = converter.unwrap_value(quote!(value));
|
||||
@@ -612,9 +604,87 @@ fn expand_dictionary_field(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::impl_parts_for_scope;
|
||||
use super::{expand_webidl_args, expand_webidl_dictionary, impl_parts_for_scope};
|
||||
use syn::parse_quote;
|
||||
|
||||
#[test]
|
||||
fn interface_attributes_reject_conflicting_or_incomplete_metadata() {
|
||||
let cases = [
|
||||
(
|
||||
quote::quote!(brand_check = check),
|
||||
"brand_check requires #[webidl(interface = Type)]",
|
||||
),
|
||||
(
|
||||
quote::quote!(interface = Base, converter = "raw"),
|
||||
"interface cannot be combined with converter or with",
|
||||
),
|
||||
(
|
||||
quote::quote!(interface = Base, with = parse),
|
||||
"interface cannot be combined with converter or with",
|
||||
),
|
||||
(
|
||||
quote::quote!(interface = Base, treat_null_as_empty_string),
|
||||
"treat_null_as_empty_string only applies to string converters",
|
||||
),
|
||||
(
|
||||
quote::quote!(interface = Base, interface = Other),
|
||||
"duplicate interface attribute",
|
||||
),
|
||||
(
|
||||
quote::quote!(interface = Base, brand_check = check, brand_check = other),
|
||||
"duplicate brand_check attribute",
|
||||
),
|
||||
];
|
||||
for (attrs, message) in cases {
|
||||
let input: syn::DeriveInput = parse_quote! {
|
||||
struct Arguments<'s> {
|
||||
#[webidl(#attrs)]
|
||||
value: Option<v8::Local<'s, v8::Object>>,
|
||||
}
|
||||
};
|
||||
for error in [
|
||||
expand_webidl_args(input.clone()).unwrap_err(),
|
||||
expand_webidl_dictionary(input).unwrap_err(),
|
||||
] {
|
||||
assert_eq!(error.to_string(), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interface_attributes_require_object_fields_and_variadic_items() {
|
||||
for ty in [
|
||||
quote::quote!(String),
|
||||
quote::quote!(v8::Local<'s, v8::Value>),
|
||||
quote::quote!(v8::Local<'s, v8::Function>),
|
||||
] {
|
||||
let input: syn::DeriveInput = parse_quote! {
|
||||
struct Arguments<'s> {
|
||||
#[webidl(interface = Base)]
|
||||
value: Option<#ty>,
|
||||
}
|
||||
};
|
||||
for error in [
|
||||
expand_webidl_args(input.clone()).unwrap_err(),
|
||||
expand_webidl_dictionary(input).unwrap_err(),
|
||||
] {
|
||||
assert!(error.to_string().starts_with("interface fields must use"));
|
||||
}
|
||||
let input = parse_quote! {
|
||||
struct Arguments<'s> {
|
||||
#[webidl(variadic, interface = Base)]
|
||||
value: Vec<#ty>,
|
||||
}
|
||||
};
|
||||
assert!(
|
||||
expand_webidl_args(input)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.starts_with("interface fields must use")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scope_lifetime_is_inferred_for_single_lifetime_structs() {
|
||||
let generics: syn::Generics = parse_quote!(<'scope>);
|
||||
|
||||
@@ -11,6 +11,9 @@ use syn::{DeriveInput, parse_macro_input};
|
||||
/// and returns a Rust struct. Field attributes control required arguments,
|
||||
/// explicit indexes, defaults, nullable values, variadic tails, custom
|
||||
/// converters, and hand-written parser hooks.
|
||||
/// `#[webidl(interface = Type)]` validates a V8 object using `Type::NAME` and
|
||||
/// `Type::is_instance`, with an optional `brand_check = path` override. It
|
||||
/// composes with optional/nullable fields and variadic object arguments.
|
||||
#[proc_macro_derive(WebIdlArgs, attributes(webidl))]
|
||||
pub fn derive_webidl_args(input: TokenStream) -> TokenStream {
|
||||
match expand::expand_webidl_args(parse_macro_input!(input as DeriveInput)) {
|
||||
@@ -27,6 +30,8 @@ pub fn derive_webidl_args(input: TokenStream) -> TokenStream {
|
||||
/// `nullable`, legacy nullish handling, explicit converters, and hand-written
|
||||
/// member parser hooks. Unnamed fields use `camelCase` member names by default,
|
||||
/// matching common WebIDL dictionary spelling.
|
||||
/// Object members support `interface = Type` and an optional `brand_check = path`
|
||||
/// using the same native identity conversion as positional arguments.
|
||||
#[proc_macro_derive(WebIdlDictionary, attributes(webidl))]
|
||||
pub fn derive_webidl_dictionary(input: TokenStream) -> TokenStream {
|
||||
match expand::expand_webidl_dictionary(parse_macro_input!(input as DeriveInput)) {
|
||||
|
||||
@@ -12,6 +12,7 @@ v8 = "152.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
moli-v8-test-util = { path = "../moli-v8-test-util" }
|
||||
moli-webapi-declare = { path = "../moli-webapi-declare" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -449,6 +449,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> WebIdlConverter<'s> for crate::InterfaceObject<'s> {
|
||||
type Options = crate::InterfaceOptions;
|
||||
|
||||
fn convert(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
context: Context,
|
||||
options: &Self::Options,
|
||||
) -> Result<Self, WebIdlError> {
|
||||
let object = v8::Local::<v8::Object>::try_from(value)
|
||||
.map_err(|_| WebIdlError::cannot_convert(context, options.name))?;
|
||||
if !(options.brand_check)(scope, object) {
|
||||
return Err(WebIdlError::cannot_convert(context, options.name));
|
||||
}
|
||||
Ok(Self(object))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> WebIdlConverter<'s> for v8::Local<'s, v8::Value> {
|
||||
type Options = ();
|
||||
|
||||
|
||||
+11
-2
@@ -29,6 +29,15 @@
|
||||
//! generates named property parsing for dictionary objects. Both derives use
|
||||
//! the converter wrappers in `types` and only produce Rust values; object shape
|
||||
//! declaration and Web API wrapper allocation remain outside this crate.
|
||||
//!
|
||||
//! Interface fields use `#[webidl(interface = interfaces::EventTarget)]` with
|
||||
//! `v8::Local<'s, v8::Object>`, optionally wrapped in `Option` or a variadic
|
||||
//! argument's `Vec`. The interface supplies `NAME` and `is_instance`; a
|
||||
//! `brand_check = path` attribute overrides the native identity predicate.
|
||||
//! `Option` skips missing/undefined values, while `nullable` additionally accepts
|
||||
//! null. Identity is checked at the field's position in conversion order, and
|
||||
//! the original object is retained. Interface conversion cannot be combined
|
||||
//! with `converter`, `with`, or string conversion options.
|
||||
|
||||
extern crate self as moli_webidl;
|
||||
|
||||
@@ -64,8 +73,8 @@ pub use traits::{ParseOutcome, WebIdlArguments, WebIdlConverter, WebIdlDictionar
|
||||
pub use types::{
|
||||
Boolean, BufferSource, ByteString, ClampedUnsignedShort, DomString, DomString16, Double,
|
||||
EnforceRangeLong, EnforceRangeUnsignedLong, EnforceRangeUnsignedLongLong, EnumValue,
|
||||
EventListenerOptions, Long, Record, Sequence, Short, StringOptions, UnrestrictedDouble,
|
||||
UnsignedLong, UnsignedLongLong, UnsignedShort, UsvString,
|
||||
EventListenerOptions, InterfaceObject, InterfaceOptions, Long, Record, Sequence, Short,
|
||||
StringOptions, UnrestrictedDouble, UnsignedLong, UnsignedLongLong, UnsignedShort, UsvString,
|
||||
};
|
||||
|
||||
/// Restricted WebIDL float represented as a finite binary32 value.
|
||||
|
||||
@@ -3,6 +3,34 @@ use crate::WebIdlDictionary;
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct EnumValue<T>(pub T);
|
||||
|
||||
/// A platform object validated against native WebIDL interface identity.
|
||||
///
|
||||
/// Argument and dictionary derives unwrap this to `v8::Local<v8::Object>`.
|
||||
/// Validation preserves the original object, including registered native proxies.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct InterfaceObject<'s>(pub v8::Local<'s, v8::Object>);
|
||||
|
||||
/// Native interface metadata supplied by `#[webidl(interface = Type)]`.
|
||||
///
|
||||
/// The name comes from `Type::NAME` and the predicate defaults to
|
||||
/// `Type::is_instance`. `brand_check = path` overrides only the predicate.
|
||||
/// Predicates must check native identity without invoking author code.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct InterfaceOptions {
|
||||
pub name: &'static str,
|
||||
pub brand_check: for<'s, 'i> fn(&mut v8::PinScope<'s, 'i>, v8::Local<'s, v8::Object>) -> bool,
|
||||
}
|
||||
|
||||
impl Default for InterfaceOptions {
|
||||
/// Without explicit interface metadata, conversion rejects every object.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "interface",
|
||||
brand_check: |_, _| false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WebIDL `DOMString` represented at the Rust boundary as UTF-8 `String`.
|
||||
///
|
||||
/// This intentionally follows Deno's practical boundary model rather than
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
use moli_webidl::{Context, WebIdlArgs, WebIdlDictionary};
|
||||
|
||||
mod interfaces {
|
||||
moli_webapi_declare::declare_web_api_interfaces! {
|
||||
pub Base = "RenamedBase";
|
||||
pub Derived: Base;
|
||||
pub Unrelated;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(WebIdlArgs, WebIdlDictionary)]
|
||||
#[webidl(prefix = "Test.interfaces")]
|
||||
struct InterfaceMembers<'scope> {
|
||||
#[webidl(default = "")]
|
||||
before: String,
|
||||
#[webidl(interface = interfaces::Base)]
|
||||
optional: Option<v8::Local<'scope, v8::Object>>,
|
||||
#[webidl(nullable, interface = interfaces::Base)]
|
||||
nullable: Option<v8::Local<'scope, v8::Object>>,
|
||||
#[webidl(interface = interfaces::Base, brand_check = is_current_global)]
|
||||
special: Option<v8::Local<'scope, v8::Object>>,
|
||||
#[webidl(default = 0)]
|
||||
after: i32,
|
||||
}
|
||||
|
||||
#[derive(WebIdlArgs)]
|
||||
#[webidl(prefix = "Test.required")]
|
||||
struct RequiredArgs<'scope> {
|
||||
#[webidl(required)]
|
||||
before: String,
|
||||
#[webidl(required, interface = interfaces::Base)]
|
||||
object: v8::Local<'scope, v8::Object>,
|
||||
}
|
||||
|
||||
#[derive(WebIdlArgs, WebIdlDictionary)]
|
||||
#[webidl(prefix = "Test.requiredNullable")]
|
||||
struct RequiredNullable<'scope> {
|
||||
#[webidl(required, nullable, interface = interfaces::Base)]
|
||||
object: Option<v8::Local<'scope, v8::Object>>,
|
||||
}
|
||||
|
||||
#[derive(WebIdlArgs)]
|
||||
#[webidl(prefix = "Test.variadic")]
|
||||
struct VariadicArgs<'scope> {
|
||||
#[webidl(variadic, interface = interfaces::Base)]
|
||||
objects: Vec<v8::Local<'scope, v8::Object>>,
|
||||
}
|
||||
|
||||
fn is_current_global<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
object.strict_equals(scope.get_current_context().global(scope).into())
|
||||
}
|
||||
|
||||
fn object_or_null<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: Option<v8::Local<'s, v8::Object>>,
|
||||
) -> v8::Local<'s, v8::Value> {
|
||||
object
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| v8::null(scope).into())
|
||||
}
|
||||
|
||||
fn members_array<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
parsed: InterfaceMembers<'s>,
|
||||
) -> v8::Local<'s, v8::Array> {
|
||||
let values = [
|
||||
v8::String::new(scope, &parsed.before).unwrap().into(),
|
||||
object_or_null(scope, parsed.optional),
|
||||
object_or_null(scope, parsed.nullable),
|
||||
object_or_null(scope, parsed.special),
|
||||
v8::Integer::new(scope, parsed.after).into(),
|
||||
];
|
||||
v8::Array::new_with_elements(scope, &values)
|
||||
}
|
||||
|
||||
fn optional_args<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
if let Some(parsed) = moli_webidl::parse_args::<InterfaceMembers>(scope, &args) {
|
||||
rv.set(members_array(scope, parsed).into());
|
||||
}
|
||||
}
|
||||
|
||||
fn dictionary<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
match moli_webidl::parse_dictionary::<InterfaceMembers>(
|
||||
scope,
|
||||
args.get(0),
|
||||
Context::argument("Test.dictionary", 1),
|
||||
) {
|
||||
Ok(Some(parsed)) => rv.set(members_array(scope, parsed).into()),
|
||||
Ok(None) => {}
|
||||
Err(error) => moli_webidl::throw_error(scope, &error),
|
||||
}
|
||||
}
|
||||
|
||||
fn required_args<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
if let Some(parsed) = moli_webidl::parse_args::<RequiredArgs>(scope, &args) {
|
||||
let before = v8::String::new(scope, &parsed.before).unwrap();
|
||||
rv.set(v8::Array::new_with_elements(scope, &[before.into(), parsed.object.into()]).into());
|
||||
}
|
||||
}
|
||||
|
||||
fn required_nullable_args<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
if let Some(parsed) = moli_webidl::parse_args::<RequiredNullable>(scope, &args) {
|
||||
rv.set(object_or_null(scope, parsed.object));
|
||||
}
|
||||
}
|
||||
|
||||
fn required_nullable_dictionary<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
match moli_webidl::parse_dictionary::<RequiredNullable>(
|
||||
scope,
|
||||
args.get(0),
|
||||
Context::argument("Test.requiredNullableDictionary", 1),
|
||||
) {
|
||||
Ok(Some(parsed)) => rv.set(object_or_null(scope, parsed.object)),
|
||||
Ok(None) => {}
|
||||
Err(error) => moli_webidl::throw_error(scope, &error),
|
||||
}
|
||||
}
|
||||
|
||||
fn variadic_args<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
if let Some(parsed) = moli_webidl::parse_args::<VariadicArgs>(scope, &args) {
|
||||
let values: Vec<_> = parsed.objects.into_iter().map(Into::into).collect();
|
||||
rv.set(v8::Array::new_with_elements(scope, &values).into());
|
||||
}
|
||||
}
|
||||
|
||||
fn run_checks(source: &str) {
|
||||
moli_v8_test_util::ensure_v8();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate));
|
||||
let scope = &mut scope.init();
|
||||
let callee = v8::Context::new(scope, Default::default());
|
||||
let exports = {
|
||||
let scope = &mut v8::ContextScope::new(scope, callee);
|
||||
let foreign = v8::Object::new(scope);
|
||||
interfaces::Derived::DESCRIPTOR
|
||||
.initialize(scope, foreign)
|
||||
.unwrap();
|
||||
let handler = v8::Object::new(scope);
|
||||
let native = v8::Proxy::new(scope, foreign, handler).unwrap();
|
||||
moli_webapi_declare::register_web_api_proxy(scope, native).unwrap();
|
||||
let key = v8::String::new(scope, "TypeError").unwrap();
|
||||
let type_error = callee.global(scope).get(scope, key.into()).unwrap();
|
||||
[
|
||||
(
|
||||
"optionalArgs",
|
||||
v8::Function::new(scope, optional_args).unwrap().into(),
|
||||
),
|
||||
(
|
||||
"dictionary",
|
||||
v8::Function::new(scope, dictionary).unwrap().into(),
|
||||
),
|
||||
(
|
||||
"requiredArgs",
|
||||
v8::Function::new(scope, required_args).unwrap().into(),
|
||||
),
|
||||
(
|
||||
"requiredNullableArgs",
|
||||
v8::Function::new(scope, required_nullable_args)
|
||||
.unwrap()
|
||||
.into(),
|
||||
),
|
||||
(
|
||||
"requiredNullableDictionary",
|
||||
v8::Function::new(scope, required_nullable_dictionary)
|
||||
.unwrap()
|
||||
.into(),
|
||||
),
|
||||
(
|
||||
"variadicArgs",
|
||||
v8::Function::new(scope, variadic_args).unwrap().into(),
|
||||
),
|
||||
("CalleeTypeError", type_error),
|
||||
("foreign", foreign.into()),
|
||||
("native", native.into()),
|
||||
("special", callee.global(scope).into()),
|
||||
]
|
||||
};
|
||||
let caller = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, caller);
|
||||
let real = v8::Object::new(scope);
|
||||
interfaces::Derived::DESCRIPTOR
|
||||
.initialize(scope, real)
|
||||
.unwrap();
|
||||
let unrelated = v8::Object::new(scope);
|
||||
interfaces::Unrelated::DESCRIPTOR
|
||||
.initialize(scope, unrelated)
|
||||
.unwrap();
|
||||
for (name, value) in exports
|
||||
.into_iter()
|
||||
.chain([("real", real.into()), ("unrelated", unrelated.into())])
|
||||
{
|
||||
let key = v8::String::new(scope, name).unwrap();
|
||||
assert_eq!(
|
||||
caller.global(scope).set(scope, key.into(), value),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
let scope = std::pin::pin!(v8::TryCatch::new(scope));
|
||||
let scope = &mut scope.init();
|
||||
let source = v8::String::new(scope, source).unwrap();
|
||||
let script = v8::Script::compile(scope, source, None).unwrap();
|
||||
let result = script.run(scope);
|
||||
if result.is_none() {
|
||||
let exception = scope.exception().unwrap();
|
||||
panic!(
|
||||
"interface conversion assertions failed: {}",
|
||||
exception.to_rust_string_lossy(scope)
|
||||
);
|
||||
}
|
||||
assert!(result.unwrap().is_true());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interface_arguments_preserve_native_identity_conversion_order_and_callee_errors() {
|
||||
run_checks(
|
||||
r#"
|
||||
(() => {
|
||||
const assert = (ok, message) => { if (!ok) throw Error(message); };
|
||||
const caught = fn => { try { fn(); } catch (error) { return error; } };
|
||||
const typeError = fn => {
|
||||
const error = caught(fn);
|
||||
assert(error instanceof CalleeTypeError && !(error instanceof TypeError), 'callee TypeError');
|
||||
return error;
|
||||
};
|
||||
Object.setPrototypeOf(real, null);
|
||||
for (const value of [real, foreign, native]) {
|
||||
const result = optionalArgs('before', value, value, special, 7);
|
||||
assert(result[1] === value && result[2] === value && result[3] === special && result[4] === 7,
|
||||
'original objects, native proxies, inheritance and cross-realm brands');
|
||||
assert(requiredArgs('before', value)[1] === value, 'required object');
|
||||
assert(requiredNullableArgs(value) === value, 'required nullable object');
|
||||
}
|
||||
for (const value of [null, undefined]) {
|
||||
assert(optionalArgs('before', undefined, value)[2] === null, 'nullable optional');
|
||||
assert(requiredNullableArgs(value) === null, 'nullable required accepts present nullish');
|
||||
}
|
||||
assert(optionalArgs().slice(1, 4).every(value => value === null), 'missing optional members');
|
||||
typeError(() => requiredNullableArgs());
|
||||
let conversions = 0;
|
||||
const sentinel = {};
|
||||
const poison = {toString() { conversions++; throw sentinel; }};
|
||||
typeError(() => requiredArgs(poison));
|
||||
assert(conversions === 0, 'arity preflight precedes conversions');
|
||||
assert(caught(() => optionalArgs(poison)) === sentinel, 'original conversion exception');
|
||||
let traps = 0;
|
||||
const handler = {get() { traps++; throw Error('get trap'); },
|
||||
getPrototypeOf() { traps++; throw Error('prototype trap'); }};
|
||||
const revoked = Proxy.revocable(real, {}); revoked.revoke();
|
||||
const invalid = [null, {}, 1, 'object', true, Symbol('object'), 1n, unrelated,
|
||||
Object.create(real), new Proxy(real, handler), new Proxy(native, handler), revoked.proxy];
|
||||
for (const value of invalid) {
|
||||
const order = [];
|
||||
const before = {toString() { order.push('before'); return 'before'; }};
|
||||
const after = {valueOf() { order.push('after'); throw sentinel; }};
|
||||
const error = typeError(() => optionalArgs(before, value, null, undefined, after));
|
||||
assert(error.message.includes('RenamedBase'), 'declared interface name');
|
||||
assert(order.join() === 'before', 'interface validation at field position');
|
||||
if (value !== null) typeError(() => requiredNullableArgs(value));
|
||||
}
|
||||
typeError(() => requiredArgs('before', undefined));
|
||||
typeError(() => optionalArgs('before', real, real, real));
|
||||
assert(traps === 0, 'identity checks never invoke author proxy traps');
|
||||
const order = [];
|
||||
const result = optionalArgs({toString() { order.push('before'); return 'label'; }}, real,
|
||||
native, special, {valueOf() { order.push('after'); return 9; }});
|
||||
assert(order.join() === 'before,after' && result[4] === 9, 'successful conversion order');
|
||||
assert(variadicArgs().length === 0, 'empty variadic');
|
||||
const values = variadicArgs(real, foreign, native);
|
||||
assert(values[0] === real && values[1] === foreign && values[2] === native, 'variadic identity');
|
||||
for (const value of [...invalid, undefined]) typeError(() => variadicArgs(real, value, foreign));
|
||||
return true;
|
||||
})()
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interface_dictionary_members_preserve_getters_nullability_and_native_identity() {
|
||||
run_checks(
|
||||
r#"
|
||||
(() => {
|
||||
const assert = (ok, message) => { if (!ok) throw Error(message); };
|
||||
const caught = fn => { try { fn(); } catch (error) { return error; } };
|
||||
const typeError = fn => {
|
||||
const error = caught(fn);
|
||||
assert(error instanceof CalleeTypeError && !(error instanceof TypeError), 'callee TypeError');
|
||||
return error;
|
||||
};
|
||||
const result = dictionary({optional: real, nullable: native, special, after: 7});
|
||||
assert(result[1] === real && result[2] === native && result[3] === special && result[4] === 7,
|
||||
'interface members preserve original identity and custom predicate');
|
||||
assert(dictionary(Object.create({optional: foreign}))[1] === foreign, 'inherited dictionary member');
|
||||
for (const input of [{}, {optional: undefined, nullable: undefined}, {nullable: null}]) {
|
||||
assert(dictionary(input).slice(1, 4).every(value => value === null), 'optional and nullable dictionary members');
|
||||
}
|
||||
typeError(() => dictionary({optional: null}));
|
||||
typeError(() => dictionary({special: real}));
|
||||
assert(requiredNullableDictionary({object: null}) === null, 'required nullable null');
|
||||
assert(requiredNullableDictionary({object: native}) === native, 'required nullable object');
|
||||
typeError(() => requiredNullableDictionary({}));
|
||||
typeError(() => requiredNullableDictionary({object: undefined}));
|
||||
const sentinel = {};
|
||||
let traps = 0;
|
||||
const proxy = new Proxy(real, {get() { traps++; throw sentinel; }, getPrototypeOf() { traps++; throw sentinel; }});
|
||||
for (const value of [{}, proxy, Object.create(real), unrelated, 1, 'object']) {
|
||||
const order = [];
|
||||
const error = typeError(() => dictionary({
|
||||
get before() { order.push('before'); return 'label'; },
|
||||
get optional() { order.push('optional'); return value; },
|
||||
get nullable() { order.push('nullable'); throw sentinel; }
|
||||
}));
|
||||
assert(error.message.includes('RenamedBase') && error.message.includes('optional'), 'interface member error context');
|
||||
assert(order.join() === 'before,optional', 'validation precedes later getter');
|
||||
}
|
||||
assert(traps === 0, 'member value identity checks never invoke author traps');
|
||||
assert(caught(() => dictionary({get optional() { throw sentinel; }})) === sentinel, 'getter exception identity');
|
||||
const order = [];
|
||||
dictionary({
|
||||
get before() { order.push('before'); return 'label'; },
|
||||
get optional() { order.push('optional'); return real; },
|
||||
get nullable() { order.push('nullable'); return native; },
|
||||
get special() { order.push('special'); return special; },
|
||||
get after() { order.push('after'); return 1; }
|
||||
});
|
||||
assert(order.join() === 'before,optional,nullable,special,after', 'successful getter order');
|
||||
return true;
|
||||
})()
|
||||
"#,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user