Extra precedence level in parser to distinguish postfix operators and selectors

Fixes https://github.com/dart-lang/sdk/issues/31185

Change-Id: I77d617f6abd367d857741d04cf47571ea7492c6a
Reviewed-on: https://dart-review.googlesource.com/56111
Reviewed-by: Dan Rubel <danrubel@google.com>
Reviewed-by: Peter von der Ahé <ahe@google.com>
Commit-Queue: Aske Simon Christensen <askesc@google.com>
diff --git a/pkg/front_end/lib/src/fasta/parser/parser.dart b/pkg/front_end/lib/src/fasta/parser/parser.dart
index e446df1..0b81640 100644
--- a/pkg/front_end/lib/src/fasta/parser/parser.dart
+++ b/pkg/front_end/lib/src/fasta/parser/parser.dart
@@ -21,6 +21,7 @@
         Keyword,
         POSTFIX_PRECEDENCE,
         RELATIONAL_PRECEDENCE,
+        SELECTOR_PRECEDENCE,
         SyntheticBeginToken,
         SyntheticKeywordToken,
         SyntheticStringToken,
@@ -3963,7 +3964,7 @@
   Token parsePrecedenceExpression(
       Token token, int precedence, bool allowCascades) {
     assert(precedence >= 1);
-    assert(precedence <= POSTFIX_PRECEDENCE);
+    assert(precedence <= SELECTOR_PRECEDENCE);
     token = parseUnaryExpression(token, allowCascades);
     Token next = token.next;
     TokenType type = next.type;
@@ -3994,10 +3995,16 @@
           token = parsePrecedenceExpression(token.next, level, allowCascades);
           listener.handleAssignmentExpression(operator);
         } else if (identical(tokenLevel, POSTFIX_PRECEDENCE)) {
+          if ((identical(type, TokenType.PLUS_PLUS)) ||
+              (identical(type, TokenType.MINUS_MINUS))) {
+            listener.handleUnaryPostfixAssignmentExpression(token.next);
+            token = next;
+          }
+        } else if (identical(tokenLevel, SELECTOR_PRECEDENCE)) {
           if (identical(type, TokenType.PERIOD) ||
               identical(type, TokenType.QUESTION_PERIOD)) {
             // Left associative, so we recurse at the next higher precedence
-            // level. However, POSTFIX_PRECEDENCE is the highest level, so we
+            // level. However, SELECTOR_PRECEDENCE is the highest level, so we
             // should just call [parseUnaryExpression] directly. However, a
             // unary expression isn't legal after a period, so we call
             // [parsePrimary] instead.
@@ -4008,10 +4015,6 @@
               (identical(type, TokenType.OPEN_SQUARE_BRACKET))) {
             token = parseArgumentOrIndexStar(token, typeArguments);
             next = token.next;
-          } else if ((identical(type, TokenType.PLUS_PLUS)) ||
-              (identical(type, TokenType.MINUS_MINUS))) {
-            listener.handleUnaryPostfixAssignmentExpression(token.next);
-            token = next;
           } else if (identical(type, TokenType.INDEX)) {
             BeginToken replacement = link(
                 new BeginToken(TokenType.OPEN_SQUARE_BRACKET, next.charOffset,
diff --git a/pkg/front_end/lib/src/scanner/token.dart b/pkg/front_end/lib/src/scanner/token.dart
index 7842a42..3543578 100644
--- a/pkg/front_end/lib/src/scanner/token.dart
+++ b/pkg/front_end/lib/src/scanner/token.dart
@@ -29,6 +29,7 @@
 const int MULTIPLICATIVE_PRECEDENCE = 14;
 const int PREFIX_PRECEDENCE = 15;
 const int POSTFIX_PRECEDENCE = 16;
+const int SELECTOR_PRECEDENCE = 17;
 
 /**
  * The opening half of a grouping pair of tokens. This is used for curly
@@ -1289,7 +1290,7 @@
       const TokenType('#', 'HASH', NO_PRECEDENCE, HASH_TOKEN);
 
   static const TokenType INDEX = const TokenType(
-      '[]', 'INDEX', POSTFIX_PRECEDENCE, INDEX_TOKEN,
+      '[]', 'INDEX', SELECTOR_PRECEDENCE, INDEX_TOKEN,
       isOperator: true, isUserDefinableOperator: true);
 
   static const TokenType INDEX_EQ = const TokenType(
@@ -1328,10 +1329,10 @@
       '{', 'OPEN_CURLY_BRACKET', NO_PRECEDENCE, OPEN_CURLY_BRACKET_TOKEN);
 
   static const TokenType OPEN_PAREN =
-      const TokenType('(', 'OPEN_PAREN', POSTFIX_PRECEDENCE, OPEN_PAREN_TOKEN);
+      const TokenType('(', 'OPEN_PAREN', SELECTOR_PRECEDENCE, OPEN_PAREN_TOKEN);
 
   static const TokenType OPEN_SQUARE_BRACKET = const TokenType('[',
-      'OPEN_SQUARE_BRACKET', POSTFIX_PRECEDENCE, OPEN_SQUARE_BRACKET_TOKEN);
+      'OPEN_SQUARE_BRACKET', SELECTOR_PRECEDENCE, OPEN_SQUARE_BRACKET_TOKEN);
 
   static const TokenType PERCENT = const TokenType(
       '%', 'PERCENT', MULTIPLICATIVE_PRECEDENCE, PERCENT_TOKEN,
@@ -1342,7 +1343,7 @@
       isOperator: true);
 
   static const TokenType PERIOD =
-      const TokenType('.', 'PERIOD', POSTFIX_PRECEDENCE, PERIOD_TOKEN);
+      const TokenType('.', 'PERIOD', SELECTOR_PRECEDENCE, PERIOD_TOKEN);
 
   static const TokenType PERIOD_PERIOD = const TokenType(
       '..', 'PERIOD_PERIOD', CASCADE_PRECEDENCE, PERIOD_PERIOD_TOKEN,
@@ -1365,7 +1366,7 @@
       isOperator: true);
 
   static const TokenType QUESTION_PERIOD = const TokenType(
-      '?.', 'QUESTION_PERIOD', POSTFIX_PRECEDENCE, QUESTION_PERIOD_TOKEN,
+      '?.', 'QUESTION_PERIOD', SELECTOR_PRECEDENCE, QUESTION_PERIOD_TOKEN,
       isOperator: true);
 
   static const TokenType QUESTION_QUESTION = const TokenType(
@@ -1700,6 +1701,12 @@
       this == TokenType.PLUS_PLUS ||
       this == TokenType.MINUS_MINUS;
 
+  /**
+   * Return `true` if this type of token represents a selector operator
+   * (starting token of a selector).
+   */
+  bool get isSelectorOperator => precedence == SELECTOR_PRECEDENCE;
+
   @override
   String toString() => name;
 
diff --git a/pkg/front_end/test/precedence_info_test.dart b/pkg/front_end/test/precedence_info_test.dart
index 60cb57a..3d6ee26 100644
--- a/pkg/front_end/test/precedence_info_test.dart
+++ b/pkg/front_end/test/precedence_info_test.dart
@@ -216,12 +216,7 @@
   void test_isUnaryPostfixOperator() {
     const unaryPostfixLexemes = const [
       '--',
-      '(',
-      '[',
-      '.',
       '++',
-      '?.',
-      '[]',
     ];
     assertInfo((String source, Token token) {
       expect(token.type.isUnaryPostfixOperator,
@@ -245,6 +240,20 @@
     });
   }
 
+  void test_isSelectorOperator() {
+    const selectorLexemes = const [
+      '(',
+      '[',
+      '.',
+      '?.',
+      '[]',
+    ];
+    assertInfo((String source, Token token) {
+      expect(token.type.isSelectorOperator, selectorLexemes.contains(source),
+          reason: source);
+    });
+  }
+
   void test_isUserDefinableOperator() {
     const userDefinableOperatorLexemes = const [
       '&',
@@ -358,7 +367,8 @@
   /// because it is interpreted as a minus token (precedence 13).
   void test_precedence() {
     const precedenceTable = const <int, List<String>>{
-      16: const <String>['.', '?.', '++', '--', '[', '('],
+      17: const <String>['.', '?.', '[', '('],
+      16: const <String>['++', '--'],
       15: const <String>['!', '~'], // excluded '-', '++', '--'
       14: const <String>['*', '/', '~/', '%'],
       13: const <String>['+', '-'],
diff --git a/pkg/front_end/testcases/regress/issue_31185.dart b/pkg/front_end/testcases/regress/issue_31185.dart
new file mode 100644
index 0000000..568d2af
--- /dev/null
+++ b/pkg/front_end/testcases/regress/issue_31185.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2018, 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.
+
+int i = 5;
+
+int test1() {
+  return i ++ (i);
+}
+
+int test2() {
+  return (i) ++ (i);
+}
+
+main() {
+  test1();
+  // Don't call test2, as error recovery has put a runtime error in there.
+}
diff --git a/pkg/front_end/testcases/regress/issue_31185.dart.direct.expect b/pkg/front_end/testcases/regress/issue_31185.dart.direct.expect
new file mode 100644
index 0000000..bf6678e
--- /dev/null
+++ b/pkg/front_end/testcases/regress/issue_31185.dart.direct.expect
@@ -0,0 +1,23 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+static field core::int i = 5;
+static const field dynamic #errors = const <dynamic>["pkg/front_end/testcases/regress/issue_31185.dart:8:15: Error: Expected ';' before this.
+  return i ++ (i);
+              ^", "pkg/front_end/testcases/regress/issue_31185.dart:12:17: Error: Expected ';' before this.
+  return (i) ++ (i);
+                ^"]/* from null */;
+static method test1() → core::int {
+  return let final dynamic #t1 = self::i in let final dynamic #t2 = self::i = #t1.+(1) in #t1;
+  self::i;
+}
+static method test2() → core::int {
+  return let final dynamic #t3 = self::i in let final dynamic #t4 = #t3 in let final dynamic #t5 = invalid-expression "pkg/front_end/testcases/regress/issue_31185.dart:12:12: Error: Can't assign to a parenthesized expression.
+  return (i) ++ (i);
+           ^" in #t4;
+  self::i;
+}
+static method main() → dynamic {
+  self::test1();
+}
diff --git a/pkg/front_end/testcases/regress/issue_31185.dart.direct.transformed.expect b/pkg/front_end/testcases/regress/issue_31185.dart.direct.transformed.expect
new file mode 100644
index 0000000..bf6678e
--- /dev/null
+++ b/pkg/front_end/testcases/regress/issue_31185.dart.direct.transformed.expect
@@ -0,0 +1,23 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+static field core::int i = 5;
+static const field dynamic #errors = const <dynamic>["pkg/front_end/testcases/regress/issue_31185.dart:8:15: Error: Expected ';' before this.
+  return i ++ (i);
+              ^", "pkg/front_end/testcases/regress/issue_31185.dart:12:17: Error: Expected ';' before this.
+  return (i) ++ (i);
+                ^"]/* from null */;
+static method test1() → core::int {
+  return let final dynamic #t1 = self::i in let final dynamic #t2 = self::i = #t1.+(1) in #t1;
+  self::i;
+}
+static method test2() → core::int {
+  return let final dynamic #t3 = self::i in let final dynamic #t4 = #t3 in let final dynamic #t5 = invalid-expression "pkg/front_end/testcases/regress/issue_31185.dart:12:12: Error: Can't assign to a parenthesized expression.
+  return (i) ++ (i);
+           ^" in #t4;
+  self::i;
+}
+static method main() → dynamic {
+  self::test1();
+}
diff --git a/pkg/front_end/testcases/regress/issue_31185.dart.outline.expect b/pkg/front_end/testcases/regress/issue_31185.dart.outline.expect
new file mode 100644
index 0000000..608d890
--- /dev/null
+++ b/pkg/front_end/testcases/regress/issue_31185.dart.outline.expect
@@ -0,0 +1,11 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+static field core::int i;
+static method test1() → core::int
+  ;
+static method test2() → core::int
+  ;
+static method main() → dynamic
+  ;
diff --git a/pkg/front_end/testcases/regress/issue_31185.dart.strong.expect b/pkg/front_end/testcases/regress/issue_31185.dart.strong.expect
new file mode 100644
index 0000000..a6e3919
--- /dev/null
+++ b/pkg/front_end/testcases/regress/issue_31185.dart.strong.expect
@@ -0,0 +1,23 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+static field core::int i = 5;
+static const field dynamic #errors = const <dynamic>["pkg/front_end/testcases/regress/issue_31185.dart:8:15: Error: Expected ';' before this.
+  return i ++ (i);
+              ^", "pkg/front_end/testcases/regress/issue_31185.dart:12:17: Error: Expected ';' before this.
+  return (i) ++ (i);
+                ^"]/* from null */;
+static method test1() → core::int {
+  return let final core::int #t1 = self::i in let final core::int #t2 = self::i = #t1.{core::num::+}(1) in #t1;
+  self::i;
+}
+static method test2() → core::int {
+  return (let final dynamic #t3 = self::i in let final dynamic #t4 = #t3 in let final dynamic #t5 = invalid-expression "pkg/front_end/testcases/regress/issue_31185.dart:12:12: Error: Can't assign to a parenthesized expression.
+  return (i) ++ (i);
+           ^" in #t4) as{TypeError} core::int;
+  self::i;
+}
+static method main() → dynamic {
+  self::test1();
+}
diff --git a/pkg/front_end/testcases/regress/issue_31185.dart.strong.transformed.expect b/pkg/front_end/testcases/regress/issue_31185.dart.strong.transformed.expect
new file mode 100644
index 0000000..719e1e1
--- /dev/null
+++ b/pkg/front_end/testcases/regress/issue_31185.dart.strong.transformed.expect
@@ -0,0 +1,23 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+static field core::int i = 5;
+static const field dynamic #errors = const <dynamic>["pkg/front_end/testcases/regress/issue_31185.dart:8:15: Error: Expected ';' before this.
+  return i ++ (i);
+              ^", "pkg/front_end/testcases/regress/issue_31185.dart:12:17: Error: Expected ';' before this.
+  return (i) ++ (i);
+                ^"]/* from null */;
+static method test1() → core::int {
+  return let final core::int #t1 = self::i in let final core::int #t2 = self::i = #t1.{core::num::+}(1) in #t1;
+  self::i;
+}
+static method test2() → core::int {
+  return (let final core::int #t3 = self::i in let final core::int #t4 = #t3 in let final dynamic #t5 = invalid-expression "pkg/front_end/testcases/regress/issue_31185.dart:12:12: Error: Can't assign to a parenthesized expression.
+  return (i) ++ (i);
+           ^" in #t4) as{TypeError} core::int;
+  self::i;
+}
+static method main() → dynamic {
+  self::test1();
+}