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