diff --git a/sdk/lib/_internal/js_runtime/lib/constant_map.dart b/sdk/lib/_internal/js_runtime/lib/constant_map.dart index c7a2d848310..669a7e8edbd 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 @@ class GeneralConstantMap extends ConstantMap { Map _getMap() { LinkedHashMap? backingMap = JS('LinkedHashMap|Null', r'#.$map', this); if (backingMap == null) { - backingMap = JsLinkedHashMap(); + backingMap = LinkedHashMap( + 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() : _typeTest()); 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() => (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 00000000000..049486ea41a --- /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 {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 {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' because 'Comparable' 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 00000000000..3ce3460abd5 --- /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 {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>(m1.keys); + Expect.type>(m1.keys); + Expect.notType>(m1.keys); +} + +void test2() { + var m2 = const {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>(m2.keys); + Expect.type>(m2.keys); + Expect.notType>(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() {}