From 761b39bc84733e2c08031ebffa16b2f2f62ee42f Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 6 Oct 2026 03:37:00 +0800 Subject: [PATCH] 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. --- Cargo.lock | 1 + moli-webidl-derive/src/attrs.rs | 38 +++ moli-webidl-derive/src/converter.rs | 56 ++++- moli-webidl-derive/src/expand.rs | 94 +++++++- moli-webidl-derive/src/lib.rs | 5 + moli-webidl/Cargo.toml | 1 + moli-webidl/src/convert.rs | 18 ++ moli-webidl/src/lib.rs | 13 +- moli-webidl/src/types.rs | 28 +++ moli-webidl/tests/interfaces.rs | 357 ++++++++++++++++++++++++++++ 10 files changed, 594 insertions(+), 17 deletions(-) create mode 100644 moli-webidl/tests/interfaces.rs diff --git a/Cargo.lock b/Cargo.lock index dd0eebab73..c7afbc610d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/moli-webidl-derive/src/attrs.rs b/moli-webidl-derive/src/attrs.rs index e79f24f605..80ffc1e476 100644 --- a/moli-webidl-derive/src/attrs.rs +++ b/moli-webidl-derive/src/attrs.rs @@ -56,6 +56,8 @@ pub(crate) struct FieldAttrs { pub(crate) name: Option, pub(crate) default: Option, pub(crate) converter: Option, + pub(crate) interface: Option, + pub(crate) brand_check: Option, pub(crate) missing_message: Option, pub(crate) index: Option, pub(crate) legacy_nullish: bool, @@ -119,9 +121,45 @@ pub(crate) fn parse_field_attrs(field: &Field) -> Result { 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) } diff --git a/moli-webidl-derive/src/converter.rs b/moli-webidl-derive/src/converter.rs index 20d78024ce..2081422ddd 100644 --- a/moli-webidl-derive/src/converter.rs +++ b/moli-webidl-derive/src/converter.rs @@ -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 { + 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 { + 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 { + 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; diff --git a/moli-webidl-derive/src/expand.rs b/moli-webidl-derive/src/expand.rs index 83201281c5..68da3450be 100644 --- a/moli-webidl-derive/src/expand.rs +++ b/moli-webidl-derive/src/expand.rs @@ -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 { @@ -295,16 +296,7 @@ fn expand_args_field( "variadic WebIdlArgs fields must use Vec", )); }; - 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>, + } + }; + 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>); diff --git a/moli-webidl-derive/src/lib.rs b/moli-webidl-derive/src/lib.rs index 3393b735c3..fae3095203 100644 --- a/moli-webidl-derive/src/lib.rs +++ b/moli-webidl-derive/src/lib.rs @@ -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)) { diff --git a/moli-webidl/Cargo.toml b/moli-webidl/Cargo.toml index ae1e408763..cc4e47d1c0 100644 --- a/moli-webidl/Cargo.toml +++ b/moli-webidl/Cargo.toml @@ -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 diff --git a/moli-webidl/src/convert.rs b/moli-webidl/src/convert.rs index d3fa83a5f8..fd3c0c2490 100644 --- a/moli-webidl/src/convert.rs +++ b/moli-webidl/src/convert.rs @@ -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 { + let object = v8::Local::::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 = (); diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index c0f528a8d7..1c6b88bdff 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -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. diff --git a/moli-webidl/src/types.rs b/moli-webidl/src/types.rs index 74eed66928..e5b2a0a291 100644 --- a/moli-webidl/src/types.rs +++ b/moli-webidl/src/types.rs @@ -3,6 +3,34 @@ use crate::WebIdlDictionary; #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct EnumValue(pub T); +/// A platform object validated against native WebIDL interface identity. +/// +/// Argument and dictionary derives unwrap this to `v8::Local`. +/// 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 diff --git a/moli-webidl/tests/interfaces.rs b/moli-webidl/tests/interfaces.rs new file mode 100644 index 0000000000..bc1ae13d33 --- /dev/null +++ b/moli-webidl/tests/interfaces.rs @@ -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>, + #[webidl(nullable, interface = interfaces::Base)] + nullable: Option>, + #[webidl(interface = interfaces::Base, brand_check = is_current_global)] + special: Option>, + #[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>, +} + +#[derive(WebIdlArgs)] +#[webidl(prefix = "Test.variadic")] +struct VariadicArgs<'scope> { + #[webidl(variadic, interface = interfaces::Base)] + objects: Vec>, +} + +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::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::(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::( + 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::(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::(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::( + 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::(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; + })() + "#, + ); +}