Implement runtime type equality for record types.

Change-Id: I06a717fddcc8600afa39b4a33890415456eb3173
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/255546
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analyzer/lib/dart/element/type_visitor.dart b/pkg/analyzer/lib/dart/element/type_visitor.dart
index ceedb4b..b2e5271 100644
--- a/pkg/analyzer/lib/dart/element/type_visitor.dart
+++ b/pkg/analyzer/lib/dart/element/type_visitor.dart
@@ -39,6 +39,8 @@
 
   R visitNeverType(NeverType type, A argument);
 
+  R visitRecordType(RecordType type, A argument);
+
   R visitTypeParameterType(TypeParameterType type, A argument);
 
   R visitVoidType(VoidType type, A argument);
diff --git a/pkg/analyzer/lib/src/dart/element/runtime_type_equality.dart b/pkg/analyzer/lib/src/dart/element/runtime_type_equality.dart
index c0bce30..3f67085 100644
--- a/pkg/analyzer/lib/src/dart/element/runtime_type_equality.dart
+++ b/pkg/analyzer/lib/src/dart/element/runtime_type_equality.dart
@@ -115,6 +115,50 @@
   }
 
   @override
+  bool visitRecordType(RecordType T1, DartType T2) {
+    if (T1 is! RecordTypeImpl || T2 is! RecordTypeImpl) {
+      return false;
+    }
+
+    if (!_compatibleNullability(T1, T2)) {
+      return false;
+    }
+
+    final positional1 = T1.positionalFields;
+    final positional2 = T2.positionalFields;
+    if (positional1.length != positional2.length) {
+      return false;
+    }
+
+    final named1 = T1.namedFields;
+    final named2 = T2.namedFields;
+    if (named1.length != named2.length) {
+      return false;
+    }
+
+    for (var i = 0; i < positional1.length; i++) {
+      final field1 = positional1[i];
+      final field2 = positional2[i];
+      if (!field1.type.acceptWithArgument(this, field2.type)) {
+        return false;
+      }
+    }
+
+    for (var i = 0; i < named1.length; i++) {
+      final field1 = named1[i];
+      final field2 = named2[i];
+      if (field1.name != field2.name) {
+        return false;
+      }
+      if (!field1.type.acceptWithArgument(this, field2.type)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  @override
   bool visitTypeParameterType(TypeParameterType T1, DartType T2) {
     return T2 is TypeParameterType &&
         _compatibleNullability(T1, T2) &&
diff --git a/pkg/analyzer/lib/src/dart/element/type.dart b/pkg/analyzer/lib/src/dart/element/type.dart
index 7a3f98b..5b34c0e 100644
--- a/pkg/analyzer/lib/src/dart/element/type.dart
+++ b/pkg/analyzer/lib/src/dart/element/type.dart
@@ -1090,8 +1090,7 @@
   @override
   R acceptWithArgument<R, A>(
       TypeVisitorWithArgument<R, A> visitor, A argument) {
-    // TODO: implement acceptWithArgument
-    throw UnimplementedError();
+    return visitor.visitRecordType(this, argument);
   }
 
   @override
@@ -1100,9 +1099,16 @@
   }
 
   @override
-  TypeImpl withNullability(NullabilitySuffix nullabilitySuffix) {
-    // TODO: implement withNullability
-    throw UnimplementedError();
+  RecordTypeImpl withNullability(NullabilitySuffix nullabilitySuffix) {
+    if (this.nullabilitySuffix == nullabilitySuffix) {
+      return this;
+    }
+
+    return RecordTypeImpl(
+      element2: element2,
+      fieldTypes: fieldTypes,
+      nullabilitySuffix: nullabilitySuffix,
+    );
   }
 }
 
diff --git a/pkg/analyzer/test/src/dart/element/runtime_type_equality_test.dart b/pkg/analyzer/test/src/dart/element/runtime_type_equality_test.dart
index 7efc4f3..0b6828c 100644
--- a/pkg/analyzer/test/src/dart/element/runtime_type_equality_test.dart
+++ b/pkg/analyzer/test/src/dart/element/runtime_type_equality_test.dart
@@ -8,6 +8,7 @@
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import '../../../generated/type_system_base.dart';
+import 'string_types.dart';
 
 main() {
   defineReflectiveSuite(() {
@@ -16,7 +17,14 @@
 }
 
 @reflectiveTest
-class RuntimeTypeEqualityTypeTest extends AbstractTypeSystemTest {
+class RuntimeTypeEqualityTypeTest extends AbstractTypeSystemTest
+    with StringTypes {
+  @override
+  void setUp() {
+    super.setUp();
+    defineStringTypes();
+  }
+
   test_dynamic() {
     _equal(dynamicNone, dynamicNone);
     _notEqual(dynamicNone, voidNone);
@@ -289,6 +297,35 @@
     _equal(neverQuestion, nullNone);
   }
 
+  test_recordType_andNot() {
+    _notEqual2('(int)', 'dynamic');
+    _notEqual2('(int)', 'int');
+    _notEqual2('(int)', 'void');
+  }
+
+  test_recordType_differentShape() {
+    _notEqual2('(int)', '(int, int)');
+    _notEqual2('(int)', '({int f1})');
+    _notEqual2('({int f1})', '({int f2})');
+    _notEqual2('({int f1})', '({int f1, int f2})');
+  }
+
+  test_recordType_sameShape_named() {
+    _equal2('({int f1})', '({int f1})');
+    _notEqual2('({int f1})', '({int? f1})');
+    _equal2('({int f1})', '({int* f1})');
+
+    _notEqual2('({int f1})', '({double f1})');
+  }
+
+  test_recordType_sameShape_positional() {
+    _equal2('(int)', '(int)');
+    _notEqual2('(int)', '(int?)');
+    _equal2('(int)', '(int*)');
+
+    _notEqual2('(int)', '(double)');
+  }
+
   test_void() {
     _equal(voidNone, voidNone);
     _notEqual(voidNone, dynamicNone);
@@ -325,10 +362,24 @@
     _check(T1, T2, true);
   }
 
+  void _equal2(String T1, String T2) {
+    _equal(
+      typeOfString(T1),
+      typeOfString(T2),
+    );
+  }
+
   void _notEqual(DartType T1, DartType T2) {
     _check(T1, T2, false);
   }
 
+  void _notEqual2(String T1, String T2) {
+    _notEqual(
+      typeOfString(T1),
+      typeOfString(T2),
+    );
+  }
+
   String _typeString(DartType type) {
     return type.getDisplayString(withNullability: true);
   }
diff --git a/pkg/analyzer/test/src/dart/element/string_types.dart b/pkg/analyzer/test/src/dart/element/string_types.dart
index d11196a..2081d30 100644
--- a/pkg/analyzer/test/src/dart/element/string_types.dart
+++ b/pkg/analyzer/test/src/dart/element/string_types.dart
@@ -403,259 +403,68 @@
   void _defineRecordTypes() {
     _defineType('Record', recordNone);
 
-    _defineType(
-      '(double)',
-      recordTypeNone(
+    void mixed(
+      String str,
+      List<DartType> positionalTypes,
+      Map<String, DartType> namedTypes,
+    ) {
+      final type = recordTypeNone(
         element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: doubleNone),
-          ],
+          positionalFields: positionalTypes.map(
+            (fieldType) {
+              return recordPositionalField(type: fieldType);
+            },
+          ).toList(),
+          namedFields: namedTypes.entries.map((entry) {
+            return recordNamedField(name: entry.key, type: entry.value);
+          }).toList(),
         ),
-      ),
-    );
-    _defineType(
-      '(int)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: intNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '(num)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: numNone),
-          ],
-        ),
-      ),
-    );
+      );
+      expect(type.toString(), str);
+      _defineType(str, type);
+    }
 
-    _defineType(
-      '(double, int)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: doubleNone),
-            recordPositionalField(type: intNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '(int, double)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: intNone),
-            recordPositionalField(type: doubleNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '(int, Object)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: intNone),
-            recordPositionalField(type: objectNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '(int, String)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: intNone),
-            recordPositionalField(type: stringNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '(num, num)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: numNone),
-            recordPositionalField(type: numNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '(num, Object)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: numNone),
-            recordPositionalField(type: objectNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '(num, String)',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: numNone),
-            recordPositionalField(type: stringNone),
-          ],
-        ),
-      ),
-    );
+    void allPositional(String str, List<DartType> types) {
+      mixed(str, types, const {});
+    }
 
-    _defineType(
-      '({double f1})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: doubleNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({int f1})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: intNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({num f1})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: numNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({int f2})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f2', type: intNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({double f1, int f2})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: doubleNone),
-            recordNamedField(name: 'f2', type: intNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({int f1, double f2})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: intNone),
-            recordNamedField(name: 'f2', type: doubleNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({int f1, String f2})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: intNone),
-            recordNamedField(name: 'f2', type: stringNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({int f1, Object f2})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: intNone),
-            recordNamedField(name: 'f2', type: objectNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({num f1, num f2})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: numNone),
-            recordNamedField(name: 'f2', type: numNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({num f1, String f2})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: numNone),
-            recordNamedField(name: 'f2', type: stringNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '({num f1, Object f2})',
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'f1', type: numNone),
-            recordNamedField(name: 'f2', type: objectNone),
-          ],
-        ),
-      ),
-    );
+    allPositional('(double)', [doubleNone]);
+    allPositional('(int)', [intNone]);
+    allPositional('(int?)', [intQuestion]);
+    allPositional('(int*)', [intStar]);
+    allPositional('(num)', [numNone]);
 
