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)';
+}