Skip to content

Commit b216616

Browse files
Merge pull request #344 from Workiva/other-identityhashcode-throwing-case
FED-358 Work around identityHashCode throwing for non-extensible objects
2 parents bcc05cd + fbe6c50 commit b216616

File tree

3 files changed

+105
-8
lines changed

3 files changed

+105
-8
lines changed

lib/react_client/js_interop_helpers.dart

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,32 @@ import "package:js/js.dart";
1010

1111
import 'js_backed_map.dart';
1212

13-
/// Like [identityHashCode], but uses a different hash for JS objects to work around an issue where
14-
/// [identityHashCode] adds an unwanted `$identityHash` property on JS objects (https://github.com/dart-lang/sdk/issues/47595).
15-
int _jsObjectFriendlyIdentityHashCode(Object object) =>
16-
object is JsMap ? _jsObjectHashCode(object) : identityHashCode(object);
13+
/// Like [identityHashCode], but uses a different hash for JS objects to work around:
14+
/// - an issue where [identityHashCode] adds an unwanted `$identityHash` property on JS objects: https://github.com/dart-lang/sdk/issues/47595
15+
/// - an issue where [identityHashCode] throws for frozen objects: https://github.com/dart-lang/sdk/issues/36354
16+
int _jsObjectFriendlyIdentityHashCode(Object object) {
17+
// Try detecting JS objects so we don't add properties to them.
18+
// Workaround for https://github.com/dart-lang/sdk/issues/47595
19+
if (object is JsMap) return _anonymousJsObjectOrFrozenObjectHashCode(object);
1720

18-
/// A hashCode implementation for JS objects.
21+
// If this fails, then most likely the object is a frozen JS object, such as props object or a variadic JSX children Array.
22+
// Note that props objects are typically handled by the above is JsMap case, though.
23+
// Fall back to a safe implementation.
24+
try {
25+
return identityHashCode(object);
26+
} catch (_) {
27+
return _anonymousJsObjectOrFrozenObjectHashCode(object);
28+
}
29+
}
30+
31+
/// A hashCode implementation for anonymous JS objects or frozen objects.
1932
///
2033
/// Even though the current implementation of returning the same hash code for all values is low-quality
2134
/// since all JS objects will collide, it is valid since it always returns the same value for the same object.
2235
///
23-
/// We also don't expect many JS objects to be passed into [jsifyAndAllowInterop], so the quality of this hash code
24-
/// is not of much concern.
25-
int _jsObjectHashCode(JsMap jsObject) => 0;
36+
/// We also don't expect many JS objects or frozen objects to be passed into [jsifyAndAllowInterop],
37+
/// so the quality of this hash code is not of much concern.
38+
int _anonymousJsObjectOrFrozenObjectHashCode(Object _) => 0;
2639

2740
// The following code is adapted from `package:js` in the dart-lang/sdk repo:
2841
// https://github.com/dart-lang/sdk/blob/2.2.0/sdk/lib/js_util/dart2js/js_util_dart2js.dart#L27

test/react_client/js_interop_helpers_test.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class Foo {
5050
external num bar();
5151
}
5252

53+
@JS()
54+
external dynamic createArray();
55+
5356
main() {
5457
group('jsifyAndAllowInterop', () {
5558
test('converts a List', () {
@@ -131,5 +134,82 @@ main() {
131134
expect(objectKeys(convertedNestedJsObject), expectedProperties,
132135
reason: 'JS object should not have any additional properties');
133136
});
137+
138+
group('works as expected when the map contains frozen objects:', () {
139+
test('anonymous object', () {
140+
final frozenAnonymousObject = jsify({'foo': 'bar'});
141+
expect(_getPrototypeOf(frozenAnonymousObject), _objectPrototype,
142+
reason: 'test setup check; should not extend from another JS type');
143+
144+
_freeze(frozenAnonymousObject);
145+
expect(_isFrozen(frozenAnonymousObject), isTrue, reason: 'test setup check; should have frozen');
146+
147+
dynamic jsObject;
148+
expect(() {
149+
jsObject = jsifyAndAllowInterop({
150+
'frozenObject': frozenAnonymousObject,
151+
});
152+
}, returnsNormally, reason: 'should not throw when it encounters a frozen object');
153+
final convertedNestedFrozenObject = getProperty(jsObject, 'frozenObject');
154+
expect(convertedNestedFrozenObject, same(frozenAnonymousObject),
155+
reason: 'JS object should have just gotten passed through');
156+
});
157+
158+
test('non-anonymous object', () {
159+
final frozenNonAnynymousObject = Foo(21);
160+
expect(_getPrototypeOf(frozenNonAnynymousObject), isNot(_objectPrototype),
161+
reason: 'test setup check; should extend from another JS type');
162+
163+
_freeze(frozenNonAnynymousObject);
164+
expect(_isFrozen(frozenNonAnynymousObject), isTrue, reason: 'test setup check; should have frozen');
165+
166+
dynamic jsObject;
167+
expect(() {
168+
jsObject = jsifyAndAllowInterop({
169+
'frozenObject': frozenNonAnynymousObject,
170+
});
171+
}, returnsNormally, reason: 'should not throw when it encounters a frozen object');
172+
final convertedNestedFrozenObject = getProperty(jsObject, 'frozenObject');
173+
expect(convertedNestedFrozenObject, same(frozenNonAnynymousObject),
174+
reason: 'JS object should have just gotten passed through');
175+
});
176+
177+
test('array object constructed in JS', () {
178+
// Create this on the JS side so that we're not starting out with Dart's list implementation
179+
// and potentially any wrapper classes.
180+
final frozenArray = createArray();
181+
expect(_getPrototypeOf(frozenArray), _arrayPrototype, reason: 'test setup check; should be an array');
182+
183+
_freeze(frozenArray);
184+
expect(_isFrozen(frozenArray), isTrue, reason: 'test setup check; should have frozen');
185+
186+
dynamic jsObject;
187+
expect(() {
188+
jsObject = jsifyAndAllowInterop({
189+
'frozenArray': frozenArray,
190+
});
191+
}, returnsNormally, reason: 'should not throw when it encounters a frozen object');
192+
final convertedNestedFrozenObject = getProperty(jsObject, 'frozenArray');
193+
// Note that we don't expect it to be `same(frozenArray)` since the array passes the `is Iterable` check,
194+
// and thus gets processed in _convertDataTree.
195+
expect(convertedNestedFrozenObject, equals(frozenArray),
196+
reason: 'JS object should have been converted properly passed through');
197+
});
198+
});
134199
});
135200
}
201+
202+
@JS('Object.prototype')
203+
external dynamic get _objectPrototype;
204+
205+
@JS('Array.prototype')
206+
external dynamic get _arrayPrototype;
207+
208+
@JS('Object.freeze')
209+
external void _freeze(Object object);
210+
211+
@JS('Object.isFrozen')
212+
external bool _isFrozen(Object object);
213+
214+
@JS('Object.getPrototypeOf')
215+
external dynamic _getPrototypeOf(Object object);

test/react_client/js_interop_helpers_test.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
else
2121
return false;
2222
}
23+
24+
function createArray() {
25+
return [1, 2, 3];
26+
}
2327
</script>
2428

2529
<link rel="x-dart-test" href="js_interop_helpers_test.dart">

0 commit comments

Comments
 (0)