Implement NORM for RecordType.

Also adds `RecordType.==` as equality of the shape and field types.
We intentionally don't check that elements are the same.

Change-Id: I4f5ef66a838a410843696f4f569ce3ed36bb7053
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/255147
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analyzer/lib/src/dart/element/normalize.dart b/pkg/analyzer/lib/src/dart/element/normalize.dart
index 136dd8c..5ae6067 100644
--- a/pkg/analyzer/lib/src/dart/element/normalize.dart
+++ b/pkg/analyzer/lib/src/dart/element/normalize.dart
@@ -145,6 +145,15 @@
       );
     }
 
+    // NORM(Record(T0, ..., Tn)) = Record(R0, ..., Rn) where Ri is NORM(Ti)
+    if (T is RecordTypeImpl) {
+      return RecordTypeImpl(
+        element2: T.element2,
+        fieldTypes: T.fieldTypes.map(_normalize).toList(),
+        nullabilitySuffix: NullabilitySuffix.none,
+      );
+    }
+
     // NORM(R Function<X extends B>(S)) = R1 Function(X extends B1>(S1)
     return _functionType(T as FunctionType);
   }
diff --git a/pkg/analyzer/lib/src/dart/element/type.dart b/pkg/analyzer/lib/src/dart/element/type.dart
index 6361b9a..7a3f98b 100644
--- a/pkg/analyzer/lib/src/dart/element/type.dart
+++ b/pkg/analyzer/lib/src/dart/element/type.dart
@@ -1012,6 +1012,14 @@
   @override
   RecordElementImpl get element => element2;
 
+  @override
+  int get hashCode {
+    return Object.hash(
+      element2.positionalFields,
+      element2.namedFieldsSorted.length,
+    );
+  }
+
   @Deprecated('Check element, or use getDisplayString()')
   @override
   String? get name => null;
@@ -1039,6 +1047,42 @@
   }
 
   @override
+  bool operator ==(Object other) {
+    if (identical(other, this)) {
+      return true;
+    }
+
+    if (other is! RecordTypeImpl) {
+      return false;
+    }
+
+    if (other.nullabilitySuffix != nullabilitySuffix) {
+      return false;
+    }
+
+    final thisPositional = positionalFields;
+    final otherPositional = other.positionalFields;
+    if (thisPositional.length != otherPositional.length) {
+      return false;
+    }
+
+    final thisNamed = namedFields;
+    final otherNamed = other.namedFields;
+    if (thisNamed.length != otherNamed.length) {
+      return false;
+    }
+    for (var i = 0; i < thisNamed.length; i++) {
+      final thisField = thisNamed[i];
+      final otherField = otherNamed[i];
+      if (thisField.name != otherField.name) {
+        return false;
+      }
+    }
+
+    return TypeImpl.equalArrays(other.fieldTypes, fieldTypes);
+  }
+
+  @override
   R accept<R>(TypeVisitor<R> visitor) {
     return visitor.visitRecordType(this);
   }
diff --git a/pkg/analyzer/test/src/dart/element/normalize_type_test.dart b/pkg/analyzer/test/src/dart/element/normalize_type_test.dart
index e3c53d6..aa6ea5d 100644
--- a/pkg/analyzer/test/src/dart/element/normalize_type_test.dart
+++ b/pkg/analyzer/test/src/dart/element/normalize_type_test.dart
@@ -338,6 +338,59 @@
     check(futureOrQuestion(objectStar), objectStar);
   }
 
+  test_recordType() {
+    _check(
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: intNone),
+          ],
+        ),
+      ),
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: intNone),
+          ],
+        ),
+      ),
+    );
+
+    _check(
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: futureOrNone(objectNone)),
+          ],
+        ),
+      ),
+      recordTypeNone(
+        element: recordElement(
+          positionalFields: [
+            recordPositionalField(type: objectNone),
+          ],
+        ),
+      ),
+    );
+
+    _check(
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo', type: futureOrNone(objectNone)),
+          ],
+        ),
+      ),
+      recordTypeNone(
+        element: recordElement(
+          namedFields: [
+            recordNamedField(name: 'foo', type: objectNone),
+          ],
+        ),
+      ),
+    );
+  }
+
   /// NORM(T*)
   /// * let S be NORM(T)
   test_star() {
diff --git a/pkg/analyzer/test/src/dart/element/upper_lower_bound_test.dart b/pkg/analyzer/test/src/dart/element/upper_lower_bound_test.dart
index 9a1b70f..4a3a6d0 100644
--- a/pkg/analyzer/test/src/dart/element/upper_lower_bound_test.dart
+++ b/pkg/analyzer/test/src/dart/element/upper_lower_bound_test.dart
@@ -3501,7 +3501,10 @@
 
     var result = typeSystem.getLeastUpperBound(T1, T2);
     var resultStr = _typeString(result);
-    expect(resultStr, expectedStr);
+    expect(result, expected, reason: '''
+expected: $expectedStr
+actual: $resultStr
+''');
 
     // Check that the result is an upper bound.
     expect(typeSystem.isSubtypeOf(T1, result), true);
@@ -3510,7 +3513,10 @@
     // Check for symmetry.
     result = typeSystem.getLeastUpperBound(T2, T1);
     resultStr = _typeString(result);
-    expect(resultStr, expectedStr);
+    expect(result, expected, reason: '''
+expected: $expectedStr
+actual: $resultStr
+''');
   }
 
   void _checkLeastUpperBound2(String T1, String T2, String expected) {