make extension declaration name optional

This CL updates the parser to parse extension declarations where
the extension name is omitted. For more details, see
https://github.com/dart-lang/language/pull/303/files#diff-9b84d2e3c88265bde2eb43544ddb757dR66

This change explicitly does not handle the case `extension on on on { … }`.
A subsequent CL should either make `on` a builtin
or add more lookahead in the parser to allow this edge case.

Change-Id: Ided821192770f88a51a3300fed8cb0fdef9eea3f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/105900
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 73b6a7c..e9c51f7 100644
--- a/pkg/analyzer/lib/src/fasta/ast_builder.dart
+++ b/pkg/analyzer/lib/src/fasta/ast_builder.dart
@@ -198,10 +198,14 @@
     debugEvent("ExtensionHeader");
 
     TypeParameterList typeParameters = pop();
-    SimpleIdentifier name = pop();
     List<Annotation> metadata = pop();
     Comment comment = _findComment(metadata, extensionKeyword);
 
+    SimpleIdentifier name;
+    if (nameToken != null) {
+      name = ast.simpleIdentifier(nameToken);
+    }
+
     extensionDeclaration = ast.extensionDeclaration(
       comment: comment,
       metadata: metadata,
diff --git a/pkg/analyzer/test/generated/parser_fasta_test.dart b/pkg/analyzer/test/generated/parser_fasta_test.dart
index 291c7ff..d4e9461 100644
--- a/pkg/analyzer/test/generated/parser_fasta_test.dart
+++ b/pkg/analyzer/test/generated/parser_fasta_test.dart
@@ -1495,6 +1495,30 @@
     expect(extension.members, hasLength(0));
   }
 
