Add record type parsing to mini_types.dart.

We now no longer need the `expr2` function to test expressions whose
type is RecordType.

Also added unit tests of the type parser in mini_types.dart (which was
previously tested only by virtue of its use in other tests).

Change-Id: I5d351f3ff924676b4d84e65c8949f42feca0dab9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/267522
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index e48cf37..ca5839a 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -122,11 +122,6 @@
 Expression expr(String typeStr) =>
     new _PlaceholderExpression(new Type(typeStr), location: computeLocation());
 
-/// Creates a pseudo-expression having type [type] that otherwise has no
-/// effect on flow analysis.
-Expression expr2(Type type) =>
-    new _PlaceholderExpression(type, location: computeLocation());
-
 /// Creates a conventional `for` statement.  Optional boolean [forCollection]
 /// indicates that this `for` statement is actually a collection element, so
 /// `null` should be passed to [for_bodyBegin].
diff --git a/pkg/_fe_analyzer_shared/test/mini_types.dart b/pkg/_fe_analyzer_shared/test/mini_types.dart
index 114b409..43d3750 100644
--- a/pkg/_fe_analyzer_shared/test/mini_types.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_types.dart
@@ -85,6 +85,16 @@
   }
 }
 
+/// Exception thrown if a type fails to parse properly.
+class ParseError extends Error {
+  final String message;
+
+  ParseError(this.message);
+
+  @override
+  String toString() => message;
+}
+
 /// Representation of a promoted type parameter type suitable for unit testing
 /// of code in the `_fe_analyzer_shared` package.  A promoted type parameter is
 /// often written using the syntax `a&b`, where `a` is the type parameter and
