diff --git a/build/fbcode_builder/getdeps/expr.py b/build/fbcode_builder/getdeps/expr.py new file mode 100644 index 000000000..073706f4c --- /dev/null +++ b/build/fbcode_builder/getdeps/expr.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# Copyright (c) 2019-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +from __future__ import absolute_import, division, print_function, unicode_literals + +import re +import shlex + + +def parse_expr(expr_text): + """ parses the simple criteria expression syntax used in + dependency specifications. + Returns an ExprNode instance that can be evaluated like this: + + ``` + expr = parse_expr("os=windows") + ok = expr.eval({ + "os": "windows" + }) + ``` + + Whitespace is allowed between tokens. The following terms + are recognized: + + KEY = VALUE # Evaluates to True if ctx[KEY] == VALUE + not(EXPR) # Evaluates to True if EXPR evaluates to False + # and vice versa + all(EXPR1, EXPR2, ...) # Evaluates True if all of the supplied + # EXPR's also evaluate True + any(EXPR1, EXPR2, ...) # Evaluates True if any of the supplied + # EXPR's also evaluate True, False if + # none of them evaluated true. + """ + + p = Parser(expr_text) + return p.parse() + + +class ExprNode(object): + def eval(self, ctx): + return False + + +class TrueExpr(ExprNode): + def eval(self, ctx): + return True + + def __str__(self): + return "true" + + +class NotExpr(ExprNode): + def __init__(self, node): + self._node = node + + def eval(self, ctx): + return not self._node.eval(ctx) + + def __str__(self): + return "not(%s)" % self._node + + +class AllExpr(ExprNode): + def __init__(self, nodes): + self._nodes = nodes + + def eval(self, ctx): + for node in self._nodes: + if not node.eval(ctx): + return False + return True + + def __str__(self): + items = [] + for node in self._nodes: + items.append(str(node)) + return "all(%s)" % ",".join(items) + + +class AnyExpr(ExprNode): + def __init__(self, nodes): + self._nodes = nodes + + def eval(self, ctx): + for node in self._nodes: + if node.eval(ctx): + return True + return False + + def __str__(self): + items = [] + for node in self._nodes: + items.append(str(node)) + return "any(%s)" % ",".join(items) + + +class EqualExpr(ExprNode): + def __init__(self, key, value): + self._key = key + self._value = value + + def eval(self, ctx): + return ctx.get(self._key) == self._value + + def __str__(self): + return "%s=%s" % (self._key, self._value) + + +class Parser(object): + def __init__(self, text): + self.text = text + self.lex = shlex.shlex(text) + + def parse(self): + expr = self.top() + garbage = self.lex.get_token() + if garbage != "": + raise Exception( + "Unexpected token %s after EqualExpr in %s" % (garbage, self.text) + ) + return expr + + def top(self): + name = self.ident() + op = self.lex.get_token() + + if op == "(": + parsers = { + "not": self.parse_not, + "any": self.parse_any, + "all": self.parse_all, + } + func = parsers.get(name) + if not func: + raise Exception("invalid term %s in %s" % (name, self.text)) + return func() + + if op == "=": + return EqualExpr(name, self.lex.get_token()) + + raise Exception( + "Unexpected token sequence '%s %s' in %s" % (name, op, self.text) + ) + + def ident(self): + ident = self.lex.get_token() + if not re.match("[a-zA-Z]+", ident): + raise Exception("expected identifier found %s" % ident) + return ident + + def parse_not(self): + node = self.top() + expr = NotExpr(node) + tok = self.lex.get_token() + if tok != ")": + raise Exception("expected ')' found %s" % tok) + return expr + + def parse_any(self): + nodes = [] + while True: + nodes.append(self.top()) + tok = self.lex.get_token() + if tok == ")": + break + if tok != ",": + raise Exception("expected ',' or ')' but found %s" % tok) + return AnyExpr(nodes) + + def parse_all(self): + nodes = [] + while True: + nodes.append(self.top()) + tok = self.lex.get_token() + if tok == ")": + break + if tok != ",": + raise Exception("expected ',' or ')' but found %s" % tok) + return AllExpr(nodes) diff --git a/build/fbcode_builder/getdeps/test/expr_test.py b/build/fbcode_builder/getdeps/test/expr_test.py new file mode 100644 index 000000000..66b7a51b9 --- /dev/null +++ b/build/fbcode_builder/getdeps/test/expr_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# Copyright (c) 2019-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +from __future__ import absolute_import, division, print_function, unicode_literals + +import unittest + +from ..expr import parse_expr + + +class ExprTest(unittest.TestCase): + def test_equal(self): + e = parse_expr("foo=bar") + self.assertTrue(e.eval({"foo": "bar"})) + self.assertFalse(e.eval({"foo": "not-bar"})) + self.assertFalse(e.eval({"not-foo": "bar"})) + + def test_not_equal(self): + e = parse_expr("not(foo=bar)") + self.assertFalse(e.eval({"foo": "bar"})) + self.assertTrue(e.eval({"foo": "not-bar"})) + + def test_bad_not(self): + with self.assertRaises(Exception): + parse_expr("foo=not(bar)") + + def test_all(self): + e = parse_expr("all(foo = bar, baz = qux)") + self.assertTrue(e.eval({"foo": "bar", "baz": "qux"})) + self.assertFalse(e.eval({"foo": "bar", "baz": "nope"})) + self.assertFalse(e.eval({"foo": "nope", "baz": "nope"})) + + def test_any(self): + e = parse_expr("any(foo = bar, baz = qux)") + self.assertTrue(e.eval({"foo": "bar", "baz": "qux"})) + self.assertTrue(e.eval({"foo": "bar", "baz": "nope"})) + self.assertFalse(e.eval({"foo": "nope", "baz": "nope"}))