Revise class and mixin header recovery

This updates the fasta parser to use the "incorrect" keyword
when "extend" or "on" is used instead of "extends" in a class declaration and
when "extend" or "extends" is used instead of "on" in a mixin declaration.

Change-Id: I6bef3f897e24c3c0100180d2bf9f4d2ec7608eef
Reviewed-on: https://dart-review.googlesource.com/c/78101
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Dan Rubel <danrubel@google.com>
diff --git a/pkg/analyzer/lib/src/fasta/ast_builder.dart b/pkg/analyzer/lib/src/fasta/ast_builder.dart
index 476d0a3..23bd360 100644
--- a/pkg/analyzer/lib/src/fasta/ast_builder.dart
+++ b/pkg/analyzer/lib/src/fasta/ast_builder.dart
@@ -1771,7 +1771,10 @@
 
   @override
   void handleClassExtends(Token extendsKeyword) {
-    assert(optionalOrNull('extends', extendsKeyword));
+    assert(extendsKeyword == null ||
+        identical('extends', extendsKeyword.lexeme) ||
+        identical('on', extendsKeyword.lexeme) ||
+        extendsKeyword.lexeme == 'extend');
     debugEvent("ClassExtends");
 
     TypeName supertype = pop();
@@ -1892,7 +1895,10 @@
 
   @override
   void handleMixinOn(Token onKeyword, int typeCount) {
-    assert(optionalOrNull('on', onKeyword));
+    assert(onKeyword == null ||
+        identical('on', onKeyword.lexeme) ||
+        identical('extends', onKeyword.lexeme) ||
+        onKeyword.lexeme == 'extend');
     debugEvent("MixinOn");
 
     if (onKeyword != null) {
diff --git a/pkg/analyzer/test/src/fasta/recovery/partial_code/class_declaration_test.dart b/pkg/analyzer/test/src/fasta/recovery/partial_code/class_declaration_test.dart
index f822baf..e932751 100644
--- a/pkg/analyzer/test/src/fasta/recovery/partial_code/class_declaration_test.dart
+++ b/pkg/analyzer/test/src/fasta/recovery/partial_code/class_declaration_test.dart
@@ -34,7 +34,8 @@
                 ParserErrorCode.EXPECTED_TYPE_NAME,
                 ParserErrorCode.MISSING_CLASS_BODY
               ],
-              'class A extends _s_ {}',
+              'class A extend _s_ {}',
+              expectedErrorsInValidCode: [ParserErrorCode.EXPECTED_INSTEAD],
               failing: ['functionVoid', 'functionNonVoid', 'getter']),
           new TestDescriptor(
               'extends',
@@ -53,7 +54,8 @@
                 ParserErrorCode.EXPECTED_TYPE_NAME,
                 ParserErrorCode.MISSING_CLASS_BODY
               ],
-              'class A extends _s_ {}',
+              'class A on _s_ {}',
+              expectedErrorsInValidCode: [ParserErrorCode.EXPECTED_INSTEAD],
               failing: ['functionVoid', 'functionNonVoid', 'getter']),
           new TestDescriptor('extendsBody', 'class A extends {}',
               [ParserErrorCode.EXPECTED_TYPE_NAME], 'class A extends _s_ {}'),
diff --git a/pkg/analyzer/test/src/fasta/recovery/partial_code/mixin_declaration_test.dart b/pkg/analyzer/test/src/fasta/recovery/partial_code/mixin_declaration_test.dart
index 70198a8..33fe03f 100644
--- a/pkg/analyzer/test/src/fasta/recovery/partial_code/mixin_declaration_test.dart
+++ b/pkg/analyzer/test/src/fasta/recovery/partial_code/mixin_declaration_test.dart
@@ -43,7 +43,8 @@
                 ParserErrorCode.EXPECTED_TYPE_NAME,
                 ParserErrorCode.MISSING_CLASS_BODY
               ],
-              'mixin A on _s_ {}',
+              'mixin A extend _s_ {}',
+              expectedErrorsInValidCode: [ParserErrorCode.EXPECTED_INSTEAD],
               failing: ['functionVoid', 'functionNonVoid', 'getter']),
           new TestDescriptor(
               'extends',
@@ -53,7 +54,8 @@
                 ParserErrorCode.EXPECTED_TYPE_NAME,
                 ParserErrorCode.MISSING_CLASS_BODY
               ],
-              'mixin A on _s_ {}',
+              'mixin A extends _s_ {}',
+              expectedErrorsInValidCode: [ParserErrorCode.EXPECTED_INSTEAD],
               failing: ['functionVoid', 'functionNonVoid', 'getter']),
           new TestDescriptor('onBody', 'mixin A on {}',
               [ParserErrorCode.EXPECTED_TYPE_NAME], 'mixin A on _s_ {}'),
diff --git a/pkg/front_end/lib/src/fasta/parser/parser.dart b/pkg/front_end/lib/src/fasta/parser/parser.dart
index fd10332..2b0748d 100644
--- a/pkg/front_end/lib/src/fasta/parser/parser.dart
+++ b/pkg/front_end/lib/src/fasta/parser/parser.dart
@@ -1759,11 +1759,13 @@
           const ['extend', 'on'].contains(token.next.lexeme)) {
         reportRecoverableError(
             token.next, fasta.templateExpectedInstead.withArguments('extends'));
-        token = token.next;
-        rewriter.insertToken(token,
-            new SyntheticKeywordToken(Keyword.EXTENDS, token.next.charOffset));
+        Token incorrectExtendsKeyword = token.next;
+        token = computeType(incorrectExtendsKeyword, true)
+            .ensureTypeNotVoid(incorrectExtendsKeyword, this);
+        listener.handleClassExtends(incorrectExtendsKeyword);
+      } else {
+        token = parseClassExtendsOpt(token);
       }
-      token = parseClassExtendsOpt(token);
 
       if (recoveryListener.extendsKeyword != null) {
         if (hasExtends) {
@@ -1916,11 +1918,10 @@
           const ['extend', 'extends'].contains(token.next.lexeme)) {
         reportRecoverableError(
             token.next, fasta.templateExpectedInstead.withArguments('on'));
-        token = token.next;
-        rewriter.insertToken(token,
-            new SyntheticKeywordToken(Keyword.ON, token.next.charOffset));
+        token = parseMixinOn(token);
+      } else {
+        token = parseMixinOnOpt(token);
       }
-      token = parseMixinOnOpt(token);
 
       if (recoveryListener.onKeyword != null) {
         if (hasOn) {
@@ -1961,16 +1962,24 @@
   /// ;
   /// ```
   Token parseMixinOnOpt(Token token) {
-    Token onKeyword;
-    int typeCount = 0;
-    if (optional('on', token.next)) {
-      onKeyword = token.next;
-      do {
-        token =
-            computeType(token.next, true).ensureTypeNotVoid(token.next, this);
-        ++typeCount;
-      } while (optional(',', token.next));
+    if (!optional('on', token.next)) {
+      listener.handleMixinOn(null, 0);
+      return token;
     }
+    return parseMixinOn(token);
+  }
+
+  Token parseMixinOn(Token token) {
+    Token onKeyword = token.next;
+    // During recovery, the [onKeyword] can be "extend" or "extends"
+    assert(optional('on', onKeyword) ||
+        optional('extends', onKeyword) ||
+        onKeyword.lexeme == 'extend');
+    int typeCount = 0;
+    do {
+      token = computeType(token.next, true).ensureTypeNotVoid(token.next, this);
+      ++typeCount;
+    } while (optional(',', token.next));
     listener.handleMixinOn(onKeyword, typeCount);
     return token;
   }