[js_runtime] Use custom hashCode for GeneralConstantMap
Fixes #46580
Change-Id: Ida2b7df75415881973085f9afeacd9ee384fd910
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/207160
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Mayank Patke <fishythefish@google.com>
diff --git a/sdk/lib/_internal/js_runtime/lib/constant_map.dart b/sdk/lib/_internal/js_runtime/lib/constant_map.dart
index c7a2d84..669a7e8 100644
--- a/sdk/lib/_internal/js_runtime/lib/constant_map.dart
+++ b/sdk/lib/_internal/js_runtime/lib/constant_map.dart
@@ -173,13 +173,35 @@
Map<K, V> _getMap() {
LinkedHashMap<K, V>? backingMap = JS('LinkedHashMap|Null', r'#.$map', this);
if (backingMap == null) {
- backingMap = JsLinkedHashMap<K, V>();
+ backingMap = LinkedHashMap<K, V>(
+ hashCode: _constantMapHashCode,
+ // In legacy mode (--no-sound-null-safety), `null` keys are
+ // permitted. In sound mode, `null` keys are permitted only if [K] is
+ // nullable.
+ isValidKey: JS_GET_FLAG('LEGACY') ? _typeTest<K?>() : _typeTest<K>());
fillLiteralMap(_jsData, backingMap);
JS('', r'#.$map = #', this, backingMap);
}
return backingMap;
}
+ static int _constantMapHashCode(Object? key) {
+ // Types are tested here one-by-one so that each call to get:hashCode can be
+ // resolved differently.
+
+ // Some common primitives in a GeneralConstantMap.
+ if (key is num) return key.hashCode; // One method on JSNumber.
+
+ // Specially handled known types.
+ if (key is Symbol) return key.hashCode;
+ if (key is Type) return key.hashCode;
+
+ // Everything else, including less common primitives.
+ return identityHashCode(key);
+ }
+
+ static bool Function(Object?) _typeTest<T>() => (Object? o) => o is T;
+
bool containsValue(Object? needle) {
return _getMap().containsValue(needle);
}
diff --git a/tests/language/map/literal15_test.dart b/tests/language/map/literal15_test.dart
new file mode 100644
index 0000000..049486e
--- /dev/null
+++ b/tests/language/map/literal15_test.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Test the use of `null` keys in const maps.
+
+library map_literal15_test;
+
+import "package:expect/expect.dart";
+
+void main() {
+ var m1 = const <String, int>{null: 10, 'null': 20};
+ // ^^^^
+ // [analyzer] COMPILE_TIME_ERROR.MAP_KEY_TYPE_NOT_ASSIGNABLE
+ // [cfe] The value 'null' can't be assigned to a variable of type 'String' because 'String' is not nullable.
+
+ var m2 = const <Comparable, int>{null: 10, 'null': 20};
+ // ^^^^
+ // [analyzer] COMPILE_TIME_ERROR.MAP_KEY_TYPE_NOT_ASSIGNABLE
+ // [cfe] The value 'null' can't be assigned to a variable of type 'Comparable<dynamic>' because 'Comparable<dynamic>' is not nullable.
+}
diff --git a/tests/language_2/map/literal15_test.dart b/tests/language_2/map/literal15_test.dart
new file mode 100644
index 0000000..3ce3460
--- /dev/null
+++ b/tests/language_2/map/literal15_test.dart
@@ -0,0 +1,54 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart = 2.9
+
+// Test the use of `null` keys in const maps. In versions before 2.12, when
+// nullable types were introduced, types were nullable so it was legal to have
+// `null` keys in maps.
+
+library map_literal15_test;
+
+import "package:expect/expect.dart";
+
+void test1() {
+ var m1 = const <String, int>{null: 10, 'null': 20};
+ Expect.isTrue(m1.containsKey(null));
+ Expect.isTrue(m1.containsKey(undefined()));
+ Expect.equals(10, m1[null]);
+ Expect.equals(10, m1[undefined()]);
+ Expect.isTrue(m1.containsKey('null'));
+ Expect.equals(20, m1['null']);
+ // The '.keys' carry the 'String' type
+ Expect.type<Iterable<String>>(m1.keys);
+ Expect.type<Iterable<Comparable>>(m1.keys);
+ Expect.notType<Iterable<int>>(m1.keys);
+}
+
+void test2() {
+ var m2 = const <Comparable, int>{null: 10, 'null': 20};
+ Expect.isTrue(m2.containsKey(null));
+ Expect.isTrue(m2.containsKey(undefined()));
+ Expect.equals(10, m2[null]);
+ Expect.equals(10, m2[undefined()]);
+ Expect.isTrue(m2.containsKey('null'));
+ Expect.equals(20, m2['null']);
+ // The '.keys' carry the 'Comparable' type
+ Expect.notType<Iterable<String>>(m2.keys);
+ Expect.type<Iterable<Comparable>>(m2.keys);
+ Expect.notType<Iterable<int>>(m2.keys);
+}
+
+main() {
+ test1();
+ test2();
+}
+
+// Calling `undefined()` gives us a `null` that is implemented as JavaScript
+// `undefined` on dart2js.
+@pragma('dart2js:noInline')
+dynamic get undefined => _undefined;
+
+@pragma('dart2js:noInline')
+void _undefined() {}