[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() {}