Skip to content

Commit

Permalink
Add record type parsing to mini_types.dart.
Browse files Browse the repository at this point in the history
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 <[email protected]>
Commit-Queue: Paul Berry <[email protected]>
  • Loading branch information
stereotype441 authored and Commit Queue committed Nov 3, 2022
1 parent 024a460 commit 402a05d
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 67 deletions.
5 changes: 0 additions & 5 deletions pkg/_fe_analyzer_shared/test/mini_ast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,6 @@ Statement do_(List<Statement> body, Expression condition) {
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].
Expand Down
92 changes: 86 additions & 6 deletions pkg/_fe_analyzer_shared/test/mini_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ class NonFunctionType extends Type {
}
}

/// 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
Expand Down Expand Up @@ -299,7 +309,7 @@ class UnknownType extends Type {

class _TypeParser {
static final _typeTokenizationRegexp =
RegExp(_identifierPattern + r'|\(|\)|<|>|,|\?|\*|&');
RegExp(_identifierPattern + r'|\(|\)|<|>|,|\?|\*|&|{|}');

static const _identifierPattern = '[_a-zA-Z][_a-zA-Z0-9]*';

Expand All @@ -320,7 +330,61 @@ class _TypeParser {
}

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) {
Expand Down Expand Up @@ -364,6 +428,13 @@ class _TypeParser {
// 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)* `)`
Expand All @@ -387,9 +458,16 @@ class _TypeParser {
}
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;
Expand Down Expand Up @@ -422,7 +500,7 @@ class _TypeParser {
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;
Expand All @@ -434,14 +512,16 @@ class _TypeParser {
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;
Expand Down
200 changes: 200 additions & 0 deletions pkg/_fe_analyzer_shared/test/mini_types_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,206 @@ import 'package:test/test.dart';
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:', () {
Expand Down
Loading

0 comments on commit 402a05d

Please sign in to comment.