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()) {