Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/main/grammar/Rego.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
CONTAINS = 'contains'
EVERY = 'every'
IN = 'in'
AND = 'and'
OR = 'or'
LBRACE = '{'
RBRACE = '}'
SEMICOLON = ';'
Expand Down Expand Up @@ -79,7 +81,19 @@ rule-body ::= else-expr | query-block
else-expr ::= else (':=' | '=') expr "if"? query-block? | else (':=' | '=')? "if"? query-block
query-block ::= "{" query "}"
query ::= ( literal |';' )+
literal ::= ( every-decl | some-in-decl | some-decl | literal-expr | "not" literal-expr ) with-modifier*
literal ::= or-expr with-modifier*
// `and` binds tighter than `or`, and both bind looser than `not`.
// Operands are either a single expression (`a or b`) or an explicit body (`{ a } or { b; c }`).
private or-expr ::= and-expr ( "or" and-rhs )*
private and-expr ::= operand ( "and" rhs-operand )*
private and-rhs ::= rhs-operand ( "and" rhs-operand )*
private operand ::= every-decl | some-in-decl | some-decl | lhs-body | literal-expr | "not" not-operand | paren-group
private rhs-operand ::= query-block | literal-expr | "not" not-operand | paren-group
// `{...}` only opens a body here when an `and`/`or` follows it, otherwise it is a set or an object,
// and a rule body must stay available to `rule-body`.
private lhs-body ::= &( query-block ( "and" | "or" ) ) query-block
private not-operand ::= literal-expr | paren-group
private paren-group ::= "(" or-expr with-modifier* ")"
with-modifier ::= "with" term "as" term
some-in-decl ::= "some" var ( "," var )? "in" expr
some-decl ::= "some" var ( "," var )*
Expand All @@ -101,8 +115,8 @@ bin-operator ::= "&" | "|"
ref ::= ( expr-call | array | object | set | array-compr | object-compr | set-compr |var ) ref-arg*
ref-arg ::= ref-arg-dot | ref-arg-brack
ref-arg-brack ::= "[" ( expr | "_" ) "]"
ref-arg-dot ::= "." var
var ::= ASCII_LETTER | "contains" | "in" | "every" | "if"
ref-arg-dot ::= "." ( var | "not" )
var ::= ASCII_LETTER | "contains" | "in" | "every" | "if" | "and" | "or"
scalar ::= string | NUMBER | TRUE | FALSE | NULL
string ::= STRING_TOKEN| RAW_STRING
array ::= '[' expr? ( ',' expr )* ','? ']'
Expand Down
2 changes: 2 additions & 0 deletions src/main/grammar/RegoLexer.flex
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ COMMENT=[ \t]*#[^\r\n]*
"some" { return SOME; }
"every" { return EVERY; }
"in" { return IN; }
"and" { return AND; }
"or" { return OR; }
"contains" { return CONTAINS; }
"," { return COMMA; }
"." { return DOT; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ class RegoHighlighterAnnotator : AnnotatorBase() {
is RegoEmptySet -> Pair(RegoColor.CALL, element.textRange)

is RegoExprCall -> {
val varlist = element.refArgDotList
val textRange = if (varlist.size >= 1) varlist[varlist.size - 1].`var`.textRange else element.`var`.textRange
// a ref-arg-dot holds no var when it is a keyword, as in `data.foo.not`,
// in which case the call name falls back to the root var
val textRange = element.refArgDotList.lastOrNull()?.`var`?.textRange ?: element.`var`.textRange
Pair(RegoColor.CALL, textRange)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ val REGO_KEYWORDS = tokenSetOf(
RegoTypes.IF,
RegoTypes.CONTAINS,
RegoTypes.EVERY,
RegoTypes.IN
RegoTypes.IN,
RegoTypes.AND,
RegoTypes.OR
)

val REGO_OPERATOR = tokenSetOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ class RegoParsingV1Test : ParsingTestCase(
fun `test in operator`() = doTestNoError()
fun `test some in`() = doTestNoError()
fun `test keywords as identifiers`() = doTestNoError()
fun `test logical and or`() = doTestNoError()
fun `test logical grouping`() = doTestNoError()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ if := 1

every := 2

and := 3

or := 4

uses_if if {
input.if == 1
}
Expand All @@ -12,6 +16,18 @@ uses_every if {
input.every.woo != null
}

uses_and if {
input.and == 1
}

uses_or if {
input.or.woo != null
}

check_if(if) := true

check_every(every, foo) := every + foo

check_and(and) := true

check_or(or, foo) := or + foo
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package play

import future.keywords.and
import future.keywords.or

allow if {
input.method == "GET" or input.method == "HEAD"
}

deny if {
input.role == "guest" and startswith(input.path, "/admin")
}

chained if {
input.a or input.b or input.c
}

# "and" binds tighter than "or"
mixed if {
input.a == 1 and input.b == 2 or input.c == 3
}

# "not" binds tighter than "and"
negated if {
not input.banned and input.active
}

membership if {
"admin" in input.roles or "root" in input.roles
}

# a trailing "with" applies to the whole expression
audited if {
data.acl.allow and data.acl.audit with input as {"user": "alice"}
}

one_line if input.x == 1 or input.y == 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package play

import future.keywords.and
import future.keywords.not
import future.keywords.or

# explicit bodies as operands
allow if {
{input.user == "alice"} or {input.role == "admin"; input.active}
}

leading_body if {
{input.a; input.b} and input.c
}

# parentheses override the precedence of "and" over "or"
grouped if {
(input.a or input.b) and input.c
}

nested if {
((input.a or input.b) and input.c) or input.d
}

group_with if {
(input.a and input.b with input.x as 1)
}

negated_group if {
input.c and not (input.a or input.b)
}
Loading