[beta] Fix handling of extension types in relational patterns.
In https://dart-review.googlesource.com/c/sdk/+/345082, type erasure
was added to the handling of relational patterns, to address some co19
failures, e.g.:
extension type const BoolET1(bool _) {}
const True1 = BoolET1(true);
String testStatement1(bool b) {
switch (b) {
case == True1:
...
}
}
This was failing because the type of `True1` (`BoolET1`) is not
assignable to the argument type of `operator==`, which is
`Object`. (This is because extension types do not, by default, extend
`Object`; they extend `Object?`).
Adding type erasure elimited the co19 failure, but it caused other
code to be allowed that shouldn't be allowed, such as:
extension type const E(int representation) implements Object {}
class A {
bool operator <(int other) => ...;
}
const E0 = E(0);
test(A a) {
if (a case < E0) ...;
}
This shouldn't be allowed because the type expected by `A.<` is `int`;
allowing `E0` to be passed to this operator breaks extension type
encapsulation.
The correct fix is for assignability checks for `operator==` to use
`S?` rather than `S`, where `S` is the argument type of
`operator==`. This is consistent with the patterns specification, and
it ensures that `== null` and `!= null` are allowed, while continuing
to prohibit relational patterns that break extension type
encapsulation.
Fixes https://github.com/dart-lang/sdk/issues/54594.
Bug: https://github.com/dart-lang/sdk/issues/54594.
Change-Id: Id1f7a52f75e91c0b3b3d535aab68d38da68d37a5
Cherry-pick: https://dart-review.googlesource.com/c/sdk/+/345860
Cherry-pick-request: https://github.com/dart-lang/sdk/issues/54612
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/345826
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
index bef1b4b..f1292bf 100644
--- a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
@@ -1630,13 +1630,15 @@
Error? argumentTypeNotAssignableError;
Error? operatorReturnTypeNotAssignableToBoolError;
if (operator != null) {
- Type argumentType =
- isEquality ? operations.promoteToNonNull(operandType) : operandType;
- if (!operations.isAssignableTo(argumentType, operator.parameterType)) {
+ Type parameterType = operator.parameterType;
+ if (isEquality) {
+ parameterType = operations.makeNullable(parameterType);
+ }
+ if (!operations.isAssignableTo(operandType, parameterType)) {
argumentTypeNotAssignableError =
errors.relationalPatternOperandTypeNotAssignable(
pattern: node,
- operandType: argumentType,
+ operandType: operandType,
parameterType: operator.parameterType,
);
}
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index b105dbb..253f219 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -1619,6 +1619,8 @@
class Harness {
static Map<String, Type> _coreMemberTypes = {
+ 'int.<': Type('bool Function(num)'),
+ 'int.<=': Type('bool Function(num)'),
'int.>': Type('bool Function(num)'),
'int.>=': Type('bool Function(num)'),
'num.sign': Type('num'),
@@ -1684,6 +1686,12 @@
operations.addExhaustiveness(type, isExhaustive);
}
+ /// Updates the harness so that when an extension type erasure query is
+ /// invoked on type [type], [representation] will be returned.
+ void addExtensionTypeErasure(String type, String representation) {
+ operations.addExtensionTypeErasure(type, representation);
+ }
+
/// Updates the harness so that when member [memberName] is looked up on type
/// [targetType], a member is found having the given [type].
///
@@ -2617,6 +2625,7 @@
'int, num': Type('num'),
'Never, int': Type('int'),
'Null, int': Type('int?'),
+ 'Null, Object': Type('Object?'),
'?, int': Type('int'),
'?, List<?>': Type('List<?>'),
'?, Null': Type('Null'),
@@ -2682,6 +2691,8 @@
final Map<String, bool> _exhaustiveness = Map.of(_coreExhaustiveness);
+ final Map<String, Type> _extensionTypeErasure = {};
+
final Map<String, Type> _glbs = Map.of(_coreGlbs);
final Map<String, Type> _lubs = Map.of(_coreLubs);
@@ -2726,6 +2737,12 @@
_exhaustiveness[type] = isExhaustive;
}
+ /// Updates the harness so that when an extension type erasure query is
+ /// invoked on type [type], [representation] will be returned.
+ void addExtensionTypeErasure(String type, String representation) {
+ _extensionTypeErasure[type] = Type(representation);
+ }
+
void addPromotionException(String from, String to, String result) {
(_promotionExceptions[from] ??= {})[to] = result;
}
@@ -2783,6 +2800,11 @@
fail('Unknown downward inference query: $query');
}
+ Type extensionTypeErasure(Type type) {
+ var query = '$type';
+ return _extensionTypeErasure[query] ?? type;
+ }
+
@override
Type factor(Type from, Type what) {
return _typeSystem.factor(from, what);
diff --git a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
index 812b09b..fc87758 100644
--- a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
+++ b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
@@ -3587,19 +3587,129 @@
'matchedType: Object), variables(), true, block(), noop)')
]);
});
- test('argument type not assignable', () {
- h.run([
- ifCase(
- expr('int').checkContext('?'),
- relationalPattern('>', expr('String'))..errorId = 'PATTERN',
- [],
- ).checkIR('ifCase(expr(int), >(expr(String), '
- 'matchedType: int), variables(), true, block(), noop)')
- ], expectedErrors: {
- 'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
- 'operandType: String, parameterType: num)'
+
+ group('argument type not assignable:', () {
+ test('basic', () {
+ h.run([
+ ifCase(
+ expr('int').checkContext('?'),
+ relationalPattern('>', expr('String'))..errorId = 'PATTERN',
+ [],
+ ).checkIR('ifCase(expr(int), >(expr(String), '
+ 'matchedType: int), variables(), true, block(), noop)')
+ ], expectedErrors: {
+ 'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
+ 'operandType: String, parameterType: num)'
+ });
+ });
+
+ test('> nullable', () {
+ h.run([
+ ifCase(
+ expr('int'),
+ relationalPattern('>', expr('int?'))..errorId = 'PATTERN',
+ [],
+ )
+ ], expectedErrors: {
+ 'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
+ 'operandType: int?, parameterType: num)'
+ });
+ });
+
+ test('< nullable', () {
+ h.run([
+ ifCase(
+ expr('int'),
+ relationalPattern('<', expr('int?'))..errorId = 'PATTERN',
+ [],
+ )
+ ], expectedErrors: {
+ 'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
+ 'operandType: int?, parameterType: num)'
+ });
+ });
+
+ test('>= nullable', () {
+ h.run([
+ ifCase(
+ expr('int'),
+ relationalPattern('>=', expr('int?'))..errorId = 'PATTERN',
+ [],
+ )
+ ], expectedErrors: {
+ 'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
+ 'operandType: int?, parameterType: num)'
+ });
+ });
+
+ test('<= nullable', () {
+ h.run([
+ ifCase(
+ expr('int'),
+ relationalPattern('<=', expr('int?'))..errorId = 'PATTERN',
+ [],
+ )
+ ], expectedErrors: {
+ 'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
+ 'operandType: int?, parameterType: num)'
+ });
+ });
+
+ test('extension type to representation', () {
+ h.addSuperInterfaces('E', (_) => [Type('Object?')]);
+ h.addExtensionTypeErasure('E', 'int');
+ h.addMember('C', '>', 'bool Function(int)');
+ h.run([
+ ifCase(
+ expr('C'),
+ relationalPattern('>', expr('E'))..errorId = 'PATTERN',
+ [],
+ )
+ ], expectedErrors: {
+ 'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
+ 'operandType: E, parameterType: int)'
+ });
+ });
+
+ test('representation to extension type', () {
+ h.addSuperInterfaces('E', (_) => [Type('Object?')]);
+ h.addExtensionTypeErasure('E', 'int');
+ h.addMember('C', '>', 'bool Function(E)');
+ h.run([
+ ifCase(
+ expr('C'),
+ relationalPattern('>', expr('int'))..errorId = 'PATTERN',
+ [],
+ )
+ ], expectedErrors: {
+ 'relationalPatternOperandTypeNotAssignable(pattern: PATTERN, '
+ 'operandType: int, parameterType: E)'
+ });
});
});
+
+ group('argument type assignable:', () {
+ test('== nullable', () {
+ h.run([
+ ifCase(
+ expr('int'),
+ relationalPattern('==', expr('int?')),
+ [],
+ )
+ ]);
+ });
+
+ test('!= nullable', () {
+ h.run([
+ ifCase(
+ expr('int'),
+ relationalPattern('!=', expr('int?')),
+ [],
+ )
+ ]);
+ });
+ });
+
test('return type is not assignable to bool', () {
h.addMember('A', '>', 'int Function(Object)');
h.run([
diff --git a/tests/language/extension_type/relational_pattern_error_test.dart b/tests/language/extension_type/relational_pattern_error_test.dart
new file mode 100644
index 0000000..f1cab38
--- /dev/null
+++ b/tests/language/extension_type/relational_pattern_error_test.dart
@@ -0,0 +1,77 @@
+// Copyright (c) 2024, 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.
+
+// This test verifies that extension type erasure is not performed
+// when checking the type argument of a relational pattern.
+
+extension type const E(int representation) implements Object {}
+
+class A {
+ bool operator ==(covariant int other) => true;
+ bool operator <(int other) => true;
+ bool operator <=(int other) => true;
+ bool operator >(int other) => true;
+ bool operator >=(int other) => true;
+}
+
+class B {
+ bool operator ==(covariant E other) => true;
+ bool operator <(E other) => true;
+ bool operator <=(E other) => true;
+ bool operator >(E other) => true;
+ bool operator >=(E other) => true;
+}
+
+const E0 = E(0);
+
+test() {
+ if (A() case == E0) {}
+ // ^^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
+ if (A() case != E0) {}
+ // ^^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
+ if (A() case > E0) {}
+ // ^^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
+ if (A() case >= E0) {}
+ // ^^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
+ if (A() case < E0) {}
+ // ^^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
+ if (A() case <= E0) {}
+ // ^^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'E' can't be assigned to the parameter type 'int'.
+ if (B() case == 0) {}
+ // ^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
+ if (B() case != 0) {}
+ // ^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
+ if (B() case > 0) {}
+ // ^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
+ if (B() case >= 0) {}
+ // ^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
+ if (B() case < 0) {}
+ // ^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
+ if (B() case <= 0) {}
+ // ^
+ // [analyzer] COMPILE_TIME_ERROR.RELATIONAL_PATTERN_OPERAND_TYPE_NOT_ASSIGNABLE
+ // [cfe] The argument type 'int' can't be assigned to the parameter type 'E'.
+}