Fix recovery when parsing complex type variables
This fixes parser recovery when the user has inserted a word before
a method declaration with a complex type arguments, such as
foo Future<List<int>> bar() {}
Fix https://github.com/dart-lang/sdk/issues/34850
Change-Id: Ic6d1767668bd34d75c6ab5f9973e5762bdd8f160
Reviewed-on: https://dart-review.googlesource.com/c/80780
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Dan Rubel <danrubel@google.com>
diff --git a/pkg/analyzer/test/generated/parser_fasta_test.dart b/pkg/analyzer/test/generated/parser_fasta_test.dart
index 1b44cb5..dce0e04 100644
--- a/pkg/analyzer/test/generated/parser_fasta_test.dart
+++ b/pkg/analyzer/test/generated/parser_fasta_test.dart
@@ -927,7 +927,56 @@
*/
@reflectiveTest
class StatementParserTest_Fasta extends FastaParserTestCase
- with StatementParserTestMixin {}
+ with StatementParserTestMixin {
+ void test_invalid_typeArg_34850() {
+ var unit = parseCompilationUnit('foo Future<List<int>> bar() {}', errors: [
+ expectedError(ParserErrorCode.EXPECTED_TOKEN, 11, 4),
+ expectedError(ParserErrorCode.MISSING_FUNCTION_PARAMETERS, 4, 6),
+ expectedError(ParserErrorCode.MISSING_FUNCTION_BODY, 22, 3),
+ ]);
+ // Validate that recovery has properly updated the token stream.
+ analyzer.Token token = unit.beginToken;
+ while (!token.isEof) {
+ expect(token.type, isNot(TokenType.GT_GT));
+ analyzer.Token next = token.next;
+ expect(next.previous, token);
+ token = next;
+ }
+ }
+
+ void test_partial_typeArg1_34850() {
+ var unit = parseCompilationUnit('<bar<', errors: [
+ expectedError(ParserErrorCode.EXPECTED_EXECUTABLE, 0, 1),
+ expectedError(ParserErrorCode.MISSING_IDENTIFIER, 5, 0),
+ expectedError(ParserErrorCode.MISSING_FUNCTION_PARAMETERS, 1, 3),
+ expectedError(ParserErrorCode.MISSING_FUNCTION_BODY, 5, 0),
+ ]);
+ // Validate that recovery has properly updated the token stream.
+ analyzer.Token token = unit.beginToken;
+ while (!token.isEof) {
+ expect(token.type, isNot(TokenType.GT_GT));
+ analyzer.Token next = token.next;
+ expect(next.previous, token);
+ token = next;
+ }
+ }
+
+ void test_partial_typeArg2_34850() {
+ var unit = parseCompilationUnit('foo <bar<', errors: [
+ expectedError(ParserErrorCode.EXPECTED_TOKEN, 5, 3),
+ expectedError(ParserErrorCode.MISSING_FUNCTION_PARAMETERS, 0, 3),
+ expectedError(ParserErrorCode.MISSING_FUNCTION_BODY, 9, 0),
+ ]);
+ // Validate that recovery has properly updated the token stream.
+ analyzer.Token token = unit.beginToken;
+ while (!token.isEof) {
+ expect(token.type, isNot(TokenType.GT_GT));
+ analyzer.Token next = token.next;
+ expect(next.previous, token);
+ token = next;
+ }
+ }
+}
/**
* Tests of the fasta parser based on [TopLevelParserTestMixin].
diff --git a/pkg/analyzer/test/generated/parser_test.dart b/pkg/analyzer/test/generated/parser_test.dart
index a2be3ff..b46d1b7 100644
--- a/pkg/analyzer/test/generated/parser_test.dart
+++ b/pkg/analyzer/test/generated/parser_test.dart
@@ -4215,15 +4215,11 @@
}
void test_method_invalidTypeParameters() {
- // TODO(jmesserly): ideally we'd be better at parser recovery here.
- // It doesn't try to advance past the invalid token `!` to find the
- // valid `>`. If it did we'd get less cascading errors, at least for this
- // particular example.
createParser('void m<E, hello!>() {}');
ClassMember member = parser.parseClassMember('C');
expectNotNullIfNoErrors(member);
listener.assertErrors(usingFastaParser
- ? [expectedError(ParserErrorCode.UNEXPECTED_TOKEN, 15, 1)]
+ ? [expectedError(ParserErrorCode.EXPECTED_TOKEN, 10, 5)]
: [
expectedError(ParserErrorCode.EXPECTED_TOKEN, 0, 0) /*>*/,
expectedError(ParserErrorCode.MISSING_IDENTIFIER, 0, 0),
@@ -6576,7 +6572,7 @@
void test_parseFunctionExpression_functionInPlaceOfTypeName() {
Expression expression = parseExpression('<test(' ', (){});>[0, 1, 2]',
codes: usingFastaParser
- ? [ParserErrorCode.UNEXPECTED_TOKEN]
+ ? [ParserErrorCode.EXPECTED_TOKEN]
: [
ParserErrorCode.EXPECTED_TOKEN,
ParserErrorCode.MISSING_IDENTIFIER,
@@ -10632,7 +10628,10 @@
result[new Symbol(name)] = value;
});
return result;
-}''', errors: [expectedError(ParserErrorCode.EXPECTED_TOKEN, 12, 24)]);
+}''', errors: [
+ expectedError(ParserErrorCode.EXPECTED_TOKEN, 12, 24),
+ expectedError(ParserErrorCode.MISSING_FUNCTION_PARAMETERS, 0, 3)
+ ]);
}
}
diff --git a/pkg/analyzer/test/src/dart/analysis/unlinked_api_signature_test.dart b/pkg/analyzer/test/src/dart/analysis/unlinked_api_signature_test.dart
index 25fa645..2c9d8e1 100644
--- a/pkg/analyzer/test/src/dart/analysis/unlinked_api_signature_test.dart
+++ b/pkg/analyzer/test/src/dart/analysis/unlinked_api_signature_test.dart
@@ -758,7 +758,7 @@
Future<List<int>> bar() {}
''', r'''
foo
-Future<List<int>> bar() async {}
+Future<List<int>> bar(int x) {}
''');
}
diff --git a/pkg/analyzer/test/src/fasta/recovery/invalid_code_test.dart b/pkg/analyzer/test/src/fasta/recovery/invalid_code_test.dart
index 107fd21..b53afa1 100644
--- a/pkg/analyzer/test/src/fasta/recovery/invalid_code_test.dart
+++ b/pkg/analyzer/test/src/fasta/recovery/invalid_code_test.dart
@@ -40,7 +40,7 @@
f() {
return <g('')>[0, 1, 2];
}
-''', [ParserErrorCode.UNEXPECTED_TOKEN], '''
+''', [ParserErrorCode.EXPECTED_TOKEN], '''
f() {
return <g>[0, 1, 2];
}
@@ -53,7 +53,7 @@
f() {
return <test('', (){})>[0, 1, 2];
}
-''', [ParserErrorCode.UNEXPECTED_TOKEN], '''
+''', [ParserErrorCode.EXPECTED_TOKEN], '''
f() {
return <test>[0, 1, 2];
}
diff --git a/pkg/analyzer/test/src/fasta/recovery/paired_tokens_test.dart b/pkg/analyzer/test/src/fasta/recovery/paired_tokens_test.dart
index edc6d29..50e04a7 100644
--- a/pkg/analyzer/test/src/fasta/recovery/paired_tokens_test.dart
+++ b/pkg/analyzer/test/src/fasta/recovery/paired_tokens_test.dart
@@ -41,7 +41,10 @@
void test_typeParameters_funct() {
testRecovery('''
f<T extends Function()() => null;
-''', [ParserErrorCode.EXPECTED_TOKEN], '''
+''', [
+ ParserErrorCode.EXPECTED_TOKEN,
+ ParserErrorCode.MISSING_FUNCTION_PARAMETERS
+ ], '''
f<T extends Function()>() => null;
''');
}
@@ -49,7 +52,10 @@
void test_typeParameters_funct2() {
testRecovery('''
f<T extends Function<X>()() => null;
-''', [ParserErrorCode.EXPECTED_TOKEN], '''
+''', [
+ ParserErrorCode.EXPECTED_TOKEN,
+ ParserErrorCode.MISSING_FUNCTION_PARAMETERS
+ ], '''
f<T extends Function<X>()>() => null;
''');
}
@@ -133,7 +139,10 @@
void test_typeParameters_last() {
testRecovery('''
f<T() => null;
-''', [ParserErrorCode.EXPECTED_TOKEN], '''
+''', [
+ ParserErrorCode.EXPECTED_TOKEN,
+ ParserErrorCode.MISSING_FUNCTION_PARAMETERS
+ ], '''
f<T>() => null;
''');
}
@@ -141,7 +150,10 @@
void test_typeParameters_outer_last() {
testRecovery('''
f<T extends List<int>() => null;
-''', [ParserErrorCode.EXPECTED_TOKEN], '''
+''', [
+ ParserErrorCode.EXPECTED_TOKEN,
+ ParserErrorCode.MISSING_FUNCTION_PARAMETERS
+ ], '''
f<T extends List<int>>() => null;
''');
}
diff --git a/pkg/front_end/lib/src/fasta/parser/type_info_impl.dart b/pkg/front_end/lib/src/fasta/parser/type_info_impl.dart
index e19d8e3..e76bcff 100644
--- a/pkg/front_end/lib/src/fasta/parser/type_info_impl.dart
+++ b/pkg/front_end/lib/src/fasta/parser/type_info_impl.dart
@@ -12,6 +12,8 @@
import '../util/link.dart' show Link;
+import 'forwarding_listener.dart' show ForwardingListener;
+
import 'identifier_context.dart' show IdentifierContext;
import 'member_kind.dart' show MemberKind;
@@ -662,9 +664,11 @@
/// given unbalanced `<` `>` and invalid parameters or arguments.
final bool inDeclaration;
- /// The token before the end group token (e.g. `>`, `>>`, `>=`, or `>>=`)
- /// or after which a synthetic end group token should be inserted.
- Token beforeEnd;
+ /// The `>` token which ends the type parameter or argument.
+ /// This closer may be synthetic, points to the next token in the stream,
+ /// is only used when skipping over the type parameters or arguments,
+ /// and may not be part of the token stream.
+ Token skipEnd;
ComplexTypeParamOrArgInfo(Token token, this.inDeclaration)
: assert(optional('<', token.next)),
@@ -704,8 +708,8 @@
next = token.next;
}
if (!optional(',', next)) {
- if (isCloser(next)) {
- beforeEnd = token;
+ skipEnd = splitCloser(next);
+ if (skipEnd != null) {
return this;
}
if (!inDeclaration) {
@@ -722,18 +726,18 @@
}
// Recovery
- beforeEnd = token;
- if (!isCloser(next)) {
+ skipEnd = splitCloser(next);
+ if (skipEnd == null) {
if (optional('(', next)) {
token = next.endGroup;
next = token.next;
}
- if (!isCloser(next)) {
- token = next;
- next = token.next;
+ skipEnd = splitCloser(next);
+ if (skipEnd == null) {
+ skipEnd = splitCloser(next.next);
}
- if (isCloser(next)) {
- beforeEnd = token;
+ if (skipEnd == null) {
+ skipEnd = syntheticGt(next);
}
}
return this;
@@ -762,20 +766,19 @@
++count;
if (!optional(',', next)) {
if (parseCloser(token)) {
- beforeEnd = token;
break;
}
// Recovery
if (!looksLikeTypeParamOrArg(inDeclaration, next)) {
- parseUnexpectedEnd(token, parser);
+ token = parseUnexpectedEnd(token, true, parser);
break;
}
// Missing comma. Report error, insert comma, and continue looping.
next = parseMissingComma(token, parser);
}
}
- Token endGroup = beforeEnd.next;
+ Token endGroup = token.next;
parser.listener.endTypeArguments(count, start, endGroup);
return endGroup;
}
@@ -795,9 +798,6 @@
token = parser.parseMetadataStar(next);
next = parser.ensureIdentifier(
token, IdentifierContext.typeVariableDeclaration);
- if (beforeEnd == token) {
- beforeEnd = next;
- }
token = next;
listener.beginTypeVariable(token);
typeStarts = typeStarts.prepend(token);
@@ -857,12 +857,10 @@
superTypeInfos = superTypeInfos.tail;
}
- if (parseCloser(token)) {
- beforeEnd = token;
- } else {
- parseUnexpectedEnd(token, parser);
+ if (!parseCloser(token)) {
+ token = parseUnexpectedEnd(token, false, parser);
}
- Token endGroup = beforeEnd.next;
+ Token endGroup = token.next;
listener.endTypeVariables(start, endGroup);
return endGroup;
}
@@ -875,43 +873,109 @@
token, new SyntheticToken(TokenType.COMMA, next.charOffset));
}
- void parseUnexpectedEnd(Token token, Parser parser) {
- if (beforeEnd.isSynthetic && beforeEnd.charOffset == token.charOffset) {
- // Ensure that beforeEnd is in the token stream
- // as a nested type argument or parameter may have inserted
- // a synthetic closer.
- beforeEnd = token;
- }
- if (parseCloser(beforeEnd)) {
- parser.reportRecoverableErrorWithToken(
- token.next, fasta.templateUnexpectedToken);
- } else {
- // If token is synthetic, then an error has already been reported.
- if (!token.isSynthetic) {
+ Token parseUnexpectedEnd(Token token, bool isArguments, Parser parser) {
+ Token next = token.next;
+ bool errorReported = token.isSynthetic || (next.isSynthetic && !next.isEof);
+
+ bool typeFollowsExtends = false;
+ if (optional('extends', next)) {
+ if (!errorReported) {
parser.reportRecoverableError(
token, fasta.templateExpectedAfterButGot.withArguments('>'));
+ errorReported = true;
}
- Token next = beforeEnd.next;
- Token endGroup = syntheticGt(next);
- endGroup.setNext(next);
- beforeEnd.setNext(endGroup);
+ token = next;
+ next = token.next;
+ typeFollowsExtends = isValidTypeReference(next);
+
+ if (parseCloser(token)) {
+ return token;
+ }
}
+
+ if (typeFollowsExtends ||
+ optional('dynamic', next) ||
+ optional('void', next) ||
+ optional('Function', next)) {
+ TypeInfo invalidType = computeType(token, true);
+ if (invalidType != noType) {
+ if (!errorReported) {
+ parser.reportRecoverableError(
+ token, fasta.templateExpectedAfterButGot.withArguments('>'));
+ errorReported = true;
+ }
+
+ // Parse the type so that the token stream is properly modified,
+ // but ensure that parser events are ignored by replacing the listener.
+ final originalListener = parser.listener;
+ parser.listener = new ForwardingListener();
+ token = invalidType.parseType(token, parser);
+ next = token.next;
+ parser.listener = originalListener;
+
+ if (parseCloser(token)) {
+ return token;
+ }
+ }
+ }
+
+ TypeParamOrArgInfo invalidTypeVar =
+ computeTypeParamOrArg(token, inDeclaration);
+ if (invalidTypeVar != noTypeParamOrArg) {
+ if (!errorReported) {
+ parser.reportRecoverableError(
+ token, fasta.templateExpectedAfterButGot.withArguments('>'));
+ errorReported = true;
+ }
+
+ // Parse the type so that the token stream is properly modified,
+ // but ensure that parser events are ignored by replacing the listener.
+ final originalListener = parser.listener;
+ parser.listener = new ForwardingListener();
+ token = isArguments
+ ? invalidTypeVar.parseArguments(token, parser)
+ : invalidTypeVar.parseVariables(token, parser);
+ next = token.next;
+ parser.listener = originalListener;
+
+ if (parseCloser(token)) {
+ return token;
+ }
+ }
+
+ if (optional('(', next) && next.endGroup != null) {
+ if (!errorReported) {
+ // Only report an error if one has not already been reported.
+ parser.reportRecoverableError(
+ token, fasta.templateExpectedAfterButGot.withArguments('>'));
+ errorReported = true;
+ }
+ token = next.endGroup;
+ next = token.next;
+
+ if (parseCloser(token)) {
+ return token;
+ }
+ }
+
+ if (!errorReported) {
+ // Only report an error if one has not already been reported.
+ parser.reportRecoverableError(
+ token, fasta.templateExpectedAfterButGot.withArguments('>'));
+ }
+ if (parseCloser(next)) {
+ return next;
+ }
+ Token endGroup = syntheticGt(next);
+ endGroup.setNext(next);
+ token.setNext(endGroup);
+ return token;
}
@override
Token skip(Token token) {
- final next = beforeEnd.next;
- final value = next.stringValue;
- if (identical(value, '>')) {
- return next;
- } else if (identical(value, '>>')) {
- return splitGtGt(next);
- } else if (identical(value, '>=')) {
- return splitGtEq(next);
- } else if (identical(value, '>>=')) {
- return splitGtFromGtGtEq(next);
- }
- return syntheticGt(next);
+ assert(skipEnd != null);
+ return skipEnd;
}
}
@@ -924,25 +988,35 @@
identical(value, '>>=');
}
-/// If [token] is one of `>`, `>>`, `>=', or `>>=`,
+/// If [beforeCloser].next is one of `>`, `>>`, `>=', or `>>=`,
/// then update the token stream and return `true`.
bool parseCloser(Token beforeCloser) {
- Token closer = beforeCloser.next;
- String value = closer.stringValue;
- if (identical(value, '>')) {
+ Token unsplit = beforeCloser.next;
+ Token split = splitCloser(unsplit);
+ if (split == unsplit) {
return true;
- }
- Token split;
- if (identical(value, '>>')) {
- split = splitGtGt(closer);
- } else if (identical(value, '>=')) {
- split = splitGtEq(closer);
- } else if (identical(value, '>>=')) {
- split = splitGtFromGtGtEq(closer);
- } else {
+ } else if (split == null) {
return false;
}
- split.next.setNext(closer.next);
+ split.next.setNext(unsplit.next);
beforeCloser.setNext(split);
return true;
}
+
+/// If [closer] is `>` then return it.
+/// If [closer] is one of `>>`, `>=', or `>>=` then split then token
+/// and return the leading `>` without updating the token stream.
+/// If [closer] is none of the above, then return null;
+Token splitCloser(Token closer) {
+ String value = closer.stringValue;
+ if (identical(value, '>')) {
+ return closer;
+ } else if (identical(value, '>>')) {
+ return splitGtGt(closer);
+ } else if (identical(value, '>=')) {
+ return splitGtEq(closer);
+ } else if (identical(value, '>>=')) {
+ return splitGtFromGtGtEq(closer);
+ }
+ return null;
+}
diff --git a/pkg/front_end/test/fasta/parser/type_info_test.dart b/pkg/front_end/test/fasta/parser/type_info_test.dart
index 90db70b..d50b840 100644
--- a/pkg/front_end/test/fasta/parser/type_info_test.dart
+++ b/pkg/front_end/test/fasta/parser/type_info_test.dart
@@ -1281,7 +1281,7 @@
void test_computeTypeArg_complex_recovery() {
expectComplexTypeArg('<S extends T>', expectedErrors: [
- error(codeUnexpectedToken, 3, 7)
+ error(codeExpectedAfterButGot, 1, 1)
], expectedCalls: [
'beginTypeArguments <',
'handleIdentifier S typeReference',
@@ -1290,7 +1290,7 @@
'endTypeArguments 1 < >',
]);
expectComplexTypeArg('<S extends List<T>>', expectedErrors: [
- error(codeUnexpectedToken, 3, 7)
+ error(codeExpectedAfterButGot, 1, 1)
], expectedCalls: [
'beginTypeArguments <',
'handleIdentifier S typeReference',
@@ -1524,7 +1524,7 @@
void test_computeTypeParam_complex_recovery() {
expectComplexTypeParam('<S Function()>', expectedErrors: [
- error(codeUnexpectedToken, 3, 8),
+ error(codeExpectedAfterButGot, 1, 1),
], expectedCalls: [
'beginTypeVariables <',
'beginMetadataStar S',
@@ -1538,7 +1538,6 @@
]);
expectComplexTypeParam('<void Function()>', expectedErrors: [
error(codeExpectedIdentifier, 1, 4),
- error(codeUnexpectedToken, 1, 4),
], expectedCalls: [
'beginTypeVariables <',
'beginMetadataStar void',
@@ -1551,7 +1550,7 @@
'endTypeVariables < >',
]);
expectComplexTypeParam('<S<T>>', expectedErrors: [
- error(codeUnexpectedToken, 2, 1),
+ error(codeExpectedAfterButGot, 1, 1),
], expectedCalls: [
'beginTypeVariables <',
'beginMetadataStar S',
@@ -1687,6 +1686,39 @@
'endTypeVariables < >'
]);
}
+
+ void test_computeTypeParam_34850() {
+ expectComplexTypeParam('<S<T>> A', expectedAfter: 'A', expectedErrors: [
+ error(codeExpectedAfterButGot, 1, 1),
+ ], expectedCalls: [
+ 'beginTypeVariables <',
+ 'beginMetadataStar S',
+ 'endMetadataStar 0',
+ 'handleIdentifier S typeVariableDeclaration',
+ 'beginTypeVariable S',
+ 'handleTypeVariablesDefined S 1',
+ 'handleNoType S',
+ 'endTypeVariable < 0 null',
+ 'endTypeVariables < >',
+ ]);
+ expectComplexTypeParam('<S();> A',
+ inDeclaration: true,
+ expectedAfter: 'A',
+ expectedErrors: [
+ error(codeExpectedAfterButGot, 1, 1),
+ ],
+ expectedCalls: [
+ 'beginTypeVariables <',
+ 'beginMetadataStar S',
+ 'endMetadataStar 0',
+ 'handleIdentifier S typeVariableDeclaration',
+ 'beginTypeVariable S',
+ 'handleTypeVariablesDefined S 1',
+ 'handleNoType S',
+ 'endTypeVariable ( 0 null',
+ 'endTypeVariables < >',
+ ]);
+ }
}
void expectInfo(expectedInfo, String source, {bool required}) {
@@ -1816,7 +1848,8 @@
reason: 'TypeParamOrArgInfo.skipType'
' should not modify the token stream');
- TypeInfoListener listener = new TypeInfoListener(metadataAllowed: true);
+ TypeInfoListener listener =
+ new TypeInfoListener(firstToken: start, metadataAllowed: true);
Parser parser = new Parser(listener);
Token actualEnd = typeVarInfo.parseVariables(start, parser);
validateTokens(start);
@@ -1851,7 +1884,9 @@
void expectEnd(String tokenAfter, Token end) {
if (tokenAfter == null) {
expect(end.isEof, isFalse);
- expect(end.next.isEof, isTrue);
+ if (!end.next.isEof) {
+ fail('Expected EOF after $end but found ${end.next}');
+ }
} else {
expect(end.next.lexeme, tokenAfter);
}
@@ -1885,7 +1920,7 @@
while (!token.isEof) {
Token next = token.next;
expect(token.charOffset, lessThanOrEqualTo(next.charOffset));
- expect(next.previous, token);
+ expect(next.previous, token, reason: next.type.toString());
if (next is SyntheticToken) {
expect(next.beforeSynthetic, token);
}
@@ -1899,8 +1934,13 @@
final bool metadataAllowed;
List<String> calls = <String>[];
List<ExpectedError> errors;
+ Token firstToken;
- TypeInfoListener({this.metadataAllowed: false});
+ TypeInfoListener({this.firstToken, this.metadataAllowed: false}) {
+ if (firstToken != null && firstToken.isEof) {
+ firstToken = firstToken.next;
+ }
+ }
@override
void beginArguments(Token token) {
@@ -2000,16 +2040,22 @@
@override
void endTypeArguments(int count, Token beginToken, Token endToken) {
calls.add('endTypeArguments $count $beginToken $endToken');
+ assertTokenInStream(beginToken);
+ assertTokenInStream(endToken);
}
@override
void endTypeVariable(Token token, int index, Token extendsOrSuper) {
calls.add('endTypeVariable $token $index $extendsOrSuper');
+ assertTokenInStream(token);
+ assertTokenInStream(extendsOrSuper);
}
@override
void endTypeVariables(Token beginToken, Token endToken) {
calls.add('endTypeVariables $beginToken $endToken');
+ assertTokenInStream(beginToken);
+ assertTokenInStream(endToken);
}
@override
@@ -2082,6 +2128,25 @@
noSuchMethod(Invocation invocation) {
throw '${invocation.memberName} should not be called.';
}
+
+ assertTokenInStream(Token match) {
+ if (firstToken != null && match != null && !match.isEof) {
+ Token token = firstToken;
+ while (!token.isEof) {
+ if (identical(token, match)) {
+ return;
+ }
+ token = token.next;
+ }
+ final msg = new StringBuffer();
+ msg.writeln('Expected $match in token stream, but found');
+ while (!token.isEof) {
+ msg.write(' $token');
+ token = token.next;
+ }
+ fail(msg.toString());
+ }
+ }
}
ExpectedError error(Code code, int start, int length) =>