From 0615d3a4672b680a25e4f66748b4576d8e425eee Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Tue, 24 Feb 2026 10:40:43 -0800 Subject: [PATCH] [dart:js_interop] Make isA() check that it's a wrapped function Closes https://github.com/dart-lang/sdk/issues/62573 isA() used to just check if the object was a function, leading to a possible runtime error if `toDart` is called. Fixes that by introducing some helpers and moving around some functions in the JS compilers to other internal libraries (public members can't be added to js_allow_interop_patch.dart). Also fixes a minor issue in dart2js where `allowInterop`ed functions could successfully invoke `JSExportedDartFunction.toDart`. CoreLibraryReviewExempt: Documentation change. Change-Id: I5a9d7c31d3143eb3fb6ebd3273a4bf06ca329479 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/482967 Reviewed-by: Martin Kustermann Commit-Queue: Srujan Gaddam Reviewed-by: Nate Biggs --- CHANGELOG.md | 11 ++++++ .../shared_interop_transformer.dart | 23 ++++++++++-- .../lib/js/callback_specializer.dart | 12 +++---- .../dart2js/js_interop_transforms/isa.dart | 4 ++- .../isa.dart.strong.expect | 2 ++ .../isa.dart.strong.modular.expect | 2 ++ .../isa.dart.strong.transformed.expect | 2 ++ .../patch/js_allow_interop_patch.dart | 26 ++++++-------- .../js_dev_runtime/private/interceptors.dart | 11 ++++++ .../js_runtime/lib/interceptors.dart | 15 +++++++- .../lib/js_allow_interop_patch.dart | 34 ++++++++++-------- .../js_shared/lib/js_interop_patch.dart | 15 +++++++- .../_internal/wasm/lib/js_interop_patch.dart | 7 ++++ sdk/lib/js_interop/js_interop.dart | 4 +++ .../js/static_interop_test/isa/isa_test.dart | 36 ++++++++++++++++--- 15 files changed, 158 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25de1b12590..c735e28991a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,14 @@ main() { functions must now also accept an `instantiator` callback to which they should pass the loaded results. +#### dart2js + +- `JSExportedDartFunction.toDart` sometimes incorrectly returned the original + Dart function even if the wrapper JS function was cast from a call to the + deprecated `allowInterop`. Instead, to be consistent with DDC and dart2wasm, + it now throws if the wrapper JS function wasn't a result of `Function.toJS` or + `Function.toJSCaptureThis`. + ### Libraries #### `dart:js_interop` @@ -92,6 +100,9 @@ main() { the supertype `Object?`, this change is only breaking if users referred to the extension name directly, either through applying the extension directly or through using `show`/`hide` directives. +- `isA()` now checks if the function is actually a JS + wrapper function that is returned from `Function.toJS` or + `Function.toJSCaptureThis`. [#56905]: https://github.com/dart-lang/sdk/issues/56905 diff --git a/pkg/_js_interop_checks/lib/src/transformations/shared_interop_transformer.dart b/pkg/_js_interop_checks/lib/src/transformations/shared_interop_transformer.dart index 04c3f43882b..b7f935d6751 100644 --- a/pkg/_js_interop_checks/lib/src/transformations/shared_interop_transformer.dart +++ b/pkg/_js_interop_checks/lib/src/transformations/shared_interop_transformer.dart @@ -36,9 +36,11 @@ class SharedInteropTransformer extends Transformer { final Procedure _isATearoff; final Procedure _isJSAny; final Procedure _isJSBoxedDartObject; + final Procedure _isJSExportedDartFunction; final Procedure _isJSObject; final Procedure _isNullableJSAny; final Procedure _isNullableJSBoxedDartObject; + final Procedure _isNullableJSExportedDartFunction; final Procedure _isNullableJSObject; final ExtensionTypeDeclaration _jsAny; final ExtensionTypeDeclaration _jsFunction; @@ -103,6 +105,8 @@ class SharedInteropTransformer extends Transformer { ), _isJSBoxedDartObject = _typeEnvironment.coreTypes.index .getTopLevelProcedure('dart:js_interop', '_isJSBoxedDartObject'), + _isJSExportedDartFunction = _typeEnvironment.coreTypes.index + .getTopLevelProcedure('dart:js_interop', '_isJSExportedDartFunction'), _isJSObject = _typeEnvironment.coreTypes.index.getTopLevelProcedure( 'dart:js_interop', '_isJSObject', @@ -116,6 +120,11 @@ class SharedInteropTransformer extends Transformer { 'dart:js_interop', '_isNullableJSBoxedDartObject', ), + _isNullableJSExportedDartFunction = _typeEnvironment.coreTypes.index + .getTopLevelProcedure( + 'dart:js_interop', + '_isNullableJSExportedDartFunction', + ), _isNullableJSObject = _typeEnvironment.coreTypes.index .getTopLevelProcedure('dart:js_interop', '_isNullableJSObject'), _jsAny = _typeEnvironment.coreTypes.index.getExtensionType( @@ -601,8 +610,6 @@ class SharedInteropTransformer extends Transformer { Expression? check; String? typeofString; String? instanceOfString; - // TODO(srujzs): Add specific check for `JSExportedDartFunction`. - // https://github.com/dart-lang/sdk/issues/62573 // TODO(srujzs): Maybe use `Array.isArray` for `JSArray`. // https://github.com/dart-lang/sdk/issues/62699 switch (jsTypeName) { @@ -643,6 +650,18 @@ class SharedInteropTransformer extends Transformer { Arguments([VariableGet(receiverVar)]), ); break; + case 'JSExportedDartFunction' when interopTypeDecl == jsType: + // Only do this special case when users are referring directly to the + // `dart:js_interop` type and not some wrapper. + isJSAnyCheck = null; + nullChecksNeeded = false; + check = StaticInvocation( + interopTypeNullable + ? _isNullableJSExportedDartFunction + : _isJSExportedDartFunction, + Arguments([VariableGet(receiverVar)]), + ); + break; case 'JSTypedArray' when interopTypeDecl == jsType: // Only do this special case when users are referring directly to the // `dart:js_interop` type and not some wrapper. diff --git a/pkg/dart2wasm/lib/js/callback_specializer.dart b/pkg/dart2wasm/lib/js/callback_specializer.dart index d92fbf0a350..1b3e3560a30 100644 --- a/pkg/dart2wasm/lib/js/callback_specializer.dart +++ b/pkg/dart2wasm/lib/js/callback_specializer.dart @@ -200,12 +200,12 @@ class CallbackSpecializer { /// Create a [Procedure] that will wrap a Dart callback in a JS wrapper. /// /// [node] is the conversion function that is called by the user (either - /// `allowInterop`, `Function.toJS`, or `Function.toJSCaptureThis`). [type] is - /// the static type of the callback. [boxExternRef] determines if the - /// trampoline should box the arguments and return value or convert every - /// value. [needsCastClosure] determines if a cast closure is needed in order - /// to validate the types of some arguments. [captureThis] determines if - /// `this` needs to be passed into the trampoline from the JS wrapper. + /// `Function.toJS` or `Function.toJSCaptureThis`). [type] is the static type + /// of the callback. [boxExternRef] determines if the trampoline should box + /// the arguments and return value or convert every value. [needsCastClosure] + /// determines if a cast closure is needed in order to validate the types of + /// some arguments. [captureThis] determines if `this` needs to be passed into + /// the trampoline from the JS wrapper. /// /// The procedure will call a JS method that will create a wrapper, cache the /// callback, and call the trampoline function with the callback, the JS diff --git a/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart b/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart index b9a1e0877c7..33162014e9f 100644 --- a/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart +++ b/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart @@ -45,11 +45,13 @@ void test(JSAny any, JSAny? nullableAny, Object obj, Object? nullableObj) { any.isA(); obj.isA(); - // JSTypedArray and JSBoxedDartObject handled differently. + // Check that there's a specific lowering for some JS types. any.isA(); any.isA(); + any.isA(); obj.isA(); obj.isA(); + obj.isA(); // User-defined types. any.isA(); diff --git a/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.expect b/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.expect index 84eeb182e0a..1470ed75d5d 100644 --- a/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.expect +++ b/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.expect @@ -76,8 +76,10 @@ static method test(js_::JSAny /* erasure=core::Object */ any, js_::JSAny? /* era js_::NullableObjectUtilExtension|isA? /* erasure=_interceptors::JSArray? */>(obj); js_::NullableObjectUtilExtension|isA(any); js_::NullableObjectUtilExtension|isA(any); + js_::NullableObjectUtilExtension|isA(any); js_::NullableObjectUtilExtension|isA(obj); js_::NullableObjectUtilExtension|isA(obj); + js_::NullableObjectUtilExtension|isA(obj); js_::NullableObjectUtilExtension|isA(any); js_::NullableObjectUtilExtension|isA(any); js_::NullableObjectUtilExtension|isA(any); diff --git a/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.modular.expect b/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.modular.expect index 84eeb182e0a..1470ed75d5d 100644 --- a/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.modular.expect +++ b/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.modular.expect @@ -76,8 +76,10 @@ static method test(js_::JSAny /* erasure=core::Object */ any, js_::JSAny? /* era js_::NullableObjectUtilExtension|isA? /* erasure=_interceptors::JSArray? */>(obj); js_::NullableObjectUtilExtension|isA(any); js_::NullableObjectUtilExtension|isA(any); + js_::NullableObjectUtilExtension|isA(any); js_::NullableObjectUtilExtension|isA(obj); js_::NullableObjectUtilExtension|isA(obj); + js_::NullableObjectUtilExtension|isA(obj); js_::NullableObjectUtilExtension|isA(any); js_::NullableObjectUtilExtension|isA(any); js_::NullableObjectUtilExtension|isA(any); diff --git a/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.transformed.expect index 8dfbd1288b4..5c2d01f59d7 100644 --- a/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.transformed.expect +++ b/pkg/front_end/testcases/dart2js/js_interop_transforms/isa.dart.strong.transformed.expect @@ -77,8 +77,10 @@ static method test(js_::JSAny /* erasure=core::Object */ any, js_::JSAny? /* era obj == null || js_::_isJSAny(obj) && js_::JSAnyUtilityExtension|instanceOfString(obj as js_::JSAny? /* erasure=core::Object? */, "Array"); !(any == null) && js_::JSAnyUtilityExtension|instanceof(any, js_2::JSObjectUnsafeUtilExtension|callMethodVarArgs(js_2::JSObjectUnsafeUtilExtension|[](js_::globalContext, "Object") as js_::JSObject /* erasure=_interceptors::JSObject */, js_::StringToJSString|get#toJS("getPrototypeOf"), [js_2::JSObjectUnsafeUtilExtension|[](js_::globalContext, "Int8Array") as js_::JSObject /* erasure=_interceptors::JSObject */])); js_::_isJSBoxedDartObject(any); + js_::_isJSExportedDartFunction(any); !(obj == null) && (js_::_isJSAny(obj) && js_::JSAnyUtilityExtension|instanceof(obj as js_::JSAny? /* erasure=core::Object? */, js_2::JSObjectUnsafeUtilExtension|callMethodVarArgs(js_2::JSObjectUnsafeUtilExtension|[](js_::globalContext, "Object") as js_::JSObject /* erasure=_interceptors::JSObject */, js_::StringToJSString|get#toJS("getPrototypeOf"), [js_2::JSObjectUnsafeUtilExtension|[](js_::globalContext, "Int8Array") as js_::JSObject /* erasure=_interceptors::JSObject */]))); js_::_isJSBoxedDartObject(obj); + js_::_isJSExportedDartFunction(obj); !(any == null) && js_::JSAnyUtilityExtension|instanceOfString(any, "library1.CustomJSAny"); !(any == null) && js_::JSAnyUtilityExtension|instanceOfString(any, "library1.CustomJSObject"); !(any == null) && js_::JSAnyUtilityExtension|instanceOfString(any, "library1.CustomTypedArray"); diff --git a/sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart index 57533020beb..657a0353345 100644 --- a/sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart +++ b/sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart @@ -4,7 +4,7 @@ // Patch file for dart:js_util library. import 'dart:_foreign_helper' show JS; -import 'dart:_interceptors' show JavaScriptFunction; +import 'dart:_interceptors' show functionToJSProperty, JavaScriptFunction; import 'dart:_internal' show patch; import 'dart:_runtime' as dart; @@ -50,12 +50,6 @@ Function allowInteropCaptureThis(Function f) { return ret; } -// TODO(srujzs): In dart2js, this is guaranteed to be unique per isolate. DDC -// doesn't have a mechanism to guarantee that, so use a Symbol instead to match -// the unique-per-runtime semantics of [allowInterop]. -final _functionToJSPropertyName = r'_$dart_dartClosure'; -final _functionToJSProperty = JS('!', "Symbol($_functionToJSPropertyName)"); - JavaScriptFunction _functionToJS0(Function f) { // This can only happen if a user casted a JavaScriptFunction to Function. // Such a cast is an error in dart2wasm, so we should make this behavior an @@ -73,7 +67,7 @@ JavaScriptFunction _functionToJS0(Function f) { _callDartFunctionFast0, f, ); - JS('', '#[#] = #', ret, _functionToJSProperty, f); + JS('', '#[#] = #', ret, functionToJSProperty, f); return ret; } @@ -91,7 +85,7 @@ JavaScriptFunction _functionToJS1(Function f) { _callDartFunctionFast1, f, ); - JS('', '#[#] = #', ret, _functionToJSProperty, f); + JS('', '#[#] = #', ret, functionToJSProperty, f); return ret; } @@ -109,7 +103,7 @@ JavaScriptFunction _functionToJS2(Function f) { _callDartFunctionFast2, f, ); - JS('', '#[#] = #', ret, _functionToJSProperty, f); + JS('', '#[#] = #', ret, functionToJSProperty, f); return ret; } @@ -127,7 +121,7 @@ JavaScriptFunction _functionToJS3(Function f) { _callDartFunctionFast3, f, ); - JS('', '#[#] = #', ret, _functionToJSProperty, f); + JS('', '#[#] = #', ret, functionToJSProperty, f); return ret; } @@ -145,7 +139,7 @@ JavaScriptFunction _functionToJS4(Function f) { _callDartFunctionFast4, f, ); - JS('', '#[#] = #', ret, _functionToJSProperty, f); + JS('', '#[#] = #', ret, functionToJSProperty, f); return ret; } @@ -163,7 +157,7 @@ JavaScriptFunction _functionToJS5(Function f) { _callDartFunctionFast5, f, ); - JS('', '#[#] = #', ret, _functionToJSProperty, f); + JS('', '#[#] = #', ret, functionToJSProperty, f); return ret; } @@ -183,7 +177,7 @@ JavaScriptFunction _functionToJSN(Function f, int maxLength) { f, maxLength, ); - JS('', '#[#] = #', ret, _functionToJSProperty, f); + JS('', '#[#] = #', ret, functionToJSProperty, f); return ret; } @@ -208,7 +202,7 @@ JavaScriptFunction _functionToJSCaptureThisN(Function f, int maxLength) { f, maxLength, ); - JS('', '#[#] = #', ret, _functionToJSProperty, f); + JS('', '#[#] = #', ret, functionToJSProperty, f); return ret; } @@ -360,5 +354,5 @@ _callDartFunctionFast5(callback, arg1, arg2, arg3, arg4, arg5, int length) { } Function _jsFunctionToDart(JavaScriptFunction f) { - return JS('Function', '#[#]', f, _functionToJSProperty); + return JS('Function', '#[#]', f, functionToJSProperty); } diff --git a/sdk/lib/_internal/js_dev_runtime/private/interceptors.dart b/sdk/lib/_internal/js_dev_runtime/private/interceptors.dart index 1bdd95acc43..427d332fa4f 100644 --- a/sdk/lib/_internal/js_dev_runtime/private/interceptors.dart +++ b/sdk/lib/_internal/js_dev_runtime/private/interceptors.dart @@ -305,6 +305,17 @@ findInterceptorForType(Type? type) {} /// the dart:rti library because stores information used for type checks. class JavaScriptFunction extends LegacyJavaScriptObject implements Function {} +// TODO(srujzs): In dart2js, this is guaranteed to be unique per isolate. DDC +// doesn't have a mechanism to guarantee that, so use a Symbol instead to match +// the unique-per-runtime semantics of [allowInterop]. +final _functionToJSPropertyName = r'_$dart_dartClosure'; +final functionToJSProperty = JS('!', "Symbol($_functionToJSPropertyName)"); + +/// Returns whether [f] is a wrapped Dart function through `dart:js_interop`'s +/// conversion methods. +bool isJSExportedDartFunction(JavaScriptFunction f) => + JS('', '#.#', f, functionToJSProperty) != null; + /// Interceptor for JavaScript BigInt primitive values, i.e. values `x` for /// which `typeof x == "bigint"`. @JsPeerInterface(name: 'BigInt') diff --git a/sdk/lib/_internal/js_runtime/lib/interceptors.dart b/sdk/lib/_internal/js_runtime/lib/interceptors.dart index 9662f3bac9f..9a9f340d257 100644 --- a/sdk/lib/_internal/js_runtime/lib/interceptors.dart +++ b/sdk/lib/_internal/js_runtime/lib/interceptors.dart @@ -79,6 +79,17 @@ final String DART_CLOSURE_PROPERTY_NAME = getIsolateAffinityTag( r'_$dart_dartClosure', ); +/// Present in JS functions that were converted specifically using +/// `dart:js_interop`. +final String DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME = getIsolateAffinityTag( + r'_$dart_dartClosure_dartJSInterop', +); + +/// Returns whether [f] is a wrapped Dart function through `dart:js_interop`'s +/// conversion methods. +bool isJSExportedDartFunction(JavaScriptFunction f) => + JS('', '#.#', f, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME) != null; + getDispatchProperty(object) { return JS( '', @@ -484,7 +495,9 @@ final class JavaScriptFunction extends LegacyJavaScriptObject const JavaScriptFunction(); String toString() { - var dartClosure = JS('', '#.#', this, DART_CLOSURE_PROPERTY_NAME); + var dartClosure = + JS('', '#.#', this, DART_CLOSURE_PROPERTY_NAME) ?? + JS('', '#.#', this, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME); if (dartClosure == null) return super.toString(); return 'JavaScript function for ${dartClosure.toString()}'; } diff --git a/sdk/lib/_internal/js_runtime/lib/js_allow_interop_patch.dart b/sdk/lib/_internal/js_runtime/lib/js_allow_interop_patch.dart index c8abc126bcf..27d127bad70 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_allow_interop_patch.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_allow_interop_patch.dart @@ -5,7 +5,11 @@ // Patch file for dart:js_util library. import 'dart:_foreign_helper' show JS, DART_CLOSURE_TO_JS; -import 'dart:_interceptors' show DART_CLOSURE_PROPERTY_NAME, JavaScriptFunction; +import 'dart:_interceptors' + show + DART_CLOSURE_PROPERTY_NAME, + DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, + JavaScriptFunction; import 'dart:_internal' show patch; import 'dart:_js_helper' show @@ -103,7 +107,7 @@ JavaScriptFunction _functionToJS0(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast0), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -123,7 +127,7 @@ JavaScriptFunction _functionToJSCaptureThis0(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast1), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -143,7 +147,7 @@ JavaScriptFunction _functionToJS1(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast1), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -163,7 +167,7 @@ JavaScriptFunction _functionToJSCaptureThis1(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast2), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -183,7 +187,7 @@ JavaScriptFunction _functionToJS2(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast2), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -203,7 +207,7 @@ JavaScriptFunction _functionToJSCaptureThis2(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast3), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -223,7 +227,7 @@ JavaScriptFunction _functionToJS3(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast3), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -243,7 +247,7 @@ JavaScriptFunction _functionToJSCaptureThis3(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast4), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -263,7 +267,7 @@ JavaScriptFunction _functionToJS4(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast4), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -283,7 +287,7 @@ JavaScriptFunction _functionToJSCaptureThis4(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast5), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -303,7 +307,7 @@ JavaScriptFunction _functionToJS5(Function f) { DART_CLOSURE_TO_JS(_callDartFunctionFast5), f, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -325,7 +329,7 @@ JavaScriptFunction _functionToJSN(Function f, int maxLength) { f, maxLength, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -353,7 +357,7 @@ JavaScriptFunction _functionToJSCaptureThisN(Function f, int maxLength) { f, maxLength, ); - JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f); return result; } @@ -407,5 +411,5 @@ _callDartFunctionFastN(Function callback, List arguments) { } Function _jsFunctionToDart(JavaScriptFunction f) { - return JS('Function', '#.#', f, DART_CLOSURE_PROPERTY_NAME); + return JS('Function', '#.#', f, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME); } diff --git a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart index 84cad7ce8db..ffa1c8b36df 100644 --- a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart +++ b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart @@ -3,7 +3,12 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:_foreign_helper' as foreign_helper; -import 'dart:_interceptors' show Interceptor, JavaScriptObject; +import 'dart:_interceptors' + show + Interceptor, + JavaScriptFunction, + JavaScriptObject, + isJSExportedDartFunction; import 'dart:_internal' show patch; import 'dart:_js_helper' show createObjectLiteral, staticInteropGlobalContext; import 'dart:_js_types'; @@ -130,6 +135,14 @@ bool _isJSObject(Object? any) => @pragma('dart2js:prefer-inline') bool _isNullableJSObject(Object? any) => any == null || _isJSObject(any); +@pragma('dart2js:prefer-inline') +bool _isJSExportedDartFunction(Object? any) => + any is JavaScriptFunction && isJSExportedDartFunction(any); + +@pragma('dart2js:prefer-inline') +bool _isNullableJSExportedDartFunction(Object? any) => + any == null || _isJSExportedDartFunction(any); + // ----------------------------------------------------------------------------- // JSBoxedDartObject <-> Object @patch diff --git a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart index ea2f9a4a2e3..bf6c16c3cb9 100644 --- a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart +++ b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart @@ -266,6 +266,13 @@ bool _isJSObject(Object? any) => bool _isNullableJSObject(Object? any) => any == null || _isJSObject(any); +bool _isJSExportedDartFunction(Object? any) => + _isJSAny(any) && + js_helper.isJSWrappedDartFunction(unsafeCast(any).toExternRef); + +bool _isNullableJSExportedDartFunction(Object? any) => + any == null || _isJSExportedDartFunction(any); + // ----------------------------------------------------------------------------- // JSBoxedDartObject <-> Object @patch diff --git a/sdk/lib/js_interop/js_interop.dart b/sdk/lib/js_interop/js_interop.dart index be2e0a9ccf7..77048e949c7 100644 --- a/sdk/lib/js_interop/js_interop.dart +++ b/sdk/lib/js_interop/js_interop.dart @@ -888,6 +888,10 @@ extension NullableObjectUtilExtension on Object? { /// - `JSObject`: `isA` will call an intrinsic function to check /// that the value is a JS object (`instanceof Object` is insufficient for /// some objects). + /// - `JSExportedDartFunction`: `isA` will check if + /// the value is a result of a previous + /// [FunctionToJSExportedDartFunction.toJS] or + /// [FunctionToJSExportedDartFunction.toJSCaptureThis] call. /// - User interop types whose representation types are JS primitive types: /// This will result in an error to avoid confusion on whether the user /// interop type is used in the type-check. Use the primitive JS type as the diff --git a/tests/lib/js/static_interop_test/isa/isa_test.dart b/tests/lib/js/static_interop_test/isa/isa_test.dart index 0f3eaca37b9..46486a7b7a8 100644 --- a/tests/lib/js/static_interop_test/isa/isa_test.dart +++ b/tests/lib/js/static_interop_test/isa/isa_test.dart @@ -43,9 +43,15 @@ extension type ArraySubtype._(JSArray _) implements JSObject { extension type WrapJSBoxedDartObject(JSBoxedDartObject _) implements JSBoxedDartObject {} +extension type WrapJSExportedDartFunction(JSExportedDartFunction _) + implements JSExportedDartFunction {} + @JS('WrapJSBoxedDartObject.prototype') external JSObject get wrapJSBoxedDartObjectPrototype; +@JS('WrapJSExportedDartFunction.prototype') +external JSObject get wrapJSExportedDartFunctionPrototype; + @JS('Object.setPrototypeOf') external void setPrototypeOf(JSObject obj, JSObject prototype); @@ -100,19 +106,23 @@ void testNull() { Expect.isTrue(nil.isA()); Expect.isTrue(nil.isA()); Expect.isTrue(nil.isA()); + Expect.isTrue(nil.isA()); Expect.isFalse(nil.isA()); Expect.isFalse(nil.isA()); Expect.isFalse(nil.isA()); Expect.isFalse(nil.isA()); + Expect.isFalse(nil.isA()); Object? nilObj = null; Expect.isTrue(nilObj.isA()); Expect.isTrue(nilObj.isA()); Expect.isTrue(nilObj.isA()); Expect.isTrue(nilObj.isA()); + Expect.isTrue(nilObj.isA()); Expect.isFalse(nilObj.isA()); Expect.isFalse(nilObj.isA()); Expect.isFalse(nilObj.isA()); Expect.isFalse(nilObj.isA()); + Expect.isFalse(nilObj.isA()); // JS nullish values should behave no differently. eval(''' globalThis.nullable = null; @@ -121,10 +131,12 @@ void testNull() { Expect.isTrue(nullable.isA()); Expect.isTrue(nullable.isA()); Expect.isTrue(nullable.isA()); + Expect.isTrue(nullable.isA()); Expect.isFalse(nullable.isA()); Expect.isFalse(nullable.isA()); Expect.isFalse(nullable.isA()); Expect.isFalse(nullable.isA()); + Expect.isFalse(nullable.isA()); eval(''' globalThis.nullable = undefined; '''); @@ -132,10 +144,12 @@ void testNull() { Expect.isTrue(nullable.isA()); Expect.isTrue(nullable.isA()); Expect.isTrue(nullable.isA()); + Expect.isTrue(nullable.isA()); Expect.isFalse(nullable.isA()); Expect.isFalse(nullable.isA()); Expect.isFalse(nullable.isA()); Expect.isFalse(nullable.isA()); + Expect.isFalse(nullable.isA()); } void testPrimitives() { @@ -292,14 +306,12 @@ void testJSObjects() { Expect.isTrue(jsFunction.isA()); testIsJSObject(jsFunction); Expect.isFalse(jsFunction.isA()); - // TODO(srujzs): Currently, we can't distinguish between a JS function and an - // exported function. https://github.com/dart-lang/sdk/issues/62573 - Expect.isTrue(jsFunction.isA()); + Expect.isFalse(jsFunction.isA()); Object jsFunctionObj = jsFunction; Expect.isTrue(jsFunctionObj.isA()); Expect.isFalse(jsFunctionObj.isA()); - Expect.isTrue(jsFunctionObj.isA()); + Expect.isFalse(jsFunctionObj.isA()); // JSExportedDartFunction. final jsExportedDartFunction = () {}.toJS; @@ -526,6 +538,22 @@ void testUserTypes() { Expect.isTrue(wrapJsBox.isA()); Expect.isTrue(wrapJsBoxObj.isA()); + // Test that a type wrapping `JSExportedDartFunction` should have a different + // type-check. + eval(''' + class WrapJSExportedDartFunction {} + globalThis.WrapJSExportedDartFunction = WrapJSExportedDartFunction; + '''); + final wrapJsEdf = WrapJSExportedDartFunction(() {}.toJS); + Expect.isFalse(wrapJsEdf.isA()); + setPrototypeOf(wrapJsEdf, wrapJSExportedDartFunctionPrototype); + Expect.isTrue(wrapJsEdf.isA()); + Expect.isTrue(nil.isA()); + Expect.isFalse(nil.isA()); + + Object? wrapJsEdfObj = wrapJsEdf; + Expect.isTrue(wrapJsEdfObj.isA()); + // Test subtyping a type in the browser. eval(''' class SubtypeArray extends Array {}