add limited support for parsing if control flow entries in lists, sets, and maps
This updates the parser to handle two if control flow entries
* `if` condition entry
* `if` condition spread-collection
... and also addresses comments in
https://dart-review.googlesource.com/c/sdk/+/90500
Change-Id: Ibabec8f0f4da918ef4cfe429405dae82ba74c129
Reviewed-on: https://dart-review.googlesource.com/c/90620
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 0ca01b7..d2e66e4 100644
--- a/pkg/analyzer/lib/src/fasta/ast_builder.dart
+++ b/pkg/analyzer/lib/src/fasta/ast_builder.dart
@@ -277,6 +277,31 @@
scriptTag = ast.scriptTag(token);
}
+ void beginIfControlFlow(Token ifToken) {
+ push(ifToken);
+ }
+
+ @override
+ void endIfControlFlow(Token token) {
+ CollectionElement thenElement = pop();
+ ParenthesizedExpression condition = pop();
+ Token ifToken = pop();
+ if (enableControlFlowCollections) {
+ push(ast.collectionIfElement(
+ ifKeyword: ifToken,
+ leftParenthesis: condition.leftParenthesis,
+ condition: condition.expression,
+ rightParenthesis: condition.rightParenthesis,
+ thenElement: thenElement,
+ elseKeyword: null,
+ elseElement: null));
+ } else {
+ handleRecoverableError(
+ templateUnexpectedToken.withArguments(ifToken), ifToken, ifToken);
+ push(thenElement);
+ }
+ }
+
@override
void handleSpreadExpression(Token spreadToken) {
if (enableSpreadCollections) {
diff --git a/pkg/analyzer/test/generated/parser_fasta_listener.dart b/pkg/analyzer/test/generated/parser_fasta_listener.dart
index 0808c6b..c558376 100644
--- a/pkg/analyzer/test/generated/parser_fasta_listener.dart
+++ b/pkg/analyzer/test/generated/parser_fasta_listener.dart
@@ -251,6 +251,12 @@
}
@override
+ void beginIfControlFlow(Token ifToken) {
+ super.beginIfControlFlow(ifToken);
+ begin('IfControlFlow');
+ }
+
+ @override
void beginLocalFunctionDeclaration(Token token) {
super.beginLocalFunctionDeclaration(token);
begin('LocalFunctionDeclaration');
@@ -789,6 +795,12 @@
}
@override
+ void endIfControlFlow(Token token) {
+ end('IfControlFlow');
+ super.endIfControlFlow(token);
+ }
+
+ @override
void endIfStatement(Token ifToken, Token elseToken) {
end('IfStatement');
super.endIfStatement(ifToken, elseToken);
diff --git a/pkg/analyzer/test/generated/parser_fasta_test.dart b/pkg/analyzer/test/generated/parser_fasta_test.dart
index a274174..2f532dd 100644
--- a/pkg/analyzer/test/generated/parser_fasta_test.dart
+++ b/pkg/analyzer/test/generated/parser_fasta_test.dart
@@ -119,7 +119,8 @@
errors: errors,
expectedEndOffset: expectedEndOffset,
parseSetLiterals: true,
- parseSpreadCollections: true);
+ parseSpreadCollections: true,
+ parseControlFlowCollections: true);
}
@failingTest
@@ -139,20 +140,30 @@
expect(first.value, 1);
}
- @failingTest
void test_listLiteral_if() {
ListLiteral2 list = parseCollectionLiteral('[1, if (true) 2]');
expect(list.elements, hasLength(2));
IntegerLiteral first = list.elements[0];
expect(first.value, 1);
+
+ CollectionIfElement second = list.elements[1];
+ BooleanLiteral condition = second.condition;
+ expect(condition.value, isTrue);
+ IntegerLiteral thenElement = second.thenElement;
+ expect(thenElement.value, 2);
}
- @failingTest
void test_listLiteral_ifSpread() {
ListLiteral2 list = parseCollectionLiteral('[1, if (true) ...[2]]');
expect(list.elements, hasLength(2));
IntegerLiteral first = list.elements[0];
expect(first.value, 1);
+
+ CollectionIfElement second = list.elements[1];
+ BooleanLiteral condition = second.condition;
+ expect(condition.value, isTrue);
+ SpreadElement thenElement = second.thenElement;
+ expect(thenElement.spreadOperator.lexeme, '...');
}
void test_listLiteral_spread() {
diff --git a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
index 864339b..624c748 100644
--- a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
@@ -3503,6 +3503,23 @@
token.next, token.offset, Constness.explicitConst);
}
+ void beginIfControlFlow(Token ifToken) {
+ // TODO(danrubel): remove this when control flow support is added
+ push(ifToken);
+ }
+
+ @override
+ void endIfControlFlow(Token token) {
+ debugEvent("IfControlFlow");
+ // TODO(danrubel) implement control flow support
+ var entry = pop();
+ pop(); // parenthesized expression
+ Token ifToken = pop();
+ push(entry); // push the entry back on the stack and drop the rest
+ handleRecoverableError(
+ fasta.templateUnexpectedToken.withArguments(ifToken), ifToken, ifToken);
+ }
+
@override
void handleSpreadExpression(Token spreadToken) {
debugEvent("SpreadExpression");
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 68fc71c..3292dd8 100644
--- a/pkg/front_end/lib/src/fasta/parser/forwarding_listener.dart
+++ b/pkg/front_end/lib/src/fasta/parser/forwarding_listener.dart
@@ -214,6 +214,11 @@
}
@override
+ void beginIfControlFlow(Token ifToken) {
+ listener?.beginIfControlFlow(ifToken);
+ }
+
+ @override
void beginIfStatement(Token token) {
listener?.beginIfStatement(token);
}
@@ -636,6 +641,11 @@
}
@override
+ void endIfControlFlow(Token token) {
+ listener?.endIfControlFlow(token);
+ }
+
+ @override
void endIfStatement(Token ifToken, Token elseToken) {
listener?.endIfStatement(ifToken, elseToken);
}
diff --git a/pkg/front_end/lib/src/fasta/parser/listener.dart b/pkg/front_end/lib/src/fasta/parser/listener.dart
index 4b1a823..101e349 100644
--- a/pkg/front_end/lib/src/fasta/parser/listener.dart
+++ b/pkg/front_end/lib/src/fasta/parser/listener.dart
@@ -1074,7 +1074,18 @@
logEvent("ConstExpression");
}
- /// Called after the parser has parsed an expression that starts with
+ /// Called before parsing an `if` control flow list, set, or map entry.
+ void beginIfControlFlow(Token ifToken) {}
+
+ /// Called after parsing an `if` control flow list, set, or map entry.
+ /// Substructures:
+ /// - if conditional expression
+ /// - expression
+ void endIfControlFlow(Token token) {
+ logEvent("IfControlFlow");
+ }
+
+ /// Called after parsing a list, set, or map entry that starts with
/// one of the spread collection tokens `...` or `...?`. Substructures:
/// - expression
void handleSpreadExpression(Token spreadToken) {
diff --git a/pkg/front_end/lib/src/fasta/parser/literal_entry_info.dart b/pkg/front_end/lib/src/fasta/parser/literal_entry_info.dart
index 4f1d9c9..dc7d5c2 100644
--- a/pkg/front_end/lib/src/fasta/parser/literal_entry_info.dart
+++ b/pkg/front_end/lib/src/fasta/parser/literal_entry_info.dart
@@ -1,3 +1,7 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
import '../scanner.dart';
import 'identifier_context.dart';
import 'literal_entry_info_impl.dart';
diff --git a/pkg/front_end/lib/src/fasta/parser/literal_entry_info_impl.dart b/pkg/front_end/lib/src/fasta/parser/literal_entry_info_impl.dart
index 3281051..b9bc2a4 100644
--- a/pkg/front_end/lib/src/fasta/parser/literal_entry_info_impl.dart
+++ b/pkg/front_end/lib/src/fasta/parser/literal_entry_info_impl.dart
@@ -1,3 +1,7 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
import '../../scanner/token.dart';
import '../fasta_codes.dart' show templateUnexpectedToken;
import 'identifier_context.dart';
@@ -44,10 +48,52 @@
Token parse(Token token, Parser parser) {
final ifToken = token.next;
assert(optional('if', ifToken));
- // TODO(danrubel): implement `if` control flow collection entries
- parser.reportRecoverableErrorWithToken(ifToken, templateUnexpectedToken);
- parser.ensureIdentifier(ifToken, IdentifierContext.expression);
- return ifToken;
+ parser.listener.beginIfControlFlow(ifToken);
+ return parser.ensureParenthesizedCondition(ifToken);
+ }
+
+ @override
+ LiteralEntryInfo computeNext(Token token) {
+ Token next = token.next;
+ if (optional('...', next) || optional('...?', next)) {
+ return const IfSpread();
+ }
+ // TODO(danrubel): nested control flow structures
+ return const IfEntry();
+ }
+}
+
+/// A step for parsing a spread collection
+/// as the `if` control flow's then-expression.
+class IfSpread extends SpreadOperator {
+ const IfSpread();
+
+ @override
+ LiteralEntryInfo computeNext(Token token) {
+ // TODO(danrubel): handle `else'
+ return const IfComplete();
+ }
+}
+
+/// A step for parsing a literal list, set, or map entry
+/// as the `if` control flow's then-expression.
+class IfEntry extends LiteralEntryInfo {
+ const IfEntry() : super(true);
+
+ @override
+ LiteralEntryInfo computeNext(Token token) {
+ // TODO(danrubel): handle `else'
+ return const IfComplete();
+ }
+}
+
+class IfComplete extends LiteralEntryInfo {
+ const IfComplete() : super(false);
+
+ @override
+ Token parse(Token token, Parser parser) {
+ parser.listener.endIfControlFlow(token);
+ return token;
}
}
diff --git a/pkg/front_end/lib/src/fasta/source/type_promotion_look_ahead_listener.dart b/pkg/front_end/lib/src/fasta/source/type_promotion_look_ahead_listener.dart
index 8db8764..b87ff91 100644
--- a/pkg/front_end/lib/src/fasta/source/type_promotion_look_ahead_listener.dart
+++ b/pkg/front_end/lib/src/fasta/source/type_promotion_look_ahead_listener.dart
@@ -377,6 +377,12 @@
}
@override
+ void endIfControlFlow(Token token) {
+ // TODO(danrubel) add support for if control flow collection entries
+ // but for now this is ignored and an error reported in the body builder.
+ }
+
+ @override
void handleSpreadExpression(Token spreadToken) {
// TODO(danrubel) add support for spread collections
// but for now this is ignored and an error reported in the body builder.
diff --git a/pkg/front_end/test/fasta/parser/literal_entry_info_test.dart b/pkg/front_end/test/fasta/parser/literal_entry_info_test.dart
new file mode 100644
index 0000000..a087fe3
--- /dev/null
+++ b/pkg/front_end/test/fasta/parser/literal_entry_info_test.dart
@@ -0,0 +1,395 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:front_end/src/fasta/messages.dart';
+import 'package:front_end/src/fasta/parser.dart';
+import 'package:front_end/src/fasta/scanner.dart';
+import 'package:front_end/src/scanner/token.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(CollectionElementTest);
+ defineReflectiveTests(MapElementTest);
+ });
+}
+
+@reflectiveTest
+class CollectionElementTest {
+ test_closingBrace() {
+ parseEntry(
+ 'before }',
+ [
+ 'handleIdentifier expression',
+ 'handleNoTypeArguments }',
+ 'handleNoArguments }',
+ 'handleSend }',
+ ],
+ errors: [error(codeExpectedIdentifier, 7, 1)],
+ expectAfter: '}',
+ );
+ }
+
+ test_comma() {
+ parseEntry(
+ 'before ,',
+ [
+ 'handleIdentifier expression',
+ 'handleNoTypeArguments ,',
+ 'handleNoArguments ,',
+ 'handleSend ,',
+ ],
+ errors: [error(codeExpectedIdentifier, 7, 1)],
+ expectAfter: ',',
+ );
+ }
+
+ test_expression() {
+ parseEntry(
+ 'before x',
+ [
+ 'handleIdentifier x expression',
+ 'handleNoTypeArguments ',
+ 'handleNoArguments ',
+ 'handleSend x ',
+ ],
+ );
+ }
+
+ test_if() {
+ parseEntry(
+ 'before if (true) 2',
+ [
+ 'beginIfControlFlow if',
+ 'handleLiteralBool true',
+ 'handleParenthesizedCondition (',
+ 'handleLiteralInt 2',
+ 'endIfControlFlow 2',
+ ],
+ );
+ }
+
+ test_ifSpreadQ() {
+ parseEntry(
+ 'before if (true) ...?[2]',
+ [
+ 'beginIfControlFlow if',
+ 'handleLiteralBool true',
+ 'handleParenthesizedCondition (',
+ 'handleNoTypeArguments [',
+ 'handleLiteralInt 2',
+ 'handleLiteralList 1, [, null, ]',
+ 'handleSpreadExpression ...?',
+ 'endIfControlFlow ]',
+ ],
+ );
+ }
+
+ test_intLiteral() {
+ parseEntry('before 1', [
+ 'handleLiteralInt 1',
+ ]);
+ }
+
+ test_spread() {
+ parseEntry('before ...[1]', [
+ 'handleNoTypeArguments [',
+ 'handleLiteralInt 1',
+ 'handleLiteralList 1, [, null, ]',
+ 'handleSpreadExpression ...',
+ ]);
+ }
+
+ test_spreadQ() {
+ parseEntry('before ...?[1]', [
+ 'handleNoTypeArguments [',
+ 'handleLiteralInt 1',
+ 'handleLiteralList 1, [, null, ]',
+ 'handleSpreadExpression ...?',
+ ]);
+ }
+
+ void parseEntry(String source, List<String> expectedCalls,
+ {List<ExpectedError> errors, String expectAfter}) {
+ final start = scanString(source).tokens;
+ final listener = new TestInfoListener();
+ final parser = new Parser(listener);
+ final lastConsumed = parser.parseListOrSetLiteralEntry(start);
+
+ expect(listener.errors, errors);
+ expect(listener.calls, expectedCalls, reason: source);
+ if (expectAfter != null) {
+ expect(lastConsumed.next.lexeme, expectAfter);
+ } else {
+ expect(lastConsumed.next.isEof, isTrue, reason: lastConsumed.lexeme);
+ }
+ }
+}
+
+@reflectiveTest
+class MapElementTest {
+ test_closingBrace() {
+ parseEntry(
+ 'before }',
+ [
+ 'handleIdentifier expression',
+ 'handleNoTypeArguments }',
+ 'handleNoArguments }',
+ 'handleSend }',
+ 'handleIdentifier expression',
+ 'handleNoTypeArguments }',
+ 'handleNoArguments }',
+ 'handleSend }',
+ 'handleLiteralMapEntry :, }',
+ ],
+ errors: [
+ error(codeExpectedIdentifier, 7, 1),
+ error(codeExpectedButGot, 7, 1),
+ error(codeExpectedIdentifier, 7, 1),
+ ],
+ expectAfter: '}',
+ );
+ }
+
+ test_comma() {
+ parseEntry(
+ 'before ,',
+ [
+ 'handleIdentifier expression',
+ 'handleNoTypeArguments ,',
+ 'handleNoArguments ,',
+ 'handleSend ,',
+ 'handleIdentifier expression',
+ 'handleNoTypeArguments ,',
+ 'handleNoArguments ,',
+ 'handleSend ,',
+ 'handleLiteralMapEntry :, ,',
+ ],
+ errors: [
+ error(codeExpectedIdentifier, 7, 1),
+ error(codeExpectedButGot, 7, 1),
+ error(codeExpectedIdentifier, 7, 1),
+ ],
+ expectAfter: ',',
+ );
+ }
+
+ test_expression() {
+ parseEntry(
+ 'before x:y',
+ [
+ 'handleIdentifier x expression',
+ 'handleNoTypeArguments :',
+ 'handleNoArguments :',
+ 'handleSend x :',
+ 'handleIdentifier y expression',
+ 'handleNoTypeArguments ',
+ 'handleNoArguments ',
+ 'handleSend y ',
+ 'handleLiteralMapEntry :, ',
+ ],
+ );
+ }
+
+ test_if() {
+ parseEntry(
+ 'before if (true) 2:3',
+ [
+ 'beginIfControlFlow if',
+ 'handleLiteralBool true',
+ 'handleParenthesizedCondition (',
+ 'handleLiteralInt 2',
+ 'handleLiteralInt 3',
+ 'handleLiteralMapEntry :, ',
+ 'endIfControlFlow 3',
+ ],
+ );
+ }
+
+ test_ifSpread() {
+ parseEntry(
+ 'before if (true) ...{2:3}',
+ [
+ 'beginIfControlFlow if',
+ 'handleLiteralBool true',
+ 'handleParenthesizedCondition (',
+ 'handleNoTypeArguments {',
+ 'handleLiteralInt 2',
+ 'handleLiteralInt 3',
+ 'handleLiteralMapEntry :, }',
+ 'handleLiteralMap 1, {, null, }',
+ 'handleSpreadExpression ...',
+ 'endIfControlFlow }',
+ ],
+ );
+ }
+
+ test_intLiteral() {
+ parseEntry('before 1:2', [
+ 'handleLiteralInt 1',
+ 'handleLiteralInt 2',
+ 'handleLiteralMapEntry :, ',
+ ]);
+ }
+
+ test_spread() {
+ parseEntry('before ...const {1:2}', [
+ 'beginConstLiteral {',
+ 'handleNoTypeArguments {',
+ 'handleLiteralInt 1',
+ 'handleLiteralInt 2',
+ 'handleLiteralMapEntry :, }',
+ 'handleLiteralMap 1, {, const, }',
+ 'endConstLiteral ',
+ 'handleSpreadExpression ...',
+ ]);
+ }
+
+ test_spreadQ() {
+ parseEntry('before ...?const {1:3}', [
+ 'beginConstLiteral {',
+ 'handleNoTypeArguments {',
+ 'handleLiteralInt 1',
+ 'handleLiteralInt 3',
+ 'handleLiteralMapEntry :, }',
+ 'handleLiteralMap 1, {, const, }',
+ 'endConstLiteral ',
+ 'handleSpreadExpression ...?',
+ ]);
+ }
+
+ void parseEntry(String source, List<String> expectedCalls,
+ {List<ExpectedError> errors, String expectAfter}) {
+ final start = scanString(source).tokens;
+ final listener = new TestInfoListener();
+ final parser = new Parser(listener);
+ final lastConsumed = parser.parseMapLiteralEntry(start);
+
+ expect(listener.errors, errors);
+ expect(listener.calls, expectedCalls, reason: source);
+ if (expectAfter != null) {
+ expect(lastConsumed.next.lexeme, expectAfter);
+ } else {
+ expect(lastConsumed.next.isEof, isTrue, reason: lastConsumed.lexeme);
+ }
+ }
+}
+
+class TestInfoListener implements Listener {
+ List<String> calls = <String>[];
+ List<ExpectedError> errors;
+
+ @override
+ void beginConstLiteral(Token token) {
+ calls.add('beginConstLiteral $token');
+ }
+
+ @override
+ void beginIfControlFlow(Token ifToken) {
+ calls.add('beginIfControlFlow $ifToken');
+ }
+
+ @override
+ void endConstLiteral(Token token) {
+ calls.add('endConstLiteral $token');
+ }
+
+ @override
+ void endIfControlFlow(Token token) {
+ calls.add('endIfControlFlow $token');
+ }
+
+ @override
+ void handleIdentifier(Token token, IdentifierContext context) {
+ calls.add('handleIdentifier $token $context');
+ }
+
+ @override
+ void handleLiteralBool(Token token) {
+ calls.add('handleLiteralBool $token');
+ }
+
+ @override
+ void handleLiteralInt(Token token) {
+ calls.add('handleLiteralInt $token');
+ }
+
+ @override
+ void handleLiteralList(
+ int count, Token leftBracket, Token constKeyword, Token rightBracket) {
+ calls.add(
+ 'handleLiteralList $count, $leftBracket, $constKeyword, $rightBracket');
+ }
+
+ @override
+ void handleLiteralMap(
+ int count, Token leftBrace, Token constKeyword, Token rightBrace) {
+ calls
+ .add('handleLiteralMap $count, $leftBrace, $constKeyword, $rightBrace');
+ }
+
+ @override
+ void handleLiteralMapEntry(Token colon, Token endToken) {
+ calls.add('handleLiteralMapEntry $colon, $endToken');
+ }
+
+ @override
+ void handleNoArguments(Token token) {
+ calls.add('handleNoArguments $token');
+ }
+
+ @override
+ void handleParenthesizedCondition(Token token) {
+ calls.add('handleParenthesizedCondition $token');
+ }
+
+ @override
+ void handleNoTypeArguments(Token token) {
+ calls.add('handleNoTypeArguments $token');
+ }
+
+ @override
+ void handleRecoverableError(
+ Message message, Token startToken, Token endToken) {
+ errors ??= <ExpectedError>[];
+ int offset = startToken.charOffset;
+ errors.add(error(message.code, offset, endToken.charEnd - offset));
+ }
+
+ @override
+ void handleSend(Token beginToken, Token endToken) {
+ calls.add('handleSend $beginToken $endToken');
+ }
+
+ @override
+ void handleSpreadExpression(Token spreadToken) {
+ calls.add('handleSpreadExpression $spreadToken');
+ }
+
+ noSuchMethod(Invocation invocation) {
+ throw '${invocation.memberName} should not be called.';
+ }
+}
+
+ExpectedError error(Code code, int start, int length) =>
+ new ExpectedError(code, start, length);
+
+class ExpectedError {
+ final Code code;
+ final int start;
+ final int length;
+
+ ExpectedError(this.code, this.start, this.length);
+
+ @override
+ bool operator ==(other) =>
+ other is ExpectedError &&
+ code == other.code &&
+ start == other.start &&
+ length == other.length;
+
+ @override
+ String toString() => 'error(code${code.name}, $start, $length)';
+}