@@ -299,7 +309,7 @@
 
 class _TypeParser {
   static final _typeTokenizationRegexp =
-      RegExp(_identifierPattern + r'|\(|\)|<|>|,|\?|\*|&');
+      RegExp(_identifierPattern + r'|\(|\)|<|>|,|\?|\*|&|{|}');
 
   static const _identifierPattern = '[_a-zA-Z][_a-zA-Z0-9]*';
 
@@ -320,7 +330,61 @@
   }
 
   Never _parseFailure(String message) {
-    fail('Error parsing type `$_typeStr` at token $_currentToken: $message');
+    throw ParseError(
+        'Error parsing type `$_typeStr` at token $_currentToken: $message');
+  }
+
+  List<NamedType> _parseRecordTypeNamedFields() {
+    assert(_currentToken == '{');
+    _next();
+    var namedTypes = <NamedType>[];
+    while (_currentToken != '}') {
+      var type = _parseType();
+      var name = _currentToken;
+      if (_identifierRegexp.matchAsPrefix(name) == null) {
+        _parseFailure('Expected an identifier');
+      }
+      namedTypes.add(NamedType(name, type));
+      _next();
+      if (_currentToken == ',') {
+        _next();
+        continue;
+      }
+      if (_currentToken == '}') {
+        break;
+      }
+      _parseFailure('Expected `}` or `,`');
+    }
+    if (namedTypes.isEmpty) {
+      _parseFailure('Must have at least one named type between {}');
+    }
+    _next();
+    return namedTypes;
+  }
+
+  Type _parseRecordTypeRest(List<Type> positionalTypes) {
+    List<NamedType>? namedTypes;
+    while (_currentToken != ')') {
+      if (_currentToken == '{') {
+        namedTypes = _parseRecordTypeNamedFields();
+        if (_currentToken != ')') {
+          _parseFailure('Expected `)`');
+        }
+        break;
+      }
+      positionalTypes.add(_parseType());
+      if (_currentToken == ',') {
+        _next();
+        continue;
+      }
+      if (_currentToken == ')') {
+        break;
+      }
+      _parseFailure('Expected `)` or `,`');
+    }
+    _next();
+    return RecordType(
+        positional: positionalTypes, named: namedTypes ?? const []);
   }
 
   Type? _parseSuffix(Type type) {
@@ -364,6 +428,13 @@
     //   unsuffixedType := identifier typeArgs?
     //                   | `?`
     //                   | `(` type `)`
+    //                   | `(` recordTypeFields `,` recordTypeNamedFields `)`
+    //                   | `(` recordTypeFields `,`? `)`
+    //                   | `(` recordTypeNamedFields? `)`
+    //   recordTypeFields := type (`,` type)*
+    //   recordTypeNamedFields := `{` recordTypeNamedField
+    //                            (`,` recordTypeNamedField)* `,`? `}`
+    //   recordTypeNamedField := type identifier
     //   typeArgs := `<` type (`,` type)* `>`
     //   nullability := (`?` | `*`)?
     //   suffix := `Function` `(` type (`,` type)* `)`
@@ -387,9 +458,16 @@
     }
     if (_currentToken == '(') {
       _next();
+      if (_currentToken == ')' || _currentToken == '{') {
+        return _parseRecordTypeRest([]);
+      }
       var type = _parseType();
+      if (_currentToken == ',') {
+        _next();
+        return _parseRecordTypeRest([type]);
+      }
       if (_currentToken != ')') {
-        _parseFailure('Expected `)`');
+        _parseFailure('Expected `)` or `,`');
       }
       _next();
       return type;
@@ -422,7 +500,7 @@
     var parser = _TypeParser._(typeStr, _tokenizeTypeStr(typeStr));
     var result = parser._parseType();
     if (parser._currentToken != '<END>') {
-      fail('Extra tokens after parsing type `$typeStr`: '
+      throw ParseError('Extra tokens after parsing type `$typeStr`: '
           '${parser._tokens.sublist(parser._i, parser._tokens.length - 1)}');
     }
     return result;
@@ -434,14 +512,16 @@
     for (var match in _typeTokenizationRegexp.allMatches(typeStr)) {
       var extraChars = typeStr.substring(lastMatchEnd, match.start).trim();
       if (extraChars.isNotEmpty) {
-        fail('Unrecognized character(s) in type `$typeStr`: $extraChars');
+        throw ParseError(
+            'Unrecognized character(s) in type `$typeStr`: $extraChars');
       }
       result.add(typeStr.substring(match.start, match.end));
       lastMatchEnd = match.end;
     }
     var extraChars = typeStr.substring(lastMatchEnd).trim();
     if (extraChars.isNotEmpty) {
-      fail('Unrecognized character(s) in type `$typeStr`: $extraChars');
+      throw ParseError(
+          'Unrecognized character(s) in type `$typeStr`: $extraChars');
     }
     result.add('<END>');
     return result;
diff --git a/pkg/_fe_analyzer_shared/test/mini_types_test.dart b/pkg/_fe_analyzer_shared/test/mini_types_test.dart
index fe978f3..f9382af 100644
--- a/pkg/_fe_analyzer_shared/test/mini_types_test.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_types_test.dart
@@ -7,6 +7,206 @@
 import 'mini_types.dart';
 
 main() {
+  group('parse', () {
+    var throwsParseError = throwsA(TypeMatcher<ParseError>());
+
+    group('non-function type:', () {
+      test('no type args', () {
+        var t = Type('int') as NonFunctionType;
+        expect(t.name, 'int');
+        expect(t.args, isEmpty);
+      });
+
+      test('type arg', () {
+        var t = Type('List<int>') as NonFunctionType;
+        expect(t.name, 'List');
+        expect(t.args, hasLength(1));
+        expect(t.args[0].type, 'int');
+      });
+
+      test('type args', () {
+        var t = Type('Map<int, String>') as NonFunctionType;
+        expect(t.name, 'Map');
+        expect(t.args, hasLength(2));
+        expect(t.args[0].type, 'int');
+        expect(t.args[1].type, 'String');
+      });
+
+      test('invalid type arg separator', () {
+        expect(() => Type('Map<int) String>'), throwsParseError);
+      });
+    });
+
+    test('invalid initial token', () {
+      expect(() => Type('<'), throwsParseError);
+    });
+
+    test('unknown type', () {
+      var t = Type('?');
+      expect(t, TypeMatcher<UnknownType>());
+    });
+
+    test('question type', () {
+      var t = Type('int?') as QuestionType;
+      expect(t.innerType.type, 'int');
+    });
+
+    test('star type', () {
+      var t = Type('int*') as StarType;
+      expect(t.innerType.type, 'int');
+    });
+
+    test('promoted type variable', () {
+      var t = Type('T&int') as PromotedTypeVariableType;
+      expect(t.innerType.type, 'T');
+      expect(t.promotion.type, 'int');
+    });
+
+    test('parenthesized type', () {
+      var t = Type('(int)');
+      expect(t.type, 'int');
+    });
+
+    test('invalid token terminating parenthesized type', () {
+      expect(() => Type('(?<'), throwsParseError);
+    });
+
+    group('function type:', () {
+      test('no parameters', () {
+        var t = Type('int Function()') as FunctionType;
+        expect(t.returnType.type, 'int');
+        expect(t.positionalParameters, isEmpty);
+      });
+
+      test('positional parameter', () {
+        var t = Type('int Function(String)') as FunctionType;
+        expect(t.returnType.type, 'int');
+        expect(t.positionalParameters, hasLength(1));
+        expect(t.positionalParameters[0].type, 'String');
+      });
+
+      test('positional parameters', () {
+        var t = Type('int Function(String, double)') as FunctionType;
+        expect(t.returnType.type, 'int');
+        expect(t.positionalParameters, hasLength(2));
+        expect(t.positionalParameters[0].type, 'String');
+        expect(t.positionalParameters[1].type, 'double');
+      });
+
+      test('invalid parameter separator', () {
+        expect(() => Type('int Function(String Function()< double)'),
+            throwsParseError);
+      });
+
+      test('invalid token after Function', () {
+        expect(() => Type('int Function&)'), throwsParseError);
+      });
+    });
+
+    group('record type:', () {
+      test('no fields', () {
+        var t = Type('()') as RecordType;
+        expect(t.positional, isEmpty);
+        expect(t.named, isEmpty);
+      });
+
+      test('named field', () {
+        var t = Type('({int x})') as RecordType;
+        expect(t.positional, isEmpty);
+        expect(t.named, hasLength(1));
+        expect(t.named[0].name, 'x');
+        expect(t.named[0].type.type, 'int');
+      });
+
+      test('named field followed by comma', () {
+        var t = Type('({int x,})') as RecordType;
+        expect(t.positional, isEmpty);
+        expect(t.named, hasLength(1));
+        expect(t.named[0].name, 'x');
+        expect(t.named[0].type.type, 'int');
+      });
+
+      test('named field followed by invalid token', () {
+        expect(() => Type('({int x))'), throwsParseError);
+      });
+
+      test('named field name is not an identifier', () {
+        expect(() => Type('({int )})'), throwsParseError);
+      });
+
+      test('named fields', () {
+        var t = Type('({int x, String y})') as RecordType;
+        expect(t.positional, isEmpty);
+        expect(t.named, hasLength(2));
+        expect(t.named[0].name, 'x');
+        expect(t.named[0].type.type, 'int');
+        expect(t.named[1].name, 'y');
+        expect(t.named[1].type.type, 'String');
+      });
+
+      test('curly braces followed by invalid token', () {
+        expect(() => Type('({int x}&'), throwsParseError);
+      });
+
+      test('curly braces but no named fields', () {
+        expect(() => Type('({})'), throwsParseError);
+      });
+
+      test('positional field', () {
+        var t = Type('(int,)') as RecordType;
+        expect(t.named, isEmpty);
+        expect(t.positional, hasLength(1));
+        expect(t.positional[0].type, 'int');
+      });
+
+      group('positional fields:', () {
+        test('two', () {
+          var t = Type('(int, String)') as RecordType;
+          expect(t.named, isEmpty);
+          expect(t.positional, hasLength(2));
+          expect(t.positional[0].type, 'int');
+          expect(t.positional[1].type, 'String');
+        });
+
+        test('three', () {
+          var t = Type('(int, String, double)') as RecordType;
+          expect(t.named, isEmpty);
+          expect(t.positional, hasLength(3));
+          expect(t.positional[0].type, 'int');
+          expect(t.positional[1].type, 'String');
+          expect(t.positional[2].type, 'double');
+        });
+      });
+
+      test('named and positional fields', () {
+        var t = Type('(int, {String x})') as RecordType;
+        expect(t.positional, hasLength(1));
+        expect(t.positional[0].type, 'int');
+        expect(t.named, hasLength(1));
+        expect(t.named[0].name, 'x');
+        expect(t.named[0].type.type, 'String');
+      });
+
+      test('terminated by invalid token', () {
+        expect(() => Type('(int, String('), throwsParseError);
+      });
+    });
+
+    group('invalid token:', () {
+      test('before other tokens', () {
+        expect(() => Type('#int'), throwsParseError);
+      });
+
+      test('at end', () {
+        expect(() => Type('int#'), throwsParseError);
+      });
+    });
+
+    test('extra token after type', () {
+      expect(() => Type('int)'), throwsParseError);
+    });
+  });
+
   group('recursivelyDemote:', () {
     group('FunctionType:', () {
       group('return type:', () {
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 7023677..38aac9c 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
@@ -1733,12 +1733,7 @@
                       pattern: Var('b').pattern(),
                     ),
                   ]),
-                  expr2(
-                    RecordType(
-                      positional: [Type('int'), Type('String')],
-                      named: [],
-                    ),
-                  ).checkContext('(int, ?)'),
+                  expr('(int, String)').checkContext('(int, ?)'),
                 ).checkIr(
                     'match(expr((int, String)), recordPattern(varPattern(a, '
                     'matchedType: int, staticType: int), varPattern(b, '
@@ -1765,12 +1760,7 @@
                     ),
                   ])
                     ..errorId = 'PATTERN',
-                  expr2(
-                    RecordType(
-                      positional: [Type('int')],
-                      named: [],
-                    ),
-                  ).checkContext('(int, ?)'),
+                  expr('(int,)').checkContext('(int, ?)'),
                 )..errorId = 'CONTEXT')
                     .checkIr('match(expr((int)), recordPattern(varPattern(a, '
                         'matchedType: Object?, staticType: int), '
@@ -1790,12 +1780,7 @@
               test('too few', () {
                 h.run([
                   ifCase(
-                    expr2(
-                      RecordType(
-                        positional: [Type('int')],
-                        named: [],
-                      ),
-                    ).checkContext('?'),
+                    expr('(int,)').checkContext('?'),
                     recordPattern([
                       RecordPatternField(
                         name: null,
@@ -1817,12 +1802,7 @@
               test('too many', () {
                 h.run([
                   ifCase(
-                    expr2(
-                      RecordType(
-                        positional: [Type('int'), Type('String')],
-                        named: [],
-                      ),
-                    ).checkContext('?'),
+                    expr('(int, String)').checkContext('?'),
                     recordPattern([
                       RecordPatternField(
                         name: null,
@@ -1909,15 +1889,7 @@
                       pattern: Var('b').pattern(),
                     ),
                   ]),
-                  expr2(
-                    RecordType(
-                      positional: [],
-                      named: [
-                        NamedType('a', Type('int')),
-                        NamedType('b', Type('String')),
-                      ],
-                    ),
-                  ).checkContext('({int a, ? b})'),
+                  expr('({int a, String b})').checkContext('({int a, ? b})'),
                 ).checkIr('match(expr(({int a, String b})), '
                     'recordPattern(varPattern(a, matchedType: int, '
                     'staticType: int), varPattern(b, matchedType: String, '
@@ -1943,12 +1915,7 @@
                     ),
                   ])
                     ..errorId = 'PATTERN',
-                  expr2(
-                    RecordType(
-                      positional: [],
-                      named: [NamedType('a', Type('int'))],
-                    ),
-                  ).checkContext('({int a, ? b})'),
+                  expr('({int a})').checkContext('({int a, ? b})'),
                 )..errorId = 'CONTEXT')
                     .checkIr('match(expr(({int a})), '
                         'recordPattern(varPattern(a, matchedType: Object?, '
@@ -1968,14 +1935,7 @@
               test('too few', () {
                 h.run([
                   ifCase(
-                    expr2(
-                      RecordType(
-                        positional: [],
-                        named: [
-                          NamedType('a', Type('int')),
-                        ],
-                      ),
-                    ).checkContext('?'),
+                    expr('({int a})').checkContext('?'),
                     recordPattern([
                       RecordPatternField(
                         name: 'a',
@@ -1998,15 +1958,7 @@
               test('too many', () {
                 h.run([
                   ifCase(
-                    expr2(
-                      RecordType(
-                        positional: [],
-                        named: [
-                          NamedType('a', Type('int')),
-                          NamedType('b', Type('String')),
-                        ],
-                      ),
-                    ).checkContext('?'),
+                    expr('({int a, String b})').checkContext('?'),
                     recordPattern([
                       RecordPatternField(
                         name: 'a',