Skip to content

Commit d672d98

Browse files
srujzsCommit Queue
authored andcommitted
[dart:js_interop] Make isA<JSBoxedDartObject> check if it's a boxed object
Fixes #60644 Previously, we made no special case for JSBoxedDartObjects, so isA would always return true for any object (as JSBoxedDartObject contains an @js('Object') annotation). This is not obvious however, and it's much more useful to check that the value is a result of a previous `toJSBox` call. Documentation is updated/cleaned up to make note of the various exceptions in `isA`. CoreLibraryReviewExempt: Web-only library. Change-Id: Ibd8873d3f862f4c950101f5f049327a1aa5c7bf2 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/445522 Commit-Queue: Srujan Gaddam <[email protected]> Reviewed-by: Ömer Ağacan <[email protected]> Reviewed-by: Leaf Petersen <[email protected]>
1 parent bb0580f commit d672d98

File tree

6 files changed

+91
-28
lines changed

6 files changed

+91
-28
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ To learn more about the feature, check out the
7575
- `JSArray.add` is added to avoid cases where during migration from `List` to
7676
`JSArray`, `JSAnyOperatorExtension.add` is accidentally used. See [#59830][]
7777
for more details.
78+
- `isA<JSBoxedDartObject>` now checks that the value was the result of a
79+
`toJSBox` operation instead of returning true for all objects.
7880

7981
[#59830]: https://github.com/dart-lang/sdk/issues/59830
8082

pkg/_js_interop_checks/lib/src/transformations/shared_interop_transformer.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class SharedInteropTransformer extends Transformer {
4141
late StaticInvocation? _invocation;
4242
final Procedure _isA;
4343
final Procedure _isATearoff;
44+
final Procedure _isJSBoxedDartObject;
4445
final ExtensionTypeDeclaration _jsAny;
4546
final ExtensionTypeDeclaration _jsFunction;
4647
final ExtensionTypeDeclaration _jsObject;
@@ -100,6 +101,8 @@ class SharedInteropTransformer extends Transformer {
100101
'dart:js_interop',
101102
'JSAnyUtilityExtension|${LibraryIndex.tearoffPrefix}isA',
102103
),
104+
_isJSBoxedDartObject = _typeEnvironment.coreTypes.index
105+
.getTopLevelProcedure('dart:js_interop', '_isJSBoxedDartObject'),
103106
_jsAny = _typeEnvironment.coreTypes.index.getExtensionType(
104107
'dart:js_interop',
105108
'JSAny',
@@ -549,20 +552,30 @@ class SharedInteropTransformer extends Transformer {
549552
typeofString = 'symbol';
550553
break;
551554
case 'JSTypedArray' when interopTypeDecl == jsType:
552-
// Only do this special case when users are referring directly to
553-
// the `dart:js_interop` type and not some wrapper.
555+
// Only do this special case when users are referring directly to the
556+
// `dart:js_interop` type and not some wrapper.
554557

555558
// `TypedArray` doesn't exist as a property in JS, but rather as a
556559
// superclass of all typed arrays. In order to do the most sensible
557-
// thing here, we can use the prototype of some typed array, and
558-
// check that the receiver is an `instanceof` that prototype.
559-
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description
560+
// thing here, we can use the prototype of some typed array, and check
561+
// that the receiver is an `instanceof` that prototype. See
562+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description
560563
// for more details.
561564
check = StaticInvocation(
562565
_instanceof,
563566
Arguments([VariableGet(any), getInt8ArrayPrototype()]),
564567
);
565568
break;
569+
case 'JSBoxedDartObject' when interopTypeDecl == jsType:
570+
// Only do this special case when users are referring directly to the
571+
// `dart:js_interop` type and not some wrapper.
572+
573+
// Check whether the given value is the result of a previous call to
574+
// `toJSBox`.
575+
check = StaticInvocation(
576+
_isJSBoxedDartObject,
577+
Arguments([VariableGet(any)]),
578+
);
566579
default:
567580
for (final descriptor in interopTypeDecl.memberDescriptors) {
568581
final descriptorNode = descriptor.memberReference!.node;

sdk/lib/_internal/js_shared/lib/js_interop_patch.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,23 @@ final Object _jsBoxedDartObjectProperty = foreign_helper.JS(
9999
'Symbol("jsBoxedDartObjectProperty")',
100100
);
101101

102+
// Returns the value of the property we embed in every `JSBoxedDartObject` in
103+
// `any`.
104+
Object? _getJSBoxedDartObjectPropertyValue(JSAny any) =>
105+
js_util.getProperty<Object?>(any, _jsBoxedDartObjectProperty);
106+
107+
// Used in the `isA` transform.
108+
bool _isJSBoxedDartObject(JSAny any) =>
109+
_getJSBoxedDartObjectPropertyValue(any) != null;
110+
102111
// -----------------------------------------------------------------------------
103112
// JSBoxedDartObject <-> Object
104113
@patch
105114
extension JSBoxedDartObjectToObject on JSBoxedDartObject {
106115
@patch
107116
@pragma('dart2js:prefer-inline')
108117
Object get toDart {
109-
final val = js_util.getProperty<Object?>(this, _jsBoxedDartObjectProperty);
118+
final val = _getJSBoxedDartObjectPropertyValue(this);
110119
if (val == null) {
111120
throw 'Expected a wrapped Dart object, but got a JS object or a wrapped '
112121
'Dart object from a separate runtime instead.';

sdk/lib/_internal/wasm/lib/js_interop_patch.dart

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,26 @@ extension FunctionToJSExportedDartFunction on Function {
119119
final WasmExternRef? _jsBoxedDartObjectPropertyExternRef = js_helper
120120
.JS<WasmExternRef?>('() => Symbol("jsBoxedDartObjectProperty")');
121121

122+
// Returns the value of the property we embed in every `JSBoxedDartObject` in
123+
// `any`.
124+
WasmExternRef? _getJSBoxedDartObjectPropertyValue(JSAny any) =>
125+
js_helper.JS<WasmExternRef?>(
126+
'(o,s) => o[s]',
127+
any.toExternRef,
128+
_jsBoxedDartObjectPropertyExternRef,
129+
);
130+
131+
// Used in the `isA` transform.
132+
bool _isJSBoxedDartObject(JSAny any) =>
133+
!isDartNull(_getJSBoxedDartObjectPropertyValue(any));
134+
122135
// -----------------------------------------------------------------------------
123136
// JSBoxedDartObject <-> Object
124137
@patch
125138
extension JSBoxedDartObjectToObject on JSBoxedDartObject {
126139
@patch
127140
Object get toDart {
128-
final val = js_helper.JS<WasmExternRef?>(
129-
'(o,s) => o[s]',
130-
this.toExternRef,
131-
_jsBoxedDartObjectPropertyExternRef,
132-
);
141+
final val = _getJSBoxedDartObjectPropertyValue(this);
133142
if (isDartNull(val)) {
134143
throw 'Expected a wrapped Dart object, but got a JS object or a wrapped '
135144
'Dart object from a separate runtime instead.';

sdk/lib/js_interop/js_interop.dart

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ export 'dart:js_util' show NullRejectionException;
6262
/// - Renaming external declarations by parameterizing the annotation on the
6363
/// member with [name].
6464
///
65-
/// In the case where [name] is not specified, is `null`, or is empty, the Dart
66-
/// name of the extension type or external declaration is used as the default.
65+
/// In the case where [name] is not specified, the Dart name of the extension
66+
/// type or external declaration is used as the default.
6767
///
6868
/// See https://dart.dev/interop/js-interop/usage#js for more details on how to
6969
/// use this annotation.
@@ -567,17 +567,19 @@ extension JSAnyUtilityExtension on JSAny? {
567567
/// this does an `instanceOfString('JSClass')` check and not an
568568
/// `instanceOfString('Array')` check.
569569
///
570-
/// There are two exceptions to this rule. The first exception is
571-
/// `JSTypedArray`. As `TypedArray` does not exist as a property in
572-
/// JavaScript, this does some prototype checking to make `isA<JSTypedArray>`
573-
/// do the right thing. The other exception is `JSAny`. If you do a
574-
/// `isA<JSAny>` check, it will only do a `null` check.
575-
///
576-
/// Using this method with a [T] that has an object literal constructor will
577-
/// result in an error as you likely want to use [JSObject] instead.
578-
///
579-
/// Using this method with a [T] that wraps a primitive JS type will result in
580-
/// an error telling you to use the primitive JS type instead.
570+
/// There are a few values for [T] that are exceptions to this rule:
571+
/// - `JSTypedArray`: As `TypedArray` does not exist as a class in JavaScript,
572+
/// this does some prototype checking to make `isA<JSTypedArray>` do the
573+
/// right thing.
574+
/// - `JSBoxedDartObject`: `isA<JSBoxedDartObject>` will check if the value is
575+
/// a result of a previous [ObjectToJSBoxedDartObject.toJSBox] call.
576+
/// - `JSAny`: If you do an `isA<JSAny>` check, it will only check for `null`.
577+
/// - User interop types whose representation types are JS primitive types:
578+
/// This will result in an error to avoid confusion on whether the user
579+
/// interop type is used in the type-check. Use the primitive JS type as the
580+
/// value for [T] instead.
581+
/// - User interop types that have an object literal constructor: This will
582+
/// result in an error as you likely want to use [JSObject] instead.
581583
@Since('3.4')
582584
external bool isA<T extends JSAny?>();
583585

tests/lib/js/static_interop_test/isa/functional_test.dart

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ extension type ArraySubtype._(JSArray _) implements JSObject {
3232
external ArraySubtype();
3333
}
3434

35+
extension type WrapJSBoxedDartObject(JSBoxedDartObject _)
36+
implements JSBoxedDartObject {}
37+
38+
@JS('WrapJSBoxedDartObject.prototype')
39+
external JSObject get wrapJSBoxedDartObjectPrototype;
40+
41+
@JS('Object.setPrototypeOf')
42+
external void setPrototypeOf(JSObject obj, JSObject prototype);
43+
3544
@JS()
3645
external JSBigInt BigInt(String val);
3746

@@ -61,9 +70,11 @@ void main() {
6170
Expect.isTrue(nil.isA<JSString?>());
6271
Expect.isTrue(nil.isA<JSObject?>());
6372
Expect.isTrue(nil.isA<JSAny?>());
73+
Expect.isTrue(nil.isA<JSBoxedDartObject?>());
6474
Expect.isFalse(nil.isA<JSString>());
6575
Expect.isFalse(nil.isA<JSObject>());
6676
Expect.isFalse(nil.isA<JSAny>());
77+
Expect.isFalse(nil.isA<JSBoxedDartObject>());
6778

6879
// Test inheritance chain, nullability, and one false case.
6980

@@ -102,16 +113,22 @@ void main() {
102113
final jsObject = ObjectLiteral(a: 0);
103114
testIsJSObject(jsObject);
104115
Expect.isFalse(jsObject.isA<JSBoolean>());
105-
// Note that this is true even though it's a supertype - we can't
106-
// differentiate between the two types. This is okay because users will get a
107-
// consistent error when trying to internalize it.
108-
Expect.isTrue(jsObject.isA<JSBoxedDartObject>());
109116

110117
final jsBox = ''.toJSBox;
111118
Expect.isTrue(jsBox.isA<JSBoxedDartObject>());
112119
Expect.isTrue(jsBox.isA<JSBoxedDartObject?>());
113120
testIsJSObject(jsBox);
114121
Expect.isFalse(jsBox.isA<JSString>());
122+
// While `JSBoxedDartObject`s are just `Object`s, we do additional checking to
123+
// make sure `isA` checks this actually is a `JSBoxedDartObject`.
124+
Expect.isFalse(jsObject.isA<JSBoxedDartObject>());
125+
Expect.isFalse(JSArray().isA<JSBoxedDartObject>());
126+
// Check that property access on primitives isn't invalid.
127+
Expect.isFalse(jsNum.isA<JSBoxedDartObject>());
128+
Expect.isFalse(jsBool.isA<JSBoxedDartObject>());
129+
Expect.isFalse(jsString.isA<JSBoxedDartObject>());
130+
Expect.isFalse(jsBigInt.isA<JSBoxedDartObject>());
131+
Expect.isFalse(jsSymbol.isA<JSBoxedDartObject>());
115132

116133
final jsArray = <JSAny?>[].toJS;
117134
Expect.isTrue(jsArray.isA<JSArray>());
@@ -220,6 +237,8 @@ void main() {
220237
eval('''
221238
class SubtypeArray extends Array {}
222239
globalThis.SubtypeArray = SubtypeArray;
240+
class WrapJSBoxedDartObject {}
241+
globalThis.WrapJSBoxedDartObject = WrapJSBoxedDartObject;
223242
''');
224243

225244
final customJsAny = CustomJSAny(jsArray);
@@ -252,6 +271,15 @@ void main() {
252271
Expect.isTrue(customTypedArray.isA<JSTypedArray>());
253272
Expect.isTrue(customTypedArray.isA<JSTypedArray?>());
254273

274+
final wrapJsBox = WrapJSBoxedDartObject(jsBox);
275+
Expect.isFalse(wrapJsBox.isA<WrapJSBoxedDartObject>());
276+
Expect.isFalse(wrapJsBox.isA<WrapJSBoxedDartObject?>());
277+
Expect.isTrue(nil.isA<WrapJSBoxedDartObject?>());
278+
Expect.isFalse(nil.isA<WrapJSBoxedDartObject>());
279+
setPrototypeOf(wrapJsBox, wrapJSBoxedDartObjectPrototype);
280+
Expect.isTrue(wrapJsBox.isA<WrapJSBoxedDartObject>());
281+
Expect.isTrue(wrapJsBox.isA<WrapJSBoxedDartObject?>());
282+
255283
final arraySubtype = ArraySubtype();
256284
Expect.isTrue(arraySubtype.isA<ArraySubtype>());
257285
Expect.isTrue(arraySubtype.isA<ArraySubtype?>());

0 commit comments

Comments
 (0)