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) =>