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',