From f55a597bd5f3efad5027e280ee5f12b938589603 Mon Sep 17 00:00:00 2001 From: Sebastian Spaink Date: Tue, 11 Aug 2026 11:55:50 -0500 Subject: [PATCH] grammar: Update to support and and or keywords Adds the `and` and `or` logical keywords from open-policy-agent/opa#8999 to the lexer and grammar, following the precedence defined in open-policy-agent/opa#8677: `not` > `and` > `or` > `with`. Operands can be a single expression (`a or b`), an explicit body (`{a} or {b; c}`) or a parenthesized group (`(a or b) and c`). A `{...}` operand is only read as a body when an `and`/`or` follows it, so sets, objects and rule bodies keep parsing as before. A trailing `with` applies to the whole expression. Both keywords are also accepted as identifiers, as they remain identifiers in policies that do not import them, and `not` is accepted after a dot in a ref so that `import future.keywords.not` parses. Signed-off-by: Sebastian Spaink --- src/main/grammar/Rego.bnf | 20 ++++++++-- src/main/grammar/RegoLexer.flex | 2 + .../ide/highlight/RegoHighlighterAnnotator.kt | 5 ++- .../ideaplugin/lang/psi/RegoTokenType.kt | 4 +- .../ideaplugin/lang/RegoParsingV1Test.kt | 2 + .../fixtures/v1/keywords_as_identifiers.rego | 16 ++++++++ .../parser/fixtures/v1/logical_and_or.rego | 37 +++++++++++++++++++ .../parser/fixtures/v1/logical_grouping.rego | 31 ++++++++++++++++ 8 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/logical_and_or.rego create mode 100644 src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/logical_grouping.rego diff --git a/src/main/grammar/Rego.bnf b/src/main/grammar/Rego.bnf index ab5fb42..964da3a 100644 --- a/src/main/grammar/Rego.bnf +++ b/src/main/grammar/Rego.bnf @@ -35,6 +35,8 @@ CONTAINS = 'contains' EVERY = 'every' IN = 'in' + AND = 'and' + OR = 'or' LBRACE = '{' RBRACE = '}' SEMICOLON = ';' @@ -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 )* @@ -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 )* ','? ']' diff --git a/src/main/grammar/RegoLexer.flex b/src/main/grammar/RegoLexer.flex index d4dbed9..d1f152d 100644 --- a/src/main/grammar/RegoLexer.flex +++ b/src/main/grammar/RegoLexer.flex @@ -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; } diff --git a/src/main/kotlin/org/openpolicyagent/ideaplugin/ide/highlight/RegoHighlighterAnnotator.kt b/src/main/kotlin/org/openpolicyagent/ideaplugin/ide/highlight/RegoHighlighterAnnotator.kt index b3e3b04..9490976 100644 --- a/src/main/kotlin/org/openpolicyagent/ideaplugin/ide/highlight/RegoHighlighterAnnotator.kt +++ b/src/main/kotlin/org/openpolicyagent/ideaplugin/ide/highlight/RegoHighlighterAnnotator.kt @@ -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) } diff --git a/src/main/kotlin/org/openpolicyagent/ideaplugin/lang/psi/RegoTokenType.kt b/src/main/kotlin/org/openpolicyagent/ideaplugin/lang/psi/RegoTokenType.kt index 9d2057f..f8e075c 100644 --- a/src/main/kotlin/org/openpolicyagent/ideaplugin/lang/psi/RegoTokenType.kt +++ b/src/main/kotlin/org/openpolicyagent/ideaplugin/lang/psi/RegoTokenType.kt @@ -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( diff --git a/src/test/kotlin/org/openpolicyagent/ideaplugin/lang/RegoParsingV1Test.kt b/src/test/kotlin/org/openpolicyagent/ideaplugin/lang/RegoParsingV1Test.kt index a69f35b..aa14a65 100644 --- a/src/test/kotlin/org/openpolicyagent/ideaplugin/lang/RegoParsingV1Test.kt +++ b/src/test/kotlin/org/openpolicyagent/ideaplugin/lang/RegoParsingV1Test.kt @@ -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() } diff --git a/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/keywords_as_identifiers.rego b/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/keywords_as_identifiers.rego index 613c3f0..e18afbe 100644 --- a/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/keywords_as_identifiers.rego +++ b/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/keywords_as_identifiers.rego @@ -4,6 +4,10 @@ if := 1 every := 2 +and := 3 + +or := 4 + uses_if if { input.if == 1 } @@ -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 diff --git a/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/logical_and_or.rego b/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/logical_and_or.rego new file mode 100644 index 0000000..4c4903e --- /dev/null +++ b/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/logical_and_or.rego @@ -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 diff --git a/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/logical_grouping.rego b/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/logical_grouping.rego new file mode 100644 index 0000000..ce4c6f0 --- /dev/null +++ b/src/test/resources/org/openpolicyagent/ideaplugin/lang/parser/fixtures/v1/logical_grouping.rego @@ -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) +}