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