mirror of
https://github.com/facebook/proxygen.git
synced 2025-08-08 18:02:05 +03:00
fbcode_builder: getdeps: add boolean expression parser
Summary: As part of folding getdeps into fbcode_builder, this expression parser is needed to allow constrained and deterministic conditionals in the manifest file format. For example, the watchman manifest will use this cargo-inspired syntax for system dependent sections: ``` [dependencies] folly [dependencies.not(os=windows)] thrift ``` Reviewed By: sinancepel Differential Revision: D14691014 fbshipit-source-id: 080bcdb20579da40d225799f5f22debe65708b03
This commit is contained in:
committed by
Facebook Github Bot
parent
2db575972c
commit
e3b9199833
184
build/fbcode_builder/getdeps/expr.py
Normal file
184
build/fbcode_builder/getdeps/expr.py
Normal file
@@ -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)
|
42
build/fbcode_builder/getdeps/test/expr_test.py
Normal file
42
build/fbcode_builder/getdeps/test/expr_test.py
Normal file
@@ -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"}))
|
Reference in New Issue
Block a user