Commit 5aab1c26 authored by Wez Furlong's avatar Wez Furlong Committed by Facebook Github Bot

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
parent 43e2ad6e
#!/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)
#!/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"}))
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment