Build language: Support 'not in' operator.

--
MOS_MIGRATED_REVID=93129861
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
index b5bf4f5..2a0bd54 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
@@ -70,6 +70,33 @@
     }
   }
 
+  private boolean evalIn(Object lval, Object rval) throws EvalException {
+    if (rval instanceof SkylarkList) {
+      for (Object obj : (SkylarkList) rval) {
+        if (obj.equals(lval)) {
+          return true;
+        }
+      }
+      return false;
+    } else if (rval instanceof Collection<?>) {
+      return ((Collection<?>) rval).contains(lval);
+    } else if (rval instanceof Map<?, ?>) {
+      return ((Map<?, ?>) rval).containsKey(lval);
+    } else if (rval instanceof SkylarkNestedSet) {
+      return ((SkylarkNestedSet) rval).expandedSet().contains(lval);
+    } else if (rval instanceof String) {
+      if (lval instanceof String) {
+        return ((String) rval).contains((String) lval);
+      } else {
+        throw new EvalException(getLocation(),
+            "in operator only works on strings if the left operand is also a string");
+      }
+    } else {
+      throw new EvalException(getLocation(),
+          "in operator only works on lists, tuples, sets, dicts and strings");
+    }
+  }
+
   @Override
   Object eval(Environment env) throws EvalException, InterruptedException {
     Object lval = lhs.eval(env);
@@ -264,30 +291,11 @@
       }
 
       case IN: {
-        if (rval instanceof SkylarkList) {
-          for (Object obj : (SkylarkList) rval) {
-            if (obj.equals(lval)) {
-              return true;
-            }
-          }
-          return false;
-        } else if (rval instanceof Collection<?>) {
-          return ((Collection<?>) rval).contains(lval);
-        } else if (rval instanceof Map<?, ?>) {
-          return ((Map<?, ?>) rval).containsKey(lval);
-        } else if (rval instanceof SkylarkNestedSet) {
-          return ((SkylarkNestedSet) rval).expandedSet().contains(lval);
-        } else if (rval instanceof String) {
-          if (lval instanceof String) {
-            return ((String) rval).contains((String) lval);
-          } else {
-            throw new EvalException(getLocation(),
-                "in operator only works on strings if the left operand is also a string");
-          }
-        } else {
-          throw new EvalException(getLocation(),
-              "in operator only works on lists, tuples, sets, dicts and strings");
-        }
+        return evalIn(lval, rval);
+      }
+
+      case NOT_IN: {
+        return !evalIn(lval, rval);
       }
 
       default: {
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Operator.java b/src/main/java/com/google/devtools/build/lib/syntax/Operator.java
index 8849583..73b37cf 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Operator.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Operator.java
@@ -30,6 +30,7 @@
   MULT("*"),
   NOT("not"),
   NOT_EQUALS("!="),
+  NOT_IN("not in"),
   OR("or"),
   PERCENT("%"),
   PLUS("+");
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
index e25ca98..5c363a6 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
@@ -114,6 +114,7 @@
           .put(TokenKind.LESS_EQUALS, Operator.LESS_EQUALS)
           .put(TokenKind.MINUS, Operator.MINUS)
           .put(TokenKind.NOT_EQUALS, Operator.NOT_EQUALS)
+          .put(TokenKind.NOT_IN, Operator.NOT_IN)
           .put(TokenKind.OR, Operator.OR)
           .put(TokenKind.PERCENT, Operator.PERCENT)
           .put(TokenKind.SLASH, Operator.DIVIDE)
@@ -134,7 +135,7 @@
       EnumSet.of(Operator.AND),
       EnumSet.of(Operator.NOT),
       EnumSet.of(Operator.EQUALS_EQUALS, Operator.NOT_EQUALS, Operator.LESS, Operator.LESS_EQUALS,
-          Operator.GREATER, Operator.GREATER_EQUALS, Operator.IN),
+          Operator.GREATER, Operator.GREATER_EQUALS, Operator.IN, Operator.NOT_IN),
       EnumSet.of(Operator.MINUS, Operator.PLUS),
       EnumSet.of(Operator.DIVIDE, Operator.MULT, Operator.PERCENT));
 
@@ -924,6 +925,15 @@
     // The loop is not strictly needed, but it prevents risks of stack overflow. Depth is
     // limited to number of different precedence levels (operatorPrecedence.size()).
     for (;;) {
+
+      if (token.kind == TokenKind.NOT) {
+        // If NOT appears when we expect a binary operator, it must be followed by IN.
+        // Since the code expects every operator to be a single token, we push a NOT_IN token.
+        expect(TokenKind.NOT);
+        expect(TokenKind.IN);
+        pushToken(new Token(TokenKind.NOT_IN, token.left, token.right));
+      }
+
       if (!binaryOperators.containsKey(token.kind)) {
         return expr;
       }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java b/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java
index 57f5fc4..0ada226 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java
@@ -62,6 +62,7 @@
   NONLOCAL("nonlocal"),
   NOT("not"),
   NOT_EQUALS("!="),
+  NOT_IN("not in"),
   OR("or"),
   OUTDENT("outdent"),
   PASS("pass"),
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
index 8ca1e9e..5c7715f 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
@@ -511,6 +511,19 @@
   }
 
   @Test
+  public void testNotInOperator() throws Exception {
+    assertEquals(Boolean.FALSE, eval("'b' not in ['a', 'b']"));
+    assertEquals(Boolean.TRUE, eval("'c' not in ['a', 'b']"));
+    assertEquals(Boolean.FALSE, eval("'b' not in ('a', 'b')"));
+    assertEquals(Boolean.TRUE, eval("'c' not in ('a', 'b')"));
+    assertEquals(Boolean.FALSE, eval("'b' not in {'a' : 1, 'b' : 2}"));
+    assertEquals(Boolean.TRUE, eval("'c' not in {'a' : 1, 'b' : 2}"));
+    assertEquals(Boolean.TRUE, eval("1 not in {'a' : 1, 'b' : 2}"));
+    assertEquals(Boolean.FALSE, eval("'b' not in 'abc'"));
+    assertEquals(Boolean.TRUE, eval("'d' not in 'abc'"));
+  }
+
+  @Test
   public void testInFail() throws Exception {
     checkEvalError("in operator only works on strings if the left operand is also a string",
         "1 in '123'");
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
index 5f7deb7..613a648 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
@@ -626,7 +626,7 @@
     setFailFast(false);
     List<Statement> statements = parseFileForSkylark(
         "def foo():",
-        "  a = 2 not 4",  // parse error
+        "  a = 2 for 4",  // parse error
         "  b = [3, 4]",
         "",
         "d = 4 ada",  // parse error
@@ -637,7 +637,7 @@
         "");
 
     assertThat(getEventCollector()).hasSize(3);
-    assertContainsEvent("syntax error at 'not': expected newline");
+    assertContainsEvent("syntax error at 'for': expected newline");
     assertContainsEvent("syntax error at 'ada': expected newline");
     assertContainsEvent("syntax error at '+': expected expression");
     assertThat(statements).hasSize(3);
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java
index f25c827..edbf550 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java
@@ -334,7 +334,7 @@
         "  b = [3, 4]",
         "  if a not b:",
         "    print(a)");
-    assertContainsEvent("syntax error at 'not': expected :");
+    assertContainsEvent("syntax error at 'b': expected in");
     // Parser uses "$error" symbol for error recovery.
     // It should not be used in error messages.
     for (Event event : getEventCollector()) {