-    _defineType(
-      '(int, {String f2})',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: intNone),
-          ],
-          namedFields: [
-            recordNamedField(name: 'f2', type: stringNone),
-          ],
-        ),
-      ),
-    );
-    _defineType(
-      '(int, {Object f2})',
-      recordTypeNone(
-        element: recordElement(
-          positionalFields: [
-            recordPositionalField(type: intNone),
-          ],
-          namedFields: [
-            recordNamedField(name: 'f2', type: objectNone),
-          ],
-        ),
-      ),
-    );
+    allPositional('(double, int)', [doubleNone, intNone]);
+    allPositional('(int, double)', [intNone, doubleNone]);
+    allPositional('(int, int)', [intNone, intNone]);
+    allPositional('(int, Object)', [intNone, objectNone]);
+    allPositional('(int, String)', [intNone, stringNone]);
+    allPositional('(num, num)', [numNone, numNone]);
+    allPositional('(num, Object)', [numNone, objectNone]);
+    allPositional('(num, String)', [numNone, stringNone]);
+
+    void allNamed(String str, Map<String, DartType> types) {
+      mixed(str, const [], types);
+    }
+
+    allNamed('({double f1})', {'f1': doubleNone});
+    allNamed('({int f1})', {'f1': intNone});
+    allNamed('({int? f1})', {'f1': intQuestion});
+    allNamed('({int* f1})', {'f1': intStar});
+    allNamed('({num f1})', {'f1': numNone});
+    allNamed('({int f2})', {'f2': intNone});
+
+    allNamed('({double f1, int f2})', {'f1': doubleNone, 'f2': intNone});
+    allNamed('({int f1, double f2})', {'f1': intNone, 'f2': doubleNone});
+    allNamed('({int f1, int f2})', {'f1': intNone, 'f2': intNone});
+    allNamed('({int f1, Object f2})', {'f1': intNone, 'f2': objectNone});
+    allNamed('({int f1, String f2})', {'f1': intNone, 'f2': stringNone});
+    allNamed('({num f1, num f2})', {'f1': numNone, 'f2': numNone});
+    allNamed('({num f1, Object f2})', {'f1': numNone, 'f2': objectNone});
+    allNamed('({num f1, String f2})', {'f1': numNone, 'f2': stringNone});
+
+    mixed('(int, {Object f2})', [intNone], {'f2': objectNone});
+    mixed('(int, {String f2})', [intNone], {'f2': stringNone});
   }
 
   void _defineType(String str, DartType type) {