first cut parse extension methods

This is an incomplete parser implementation of extension methods.
Subsequent CLs will flesh out the details.

Change-Id: I8c891827e97b451388cadc9cedea3afb7a8cf922
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/103464
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analyzer/lib/src/fasta/ast_builder.dart b/pkg/analyzer/lib/src/fasta/ast_builder.dart
index 8e4b1d2..3754d84 100644
--- a/pkg/analyzer/lib/src/fasta/ast_builder.dart
+++ b/pkg/analyzer/lib/src/fasta/ast_builder.dart
@@ -12,8 +12,8 @@
 import 'package:analyzer/src/dart/ast/ast.dart'
     show
         ClassDeclarationImpl,
-        ClassOrMixinDeclarationImpl,
         CompilationUnitImpl,
+        ExtensionDeclarationImpl,
         MixinDeclarationImpl;
 import 'package:analyzer/src/fasta/error_converter.dart';
 import 'package:analyzer/src/generated/utilities_dart.dart';
@@ -86,6 +86,9 @@
   /// The mixin currently being parsed, or `null` if no mixin is being parsed.
   MixinDeclarationImpl mixinDeclaration;
 
+  /// The extension currently being parsed, or `null` if none.
+  ExtensionDeclarationImpl extensionDeclaration;
+
   /// If true, this is building a full AST. Otherwise, only create method
   /// bodies.
   final bool isFullAst;
@@ -127,6 +130,26 @@
       : this.errorReporter = new FastaErrorReporter(errorReporter),
         uri = uri ?? fileUri;
 
+  NodeList<ClassMember> get currentDeclarationMembers {
+    if (classDeclaration != null) {
+      return classDeclaration.members;
+    } else if (mixinDeclaration != null) {
+      return mixinDeclaration.members;
+    } else {
+      return extensionDeclaration.members;
+    }
+  }
+
+  SimpleIdentifier get currentDeclarationName {
+    if (classDeclaration != null) {
+      return classDeclaration.name;
+    } else if (mixinDeclaration != null) {
+      return mixinDeclaration.name;
+    } else {
+      return extensionDeclaration.name;
+    }
+  }
+
   @override
   void addProblem(Message message, int charOffset, int length,
       {bool wasHandled: false, List<LocatedMessage> context}) {
@@ -155,7 +178,9 @@
 
   @override
   void beginClassDeclaration(Token begin, Token abstractToken, Token name) {
-    assert(classDeclaration == null && mixinDeclaration == null);
+    assert(classDeclaration == null &&
+        mixinDeclaration == null &&
+        extensionDeclaration == null);
     push(new _Modifiers()..abstractKeyword = abstractToken);
   }
 
@@ -165,6 +190,30 @@
   }
 
   @override
