Implement isSubtypeOf() for RecordType.

Change-Id: I42003fb5ce7bb374a37cc704a85f9eb16ca3aa42
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/254841
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analyzer/lib/src/dart/element/element.dart b/pkg/analyzer/lib/src/dart/element/element.dart
index 4b09012..a4f1282 100644
--- a/pkg/analyzer/lib/src/dart/element/element.dart
+++ b/pkg/analyzer/lib/src/dart/element/element.dart
@@ -6194,10 +6194,14 @@
   @override
   final List<RecordNamedFieldElementImpl> namedFields;
 
+  /// Maybe copy of [namedFields] lexically sorted by names.
+  final List<RecordNamedFieldElementImpl> namedFieldsSorted;
+
   RecordElementImpl({
     required this.positionalFields,
     required this.namedFields,
-  }) : super(null, -1) {
+  })  : namedFieldsSorted = _sortNamedFields(namedFields),
+        super(null, -1) {
     for (final field in positionalFields) {
       field.enclosingElement = this;
     }
@@ -6214,6 +6218,28 @@
     // TODO: implement accept
     throw UnimplementedError();
   }
+
+  /// Returns [fields], if already sorted, or the sorted copy.
+  static List<RecordNamedFieldElementImpl> _sortNamedFields(
+    List<RecordNamedFieldElementImpl> fields,
+  ) {
+    var isSorted = true;
+    String? lastName;
+    for (final field in fields) {
+      final name = field.name;
+      if (lastName != null && lastName.compareTo(name) > 0) {
+        isSorted = false;
+        break;
+      }
+      lastName = name;
+    }
+
+    if (isSorted) {
+      return fields;
+    }
+
+    return fields.sortedBy((field) => field.name);
+  }
 }
 
 class RecordFieldElementImpl extends _ExistingElementImpl
diff --git a/pkg/analyzer/lib/src/dart/element/subtype.dart b/pkg/analyzer/lib/src/dart/element/subtype.dart
index 7ae9fa77..9dfb25a 100644
--- a/pkg/analyzer/lib/src/dart/element/subtype.dart
+++ b/pkg/analyzer/lib/src/dart/element/subtype.dart
@@ -303,7 +303,7 @@
         return true;
       }
       if (T1 is RecordTypeImpl) {
-        // TODO(scheglov) record types
+        return _isRecordSubtypeOf(T0, T1);
       }
     }
 
@@ -483,6 +483,42 @@
     return false;
   }
 
+  /// Check that [subType] is a subtype of [superType].
+  bool _isRecordSubtypeOf(RecordType subType, RecordType superType) {
+    final subPositional = subType.positionalFields;
+    final superPositional = superType.positionalFields;
+    if (subPositional.length != superPositional.length) {
+      return false;
+    }
+
+    final subNamed = subType.namedFields;
+    final superNamed = superType.namedFields;
+    if (subNamed.length != superNamed.length) {
+      return false;
+    }
+
+    for (var i = 0; i < subPositional.length; i++) {
+      final subField = subPositional[i];
+      final superField = superPositional[i];
+      if (!isSubtypeOf(subField.type, superField.type)) {
+        return false;
+      }
+    }
+
+    for (var i = 0; i < subNamed.length; i++) {
+      final subField = subNamed[i];
+      final superField = superNamed[i];
+      if (subField.name != superField.name) {
+        return false;
+      }
+      if (!isSubtypeOf(subField.type, superField.type)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
   static FunctionTypeImpl _functionTypeWithNamedRequired(FunctionType type) {
     return FunctionTypeImpl(
       typeFormals: type.typeFormals,
diff --git a/pkg/analyzer/lib/src/dart/element/type.dart b/pkg/analyzer/lib/src/dart/element/type.dart
index b7f1abb..1b57924 100644
--- a/pkg/analyzer/lib/src/dart/element/type.dart
+++ b/pkg/analyzer/lib/src/dart/element/type.dart
@@ -1015,7 +1015,7 @@
 
   @override
   List<RecordTypeNamedField> get namedFields {
-    return element.namedFields.map((field) {
+    return element.namedFieldsSorted.map((field) {
       final type = substitution.substituteType(field.type);
       return RecordTypeNamedFieldImpl(
         element: field,
diff --git a/pkg/analyzer/test/src/dart/element/subtype_test.dart b/pkg/analyzer/test/src/dart/element/subtype_test.dart
index 514f8e5..cd2c985 100644
--- a/pkg/analyzer/test/src/dart/element/subtype_test.dart
+++ b/pkg/analyzer/test/src/dart/element/subtype_test.dart
@@ -4121,103 +4121,103 @@
     );
   }
 
-  test_record_dynamic() {
-    isSubtype(
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'foo', type: intNone),
-          ],
-        ),
-      ),
-      dynamicNone,
-      strT0: '({int foo})',
-      strT1: 'dynamic',
-    );
-  }
-
   test_record_functionType() {
-    isNotSubtype(
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'foo', type: intNone),
-          ],
-        ),
-      ),
-      functionTypeNone(returnType: voidNone),
-      strT0: '({int foo})',
-      strT1: 'void Function()',
-    );
+    isNotSubtype2('({int foo})', 'void Function()');
   }
 
-  test_record_int() {
-    final R = recordTypeNone(
-      element: recordElement(
-        namedFields: [
-          recordNamedField(name: 'foo', type: intNone),
-        ],
-      ),
-    );
-
-    isNotSubtype(
-      R,
-      intNone,
-      strT0: '({int foo})',
-      strT1: 'int',
-    );
-
-    isNotSubtype(
-      intNone,
-      R,
-      strT0: 'int',
-      strT1: '({int foo})',
-    );
+  test_record_interfaceType() {
+    isNotSubtype2('({int foo})', 'int');
+    isNotSubtype2('int', '({int foo})');
   }
 
   test_record_Never() {
-    isSubtype(
-      neverNone,
+    isNotSubtype2('({int foo})', 'Never');
+    isSubtype2('Never', '({int foo})');
+  }
+
+  test_record_record2_differentShape() {
+    void check(String T1, String T2) {
+      isNotSubtype2(T1, T2);
+      isNotSubtype2(T2, T1);
+    }
+
+    check('(int)', '(int, String)');
+
+    check('({int foo, String bar})', '({int foo})');
+    check('({int foo})', '({int bar})');
+  }
+
+  test_record_record2_sameShape_mixed() {
+    void check(String subType, String superType) {
+      isSubtype2(subType, superType);
+      isNotSubtype2(superType, subType);
+    }
+
+    check('(int, {String bar})', '(int, {Object bar})');
+  }
+
+  test_record_record2_sameShape_named() {
+    void check(String subType, String superType) {
+      isSubtype2(subType, superType);
+      isNotSubtype2(superType, subType);
+    }
+
+    check('({int foo})', '({num foo})');
+
+    isSubtype2('({int foo, String bar})', '({int foo, String bar})');
+    check('({int foo, String bar})', '({int foo, Object bar})');
+    check('({int foo, String bar})', '({num foo, String bar})');
+    check('({int foo, String bar})', '({num foo, Object bar})');
+  }
+
+  test_record_record2_sameShape_named_order() {
+    void check(RecordType subType, RecordType superType) {
+      isSubtype(subType, superType);
+      isSubtype(superType, subType);
+    }
+
+    check(
       recordTypeNone(
         element: recordElement(
           namedFields: [
-            recordNamedField(name: 'foo', type: intNone),
+            recordNamedField(name: 'foo01', type: intNone),
+            recordNamedField(name: 'foo02', type: intNone),
+            recordNamedField(name: 'foo03', type: intNone),
+            recordNamedField(name: 'foo04', type: intNone),
           ],
         ),
       ),
-      strT0: 'Never',
-      strT1: '({int foo})',
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo04', type: intNone),
+            recordNamedField(name: 'foo03', type: intNone),
+            recordNamedField(name: 'foo02', type: intNone),
+            recordNamedField(name: 'foo01', type: intNone),
+          ],
+        ),
+      ),
     );
   }
 