+  void test_complex_type2() {
+    var unit = parseCompilationUnit('extension E<T> on C<T> { }');
+    expect(unit.declarations, hasLength(1));
+    var extension = unit.declarations[0] as ExtensionDeclaration;
+    expect(extension.name.name, 'E');
+    expect(extension.onKeyword.lexeme, 'on');
+    var namedType = (extension.extendedType as NamedType);
+    expect(namedType.name.name, 'C');
+    expect(namedType.typeArguments.arguments, hasLength(1));
+    expect(extension.members, hasLength(0));
+  }
+
+  void test_complex_type2_no_name() {
+    var unit = parseCompilationUnit('extension<T> on C<T> { }');
+    expect(unit.declarations, hasLength(1));
+    var extension = unit.declarations[0] as ExtensionDeclaration;
+    expect(extension.name, isNull);
+    expect(extension.onKeyword.lexeme, 'on');
+    var namedType = (extension.extendedType as NamedType);
+    expect(namedType.name.name, 'C');
+    expect(namedType.typeArguments.arguments, hasLength(1));
+    expect(extension.members, hasLength(0));
+  }
+
   void test_missing_on() {
     var unit = parseCompilationUnit('extension E', errors: [
       expectedError(ParserErrorCode.EXPECTED_TOKEN, 10, 1),
@@ -1571,6 +1595,19 @@
     expect(extension.members, hasLength(0));
   }
 
+  void test_simple_no_name() {
+    var unit = parseCompilationUnit('extension on C { }');
+    expect(unit.declarations, hasLength(1));
+    var extension = unit.declarations[0] as ExtensionDeclaration;
+    expect(extension.name, isNull);
+    expect(extension.onKeyword.lexeme, 'on');
+    expect((extension.extendedType as NamedType).name.name, 'C');
+    var namedType = (extension.extendedType as NamedType);
+    expect(namedType.name.name, 'C');
+    expect(namedType.typeArguments, isNull);
+    expect(extension.members, hasLength(0));
+  }
+
   void test_simple_not_enabled() {
     parseCompilationUnit('extension E on C { }',
         errors: [
diff --git a/pkg/analyzer/test/src/fasta/recovery/partial_code/extension_declaration_test.dart b/pkg/analyzer/test/src/fasta/recovery/partial_code/extension_declaration_test.dart
index e04eadd..ad7ff9a 100644
--- a/pkg/analyzer/test/src/fasta/recovery/partial_code/extension_declaration_test.dart
+++ b/pkg/analyzer/test/src/fasta/recovery/partial_code/extension_declaration_test.dart
@@ -20,13 +20,19 @@
               'keyword',
               'extension',
               [
-                ParserErrorCode.MISSING_IDENTIFIER,
                 ParserErrorCode.EXPECTED_TOKEN,
                 ParserErrorCode.EXPECTED_TYPE_NAME,
                 ParserErrorCode.EXPECTED_BODY,
               ],
-              'extension _s_ on _s_ {}',
-              failing: ['getter', 'functionNonVoid', 'functionVoid', 'mixin']),
+              'extension on _s_ {}',
+              failing: [
+                'getter',
+                'functionNonVoid',
+                'functionVoid',
+                'mixin',
+                'setter',
+                'typedef'
+              ]),
           new TestDescriptor(
               'named',
               'extension E',
diff --git a/pkg/front_end/lib/src/fasta/parser/listener.dart b/pkg/front_end/lib/src/fasta/parser/listener.dart
index 63d333f..e92f6f3 100644
--- a/pkg/front_end/lib/src/fasta/parser/listener.dart
+++ b/pkg/front_end/lib/src/fasta/parser/listener.dart
@@ -181,7 +181,6 @@
 
   /// Handle the beginning of an extension methods declaration.  Substructures:
   /// - metadata
-  /// - extension name
   /// - type variables
   void beginExtensionDeclaration(Token extensionKeyword, Token name) {}
 
diff --git a/pkg/front_end/lib/src/fasta/parser/parser.dart b/pkg/front_end/lib/src/fasta/parser/parser.dart
index 3583dc6..8a74320 100644
--- a/pkg/front_end/lib/src/fasta/parser/parser.dart
+++ b/pkg/front_end/lib/src/fasta/parser/parser.dart
@@ -578,9 +578,19 @@
       // as an identifier such as "abstract<T>() => 0;"
       // or as a prefix such as "abstract.A b() => 0;".
       String nextValue = keyword.next.stringValue;
-      if (identical(nextValue, '(') ||
-          identical(nextValue, '<') ||
-          identical(nextValue, '.')) {
+      if (identical(nextValue, '(') || identical(nextValue, '.')) {
+        directiveState?.checkDeclaration();
+        return parseTopLevelMemberImpl(start);
+      } else if (identical(nextValue, '<')) {
+        if (identical(value, 'extension')) {
+          // The neame in an extension declaration is optional:
+          // `extension<T> on ...`
+          Token endGroup = keyword.next.endGroup;
+          if (endGroup != null && optional('on', endGroup.next)) {
+            directiveState?.checkDeclaration();
+            return parseExtension(keyword);
+          }
+        }
         directiveState?.checkDeclaration();
         return parseTopLevelMemberImpl(start);
       } else {
@@ -2058,16 +2068,21 @@
   }
 
   /// ```
-  /// 'extension' <identifier><typeParameters>? 'on' <type> '?'?
+  /// 'extension' <identifier>? <typeParameters>? 'on' <type> '?'?
   //   `{'
   //     <memberDeclaration>*
   //   `}'
   /// ```
   Token parseExtension(Token extensionKeyword) {
     assert(optional('extension', extensionKeyword));
-    Token name = ensureIdentifier(
-        extensionKeyword, IdentifierContext.classOrMixinOrExtensionDeclaration);
-    Token token = computeTypeParamOrArg(name, true).parseVariables(name, this);
+    Token token = extensionKeyword;
+    Token name = token.next;
+    if (name.isIdentifier && !optional('on', name)) {
+      token = name;
+    } else {
+      name = null;
+    }
+    token = computeTypeParamOrArg(token, true).parseVariables(token, this);
     listener.beginExtensionDeclaration(extensionKeyword, name);
     Token onKeyword = token.next;
     if (!optional('on', onKeyword)) {