[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'.
+}