-  test_record_Object() {
-    isSubtype(
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'foo', type: intNone),
-          ],
-        ),
-      ),
-      objectNone,
-      strT0: '({int foo})',
-      strT1: 'Object',
-    );
+  test_record_record2_sameShape_positional() {
+    void check(String subType, String superType) {
+      isSubtype2(subType, superType);
+      isNotSubtype2(superType, subType);
+    }
+
+    check('(int)', '(num)');
+
+    isSubtype2('(int, String)', '(int, String)');
+    check('(int, String)', '(num, String)');
+    check('(int, String)', '(num, Object)');
+    check('(int, String)', '(int, Object)');
   }
 
-  test_record_Record() {
-    isSubtype(
-      recordTypeNone(
-        element: recordElement(
-          namedFields: [
-            recordNamedField(name: 'foo', type: intNone),
-          ],
-        ),
-      ),
-      recordNone,
-      strT0: '({int foo})',
-      strT1: 'Record',
-    );
+  test_record_top() {
+    isSubtype2('({int foo})', 'dynamic');
+    isSubtype2('({int foo})', 'Object');
+    isSubtype2('({int foo})', 'Record');
   }
 
   /// The class `Record` is a subtype of `Object` and `dynamic`, and a
@@ -5529,6 +5529,13 @@
     );
 
     _defineType(
+      'void Function()',
+      functionTypeNone(
+        returnType: voidNone,
+      ),
+    );
+
+    _defineType(
       'int* Function()',
       functionTypeNone(
         returnType: intStar,
@@ -5708,6 +5715,174 @@
         returnType: numQuestion,
       ),
     );
+
+    _defineType('Record', recordNone);
+    _defineType(
+      '(int, String)',
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: intNone),
+            recordPositionalField(type: stringNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '(num, String)',
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: numNone),
+            recordPositionalField(type: stringNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '(int, Object)',
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: intNone),
+            recordPositionalField(type: objectNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '(num, Object)',
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: numNone),
+            recordPositionalField(type: objectNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '(int)',
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: intNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '(num)',
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: numNone),
+          ],
+        ),
+      ),
+    );
+
+    _defineType(
+      '({int foo})',
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo', type: intNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '({num foo})',
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo', type: numNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '({int bar})',
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'bar', type: intNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '({int foo, String bar})',
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo', type: intNone),
+            recordNamedField(name: 'bar', type: stringNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '({int foo, Object bar})',
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo', type: intNone),
+            recordNamedField(name: 'bar', type: objectNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '({num foo, String bar})',
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo', type: numNone),
+            recordNamedField(name: 'bar', type: stringNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '({num foo, Object bar})',
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo', type: numNone),
+            recordNamedField(name: 'bar', type: objectNone),
+          ],
+        ),
+      ),
+    );
+
+    _defineType(
+      '(int, {String bar})',
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: intNone),
+          ],
+          namedFields: [
+            recordNamedField(name: 'bar', type: stringNone),
+          ],
+        ),
+      ),
+    );
+    _defineType(
+      '(int, {Object bar})',
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: intNone),
+          ],
+          namedFields: [
+            recordNamedField(name: 'bar', type: objectNone),
+          ],
+        ),
+      ),
+    );
   }
 
   DartType _getTypeByStr(String str) {