+  void beginExtensionDeclaration(Token extensionKeyword, Token nameToken) {
+    assert(optional('extension', extensionKeyword));
+    assert(classDeclaration == null &&
+        mixinDeclaration == null &&
+        extensionDeclaration == null);
+    debugEvent("ExtensionHeader");
+
+    TypeParameterList typeParameters = pop();
+    SimpleIdentifier name = pop();
+    List<Annotation> metadata = pop();
+    Comment comment = _findComment(metadata, extensionKeyword);
+
+    extensionDeclaration = ast.extensionDeclaration(
+      comment: comment,
+      metadata: metadata,
+      name: name,
+      typeParameters: typeParameters,
+      extendedType: null, // extendedType is set in [endExtensionDeclaration]
+    ) as ExtensionDeclarationImpl;
+
+    declarations.add(extensionDeclaration);
+  }
+
+  @override
   void beginFactoryMethod(
       Token lastConsumed, Token externalToken, Token constToken) {
     push(new _Modifiers()
@@ -234,7 +283,9 @@
 
   @override
   void beginMixinDeclaration(Token mixinKeyword, Token name) {
-    assert(classDeclaration == null && mixinDeclaration == null);
+    assert(classDeclaration == null &&
+        mixinDeclaration == null &&
+        extensionDeclaration == null);
   }
 
   @override
@@ -481,14 +532,25 @@
   @override
   void endClassOrMixinBody(
       int memberCount, Token leftBracket, Token rightBracket) {
+    // TODO(danrubel): consider renaming endClassOrMixinBody
+    // to endClassOrMixinOrExtensionBody
     assert(optional('{', leftBracket));
     assert(optional('}', rightBracket));
     debugEvent("ClassOrMixinBody");
 
-    ClassOrMixinDeclarationImpl declaration =
-        classDeclaration ?? mixinDeclaration;
-    declaration.leftBracket = leftBracket;
-    declaration.rightBracket = rightBracket;
+    if (classDeclaration != null) {
+      classDeclaration
+        ..leftBracket = leftBracket
+        ..rightBracket = rightBracket;
+    } else if (mixinDeclaration != null) {
+      mixinDeclaration
+        ..leftBracket = leftBracket
+        ..rightBracket = rightBracket;
+    } else {
+      extensionDeclaration
+        ..leftBracket = leftBracket
+        ..rightBracket = rightBracket;
+    }
   }
 
   @override
@@ -643,6 +705,15 @@
   }
 
   @override
+  void endExtensionDeclaration(Token onKeyword, Token token) {
+    TypeAnnotation type = pop();
+    extensionDeclaration
+      ..extendedType = type
+      ..onKeyword = onKeyword;
+    extensionDeclaration = null;
+  }
+
+  @override
   void endFactoryMethod(
       Token beginToken, Token factoryKeyword, Token endToken) {
     assert(optional('factory', factoryKeyword));
@@ -694,21 +765,20 @@
           ast.simpleIdentifier(typeName.identifier.token, isDeclaration: true);
     }
 
-    (classDeclaration ?? mixinDeclaration).members.add(
-        ast.constructorDeclaration(
-            comment,
-            metadata,
-            modifiers?.externalKeyword,
-            modifiers?.finalConstOrVarKeyword,
-            factoryKeyword,
-            ast.simpleIdentifier(returnType.token),
-            period,
-            name,
-            parameters,
-            separator,
-            null,
-            redirectedConstructor,
-            body));
+    currentDeclarationMembers.add(ast.constructorDeclaration(
+        comment,
+        metadata,
+        modifiers?.externalKeyword,
+        modifiers?.finalConstOrVarKeyword,
+        factoryKeyword,
+        ast.simpleIdentifier(returnType.token),
+        period,
+        name,
+        parameters,
+        separator,
+        null,
+        redirectedConstructor,
+        body));
   }
 
   void endFieldInitializer(Token assignment, Token token) {
@@ -737,7 +807,7 @@
     Token covariantKeyword = covariantToken;
     List<Annotation> metadata = pop();
     Comment comment = _findComment(metadata, beginToken);
-    (classDeclaration ?? mixinDeclaration).members.add(ast.fieldDeclaration2(
+    currentDeclarationMembers.add(ast.fieldDeclaration2(
         comment: comment,
         metadata: metadata,
         covariantKeyword: covariantKeyword,
@@ -1415,9 +1485,6 @@
           beginToken.charOffset, uri);
     }
 
-    ClassOrMixinDeclarationImpl declaration =
-        classDeclaration ?? mixinDeclaration;
-
     void constructor(
         SimpleIdentifier prefixOrName, Token period, SimpleIdentifier name) {
       if (typeParameters != null) {
@@ -1453,7 +1520,7 @@
           initializers,
           redirectedConstructor,
           body);
-      declaration.members.add(constructor);
+      currentDeclarationMembers.add(constructor);
       if (mixinDeclaration != null) {
         // TODO (danrubel): Report an error if this is a mixin declaration.
       }
@@ -1466,7 +1533,7 @@
             messageConstMethod, modifiers.constKeyword, modifiers.constKeyword);
       }
       checkFieldFormalParameters(parameters);
-      declaration.members.add(ast.methodDeclaration(
+      currentDeclarationMembers.add(ast.methodDeclaration(
           comment,
           metadata,
           modifiers?.externalKeyword,
@@ -1481,7 +1548,7 @@
     }
 
     if (name is SimpleIdentifier) {
-      if (name.name == declaration.name.name && getOrSet == null) {
+      if (name.name == currentDeclarationName.name && getOrSet == null) {
         constructor(name, null, null);
       } else if (initializers.isNotEmpty && getOrSet == null) {
         constructor(name, null, null);
@@ -2683,7 +2750,9 @@
   @override
   void handleMixinHeader(Token mixinKeyword) {
     assert(optional('mixin', mixinKeyword));
-    assert(classDeclaration == null && mixinDeclaration == null);
+    assert(classDeclaration == null &&
+        mixinDeclaration == null &&
+        extensionDeclaration == null);
     debugEvent("MixinHeader");
 
     ImplementsClause implementsClause = pop(NullValue.IdentifierList);
diff --git a/pkg/analyzer/test/generated/parser_fasta_listener.dart b/pkg/analyzer/test/generated/parser_fasta_listener.dart
index 111bcff..26fff6d 100644
--- a/pkg/analyzer/test/generated/parser_fasta_listener.dart
+++ b/pkg/analyzer/test/generated/parser_fasta_listener.dart
@@ -201,6 +201,12 @@
   }
 
   @override
+  void beginExtensionDeclaration(Token extensionKeyword, Token name) {
+    super.beginExtensionDeclaration(extensionKeyword, name);
+    begin('ExtensionDeclaration');
+  }
+
+  @override
   void beginFactoryMethod(
       Token lastConsumed, Token externalToken, Token constToken) {
     super.beginFactoryMethod(lastConsumed, externalToken, constToken);
@@ -690,6 +696,12 @@
   }
 
   @override
+  void endExtensionDeclaration(Token onKeyword, Token token) {
+    super.endExtensionDeclaration(onKeyword, token);
+    end('ExtensionDeclaration');
+  }
+
+  @override
   void endFactoryMethod(
       Token beginToken, Token factoryKeyword, Token endToken) {
     end('FactoryMethod');
diff --git a/pkg/analyzer/test/generated/parser_fasta_test.dart b/pkg/analyzer/test/generated/parser_fasta_test.dart
index 6de285d..40b194d 100644
--- a/pkg/analyzer/test/generated/parser_fasta_test.dart
+++ b/pkg/analyzer/test/generated/parser_fasta_test.dart
@@ -36,6 +36,7 @@
 main() {
   defineReflectiveSuite(() {
     defineReflectiveTests(ClassMemberParserTest_Fasta);
+    defineReflectiveTests(ExtensionMethodsParserTest_Fasta);
     defineReflectiveTests(CollectionLiteralParserTest);
     defineReflectiveTests(ComplexParserTest_Fasta);
     defineReflectiveTests(ErrorParserTest_Fasta);
@@ -1435,6 +1436,43 @@
   }
 }
 
+@reflectiveTest
+class ExtensionMethodsParserTest_Fasta extends FastaParserTestCase {
+  @override
+  CompilationUnit parseCompilationUnit(String content,
+      {List<ErrorCode> codes,
+      List<ExpectedError> errors,
+      FeatureSet featureSet}) {
+    return super.parseCompilationUnit(content,
+        codes: codes,
+        errors: errors,
+        featureSet: featureSet ??
+            FeatureSet.forTesting(
+              sdkVersion: '2.3.0',
+              additionalFeatures: [Feature.extension_methods],
+            ));
+  }
+
+  void test_simple() {
+    var unit = parseCompilationUnit('extension E on C { }');
+    expect(unit.declarations, hasLength(1));
+    var extension = unit.declarations[0] as ExtensionDeclaration;
+    expect(extension.name.name, 'E');
+    expect(extension.onKeyword.lexeme, 'on');
+    expect((extension.extendedType as NamedType).name.name, 'C');
+    expect(extension.members, hasLength(0));
+  }
+
+  void test_simple_not_enabled() {
+    parseCompilationUnit('extension E on C { }',
+        errors: [
+          expectedError(ParserErrorCode.EXPERIMENT_NOT_ENABLED, 0, 9),
+          expectedError(ParserErrorCode.MISSING_FUNCTION_PARAMETERS, 15, 1)
+        ],
+        featureSet: FeatureSet.forTesting(sdkVersion: '2.3.0'));
+  }
+}
+
 /**
  * Implementation of [AbstractParserTestCase] specialized for testing the
  * Fasta parser.
diff --git a/pkg/front_end/lib/src/fasta/parser/forwarding_listener.dart b/pkg/front_end/lib/src/fasta/parser/forwarding_listener.dart
index 97d93c8..c596a7e 100644
--- a/pkg/front_end/lib/src/fasta/parser/forwarding_listener.dart
+++ b/pkg/front_end/lib/src/fasta/parser/forwarding_listener.dart
@@ -146,6 +146,11 @@
   }
 
   @override
+  void beginExtensionDeclaration(Token extensionKeyword, Token name) {
+    listener?.beginExtensionDeclaration(extensionKeyword, name);
+  }
+
+  @override
   void beginFactoryMethod(
       Token lastConsumed, Token externalToken, Token constToken) {
     listener?.beginFactoryMethod(lastConsumed, externalToken, constToken);
@@ -582,6 +587,11 @@
   }
 
   @override
+  void endExtensionDeclaration(Token onKeyword, Token token) {
+    listener?.endExtensionDeclaration(onKeyword, token);
+  }
+
+  @override
   void endFactoryMethod(
       Token beginToken, Token factoryKeyword, Token endToken) {
     listener?.endFactoryMethod(beginToken, factoryKeyword, endToken);
diff --git a/pkg/front_end/lib/src/fasta/parser/identifier_context.dart b/pkg/front_end/lib/src/fasta/parser/identifier_context.dart
index 460fec9..460dc1b 100644
--- a/pkg/front_end/lib/src/fasta/parser/identifier_context.dart
+++ b/pkg/front_end/lib/src/fasta/parser/identifier_context.dart
@@ -98,7 +98,8 @@
   /// Identifier is the name being declared by a class declaration, a mixin
   /// declaration, or a named mixin application, for example,
   /// `Foo` in `class Foo = X with Y;`.
-  static const classOrMixinDeclaration = const ClassOrMixinIdentifierContext();
+  static const classOrMixinOrExtensionDeclaration =
+      const ClassOrMixinOrExtensionIdentifierContext();
 
   /// Identifier is the name of a type variable being declared (e.g. `Foo` in
   /// `class C<Foo extends num> {}`).
diff --git a/pkg/front_end/lib/src/fasta/parser/identifier_context_impl.dart b/pkg/front_end/lib/src/fasta/parser/identifier_context_impl.dart
index 0dd6638..db81019 100644
--- a/pkg/front_end/lib/src/fasta/parser/identifier_context_impl.dart
+++ b/pkg/front_end/lib/src/fasta/parser/identifier_context_impl.dart
@@ -43,9 +43,9 @@
   }
 }
 
-/// See [IdentifierContext.classOrMixinDeclaration].
-class ClassOrMixinIdentifierContext extends IdentifierContext {
-  const ClassOrMixinIdentifierContext()
+/// See [IdentifierContext.classOrMixinOrExtensionDeclaration].
+class ClassOrMixinOrExtensionIdentifierContext extends IdentifierContext {
+  const ClassOrMixinOrExtensionIdentifierContext()
       : super('classOrMixinDeclaration',
             inDeclaration: true, isBuiltInIdentifierAllowed: false);
 
@@ -59,8 +59,8 @@
 
     // Recovery
     if (looksLikeStartOfNextTopLevelDeclaration(identifier) ||
-        isOneOfOrEof(
-            identifier, const ['<', '{', 'extends', 'with', 'implements'])) {
+        isOneOfOrEof(identifier,
+            const ['<', '{', 'extends', 'with', 'implements', 'on'])) {
       identifier = parser.insertSyntheticIdentifier(token, this,
           message: fasta.templateExpectedIdentifier.withArguments(identifier));
     } else if (identifier.type.isBuiltIn) {
diff --git a/pkg/front_end/lib/src/fasta/parser/listener.dart b/pkg/front_end/lib/src/fasta/parser/listener.dart
index 5239ab9..0e8f57d 100644
--- a/pkg/front_end/lib/src/fasta/parser/listener.dart
+++ b/pkg/front_end/lib/src/fasta/parser/listener.dart
@@ -151,7 +151,7 @@
     logEvent("MixinOn");
   }
 
-  /// Handle the header of a class declaration.  Substructures:
+  /// Handle the header of a mixin declaration.  Substructures:
   /// - metadata
   /// - mixin name
   /// - type variables
@@ -179,6 +179,20 @@
     logEvent("MixinDeclaration");
   }
 
+  /// Handle the beginning of an extension methods declaration.  Substructures:
+  /// - metadata
+  /// - extension name
+  /// - type variables
+  void beginExtensionDeclaration(Token extensionKeyword, Token name) {}
+
+  /// Handle the end of an extension methods declaration.  Substructures:
+  /// - substructures from [beginExtensionDeclaration]
+  /// - on type
+  /// - body
+  void endExtensionDeclaration(Token onKeyword, Token token) {
+    logEvent('ExtensionDeclaration');
+  }
+
   void beginCombinators(Token token) {}
 
   void endCombinators(int count) {
diff --git a/pkg/front_end/lib/src/fasta/parser/parser.dart b/pkg/front_end/lib/src/fasta/parser/parser.dart
index 9090a1a..38542074 100644
--- a/pkg/front_end/lib/src/fasta/parser/parser.dart
+++ b/pkg/front_end/lib/src/fasta/parser/parser.dart
@@ -585,23 +585,26 @@
         return parseTopLevelMemberImpl(start);
       } else {
         parseTopLevelKeywordModifiers(start, keyword);
-        if (identical(value, 'mixin')) {
-          directiveState?.checkDeclaration();
-          return parseMixin(keyword);
-        } else if (identical(value, 'typedef')) {
-          directiveState?.checkDeclaration();
-          return parseTypedef(keyword);
-        } else if (identical(value, 'library')) {
-          directiveState?.checkLibrary(this, keyword);
-          return parseLibraryName(keyword);
-        } else if (identical(value, 'import')) {
+        if (identical(value, 'import')) {
           directiveState?.checkImport(this, keyword);
           return parseImport(keyword);
         } else if (identical(value, 'export')) {
           directiveState?.checkExport(this, keyword);
           return parseExport(keyword);
+        } else if (identical(value, 'typedef')) {
+          directiveState?.checkDeclaration();
+          return parseTypedef(keyword);
+        } else if (identical(value, 'mixin')) {
+          directiveState?.checkDeclaration();
+          return parseMixin(keyword);
+        } else if (identical(value, 'extension')) {
+          directiveState?.checkDeclaration();
+          return parseExtension(keyword);
         } else if (identical(value, 'part')) {
           return parsePartOrPartOf(keyword, directiveState);
+        } else if (identical(value, 'library')) {
+          directiveState?.checkLibrary(this, keyword);
+          return parseLibraryName(keyword);
         }
       }
     }
@@ -1724,7 +1727,7 @@
     Token begin = abstractToken ?? classKeyword;
     listener.beginClassOrNamedMixinApplication(begin);
     Token name = ensureIdentifier(
-        classKeyword, IdentifierContext.classOrMixinDeclaration);
+        classKeyword, IdentifierContext.classOrMixinOrExtensionDeclaration);
     Token token = computeTypeParamOrArg(name, true).parseVariables(name, this);
     if (optional('=', token.next)) {
       listener.beginNamedMixinApplication(begin, abstractToken, name);
@@ -1927,7 +1930,7 @@
     assert(optional('mixin', mixinKeyword));
     listener.beginClassOrNamedMixinApplication(mixinKeyword);
     Token name = ensureIdentifier(
-        mixinKeyword, IdentifierContext.classOrMixinDeclaration);
+        mixinKeyword, IdentifierContext.classOrMixinOrExtensionDeclaration);
     Token headerStart =
         computeTypeParamOrArg(name, true).parseVariables(name, this);
     listener.beginMixinDeclaration(mixinKeyword, name);
@@ -2048,6 +2051,36 @@
     return token;
   }
 
+  /// ```
+  /// '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);
+    listener.beginExtensionDeclaration(extensionKeyword, name);
+    Token onKeyword = token.next;
+    if (!optional('on', onKeyword)) {
+      // Recovery
+      // TODO(danrubel): implement this.
+      throw "Internal error: extension recovery not implemented yet.";
+    }
+    TypeInfo typeInfo = computeType(onKeyword, true);
+    token = typeInfo.ensureTypeNotVoid(onKeyword, this);
+    if (!optional('{', token.next)) {
+      // TODO(danrubel): replace this with a better error message.
+      ensureBlock(token, fasta.templateExpectedClassOrMixinBody);
+    }
+    // TODO(danrubel): Do not allow fields or constructors
+    token = parseClassOrMixinBody(token);
+    listener.endExtensionDeclaration(onKeyword, token);
+    return token;
+  }
+
   Token parseStringPart(Token token) {
     Token next = token.next;
     if (next.kind != STRING_TOKEN) {
@@ -2321,7 +2354,27 @@
           name, name, lateToken, varFinalOrConst, isTopLevel);
       ++fieldCount;
     }
-    token = ensureSemicolon(token);
+    Token semicolon = token.next;
+    if (optional(';', semicolon)) {
+      token = semicolon;
+    } else {
+      // Recovery
+      if (isTopLevel &&
+          beforeType.next.isIdentifier &&
+          beforeType.next.lexeme == 'extension') {
+        // Looks like an extension method
+        // TODO(danrubel): Remove when extension methods are enabled by default
+        // because then 'extension' will be interpreted as a built-in
+        // and this code will never be executed
+        reportRecoverableError(
+            beforeType.next,
+            fasta.templateExperimentNotEnabled
+                .withArguments('extension-methods'));
+        token = rewriter.insertSyntheticToken(token, TokenType.SEMICOLON);
+      } else {
+        token = ensureSemicolon(token);
+      }
+    }
     if (isTopLevel) {
       listener.endTopLevelFields(staticToken, covariantToken, lateToken,
           varFinalOrConst, fieldCount, beforeStart.next, token);
@@ -4516,8 +4569,7 @@
       // TODO(danrubel): Improve this error message.
       reportRecoverableError(
           next, fasta.templateExpectedButGot.withArguments('['));
-      rewriter.insertToken(
-          token, new SyntheticToken(TokenType.INDEX, next.charOffset));
+      rewriter.insertSyntheticToken(token, TokenType.INDEX);
     }
     return parseLiteralListSuffix(token, constKeyword);
   }
diff --git a/pkg/front_end/lib/src/fasta/parser/token_stream_rewriter.dart b/pkg/front_end/lib/src/fasta/parser/token_stream_rewriter.dart
index 6cc81bb..29ab789 100644
--- a/pkg/front_end/lib/src/fasta/parser/token_stream_rewriter.dart
+++ b/pkg/front_end/lib/src/fasta/parser/token_stream_rewriter.dart
@@ -208,9 +208,11 @@
 
   /// Insert a new simple synthetic token of [newTokenType] after [token]
   /// and return the new token.
-  Token insertSyntheticToken(Token token, TokenType newTokenType) =>
-      insertToken(
-          token, new SyntheticToken(newTokenType, token.next.charOffset));
+  Token insertSyntheticToken(Token token, TokenType newTokenType) {
+    assert(newTokenType is! Keyword, 'use insertSyntheticKeyword instead');
+    return insertToken(
+        token, new SyntheticToken(newTokenType, token.next.charOffset));
+  }
 
   /// Insert [newToken] after [token] and return [newToken].
   Token insertToken(Token token, Token newToken);