Skip to content

Commit

Permalink
Better default suggestions (#848)
Browse files Browse the repository at this point in the history
And provided by Rego policy! 🎉

Also moved some things around in our `ast` package, as we were getting hit
by the file-length rule. All good!

Fixes #838

Signed-off-by: Anders Eknert <[email protected]>
  • Loading branch information
anderseknert committed Jun 18, 2024
1 parent 1edfd88 commit 1191c41
Show file tree
Hide file tree
Showing 12 changed files with 464 additions and 370 deletions.
225 changes: 0 additions & 225 deletions bundle/regal/ast/ast.rego
Original file line number Diff line number Diff line change
Expand Up @@ -90,236 +90,11 @@ identifiers := rule_and_function_names | imported_identifiers

rule_names contains ref_to_string(rule.head.ref) if some rule in rules

# METADATA
# description: parse provided snippet with a generic package declaration added
policy(snippet) := regal.parse_module("policy.rego", concat("", [
"package policy\n\n",
snippet,
]))

# METADATA
# description: parses provided policy with all future keywords imported. Primarily for testing.
with_rego_v1(policy) := regal.parse_module("policy.rego", concat("", [
`package policy
import rego.v1
`,
policy,
]))

_find_nested_vars(obj) := [value |
walk(obj, [_, value])
value.type == "var"
indexof(value.value, "$") == -1
]

# simple assignment, i.e. `x := 100` returns `x`
# always returns a single var, but wrapped in an
# array for consistency
_find_assign_vars(_, value) := var if {
value[1].type == "var"
var := [value[1]]
}

# 'destructuring' array assignment, i.e.
# [a, b, c] := [1, 2, 3]
# or
# {a: b} := {"foo": "bar"}
_find_assign_vars(_, value) := var if {
value[1].type in {"array", "object"}
var := _find_nested_vars(value[1])
}

# var declared via `some`, i.e. `some x` or `some x, y`
_find_some_decl_vars(_, value) := [v |
some v in value
v.type == "var"
]

# single var declared via `some in`, i.e. `some x in y`
_find_some_in_decl_vars(_, value) := var if {
arr := value[0].value
count(arr) == 3

var := _find_nested_vars(arr[1])
}

# two vars declared via `some in`, i.e. `some x, y in z`
_find_some_in_decl_vars(_, value) := var if {
arr := value[0].value
count(arr) == 4

var := [v |
some i in [1, 2]
some v in _find_nested_vars(arr[i])
]
}

# find vars like input[x].foo[y] where x and y are vars
# note: value.type == "ref" check must have been done before calling this function
find_ref_vars(value) := [var |
some i, var in value.value

# ignore first element, as it is the base ref (like input or data)
i > 0
var.type == "var"
]

# one or two vars declared via `every`, i.e. `every x in y {}`
# or `every`, i.e. `every x, y in y {}`
_find_every_vars(_, value) := var if {
key_var := [v | v := value.key; v.type == "var"; indexof(v.value, "$") == -1]
val_var := [v | v := value.value; v.type == "var"; indexof(v.value, "$") == -1]

var := array.concat(key_var, val_var)
}

# METADATA
# description: |
# traverses all nodes in provided term (using `walk`), and returns an array with
# all variables declared in term, i,e [x, y] or {x: y}, etc.
find_term_vars(term) := [value |
walk(term, [_, value])

value.type == "var"
]

_find_set_or_array_comprehension_vars(value) := [value.value.term] if {
value.value.term.type == "var"
} else := find_term_vars(value.value.term)

_find_object_comprehension_vars(value) := array.concat(key, val) if {
key := [value.value.key | value.value.key.type == "var"]
val := [value.value.value | value.value.value.type == "var"]
}

_find_vars(_, value, last) := find_term_vars(function_ret_args(fn_name, value)) if {
last == "terms"
value[0].type == "ref"
value[0].value[0].type == "var"
value[0].value[0].value != "assign"

fn_name := ref_to_string(value[0].value)

not contains(fn_name, "$")
fn_name in all_function_names # regal ignore:external-reference
function_ret_in_args(fn_name, value)
}

_find_vars(path, value, last) := _find_assign_vars(path, value) if {
last == "terms"
value[0].type == "ref"
value[0].value[0].type == "var"
value[0].value[0].value == "assign"
}

# `=` isn't necessarily assignment, and only considering the variable on the
# left-hand side is equally dubious, but we'll treat `x = 1` as `x := 1` for
# the purpose of this function until we have a more robust way of dealing with
# unification
_find_vars(path, value, last) := _find_assign_vars(path, value) if {
last == "terms"
value[0].type == "ref"
value[0].value[0].type == "var"
value[0].value[0].value == "eq"
}

_find_vars(_, value, _) := find_ref_vars(value) if value.type == "ref"

_find_vars(path, value, last) := _find_some_in_decl_vars(path, value) if {
last == "symbols"
value[0].type == "call"
}

_find_vars(path, value, last) := _find_some_decl_vars(path, value) if {
last == "symbols"
value[0].type != "call"
}

_find_vars(path, value, last) := _find_every_vars(path, value) if {
last == "terms"
value.domain
}

_find_vars(_, value, _) := _find_set_or_array_comprehension_vars(value) if {
value.type in {"setcomprehension", "arraycomprehension"}
}

_find_vars(_, value, _) := _find_object_comprehension_vars(value) if value.type == "objectcomprehension"

find_some_decl_vars(rule) := [var |
walk(rule, [path, value])

regal.last(path) == "symbols"
value[0].type != "call"

some var in _find_some_decl_vars(path, value)
]

# METADATA
# description: |
# traverses all nodes under provided node (using `walk`), and returns an array with
# all variables declared via assignment (:=), `some`, `every` and in comprehensions
find_vars(node) := [var |
walk(node, [path, value])

some var in _find_vars(path, value, regal.last(path))
]

_function_arg_names(rule) := {arg.value |
some arg in rule.head.args
arg.type == "var"
}

# METADATA
# description: |
# finds all vars declared in `rule` *before* the `location` provided
# note: this isn't 100% accurate, as it doesn't take into account `=`
# assignments / unification, but it's likely good enough since other rules
# recommend against those
find_vars_in_local_scope(rule, location) := [var |
some var in find_vars(rule)
not startswith(var.value, "$")
_before_location(var, location)
]

_before_location(var, location) if var.location.row < location.row

_before_location(var, location) if {
var.location.row == location.row
var.location.col < location.col
}

# METADATA
# description: find *only* names in the local scope, and not e.g. rule names
find_names_in_local_scope(rule, location) := names if {
fn_arg_names := _function_arg_names(rule)
var_names := {var.value | some var in find_vars_in_local_scope(rule, location)}

names := fn_arg_names | var_names
}

# METADATA
# description: |
# similar to `find_vars_in_local_scope`, but returns all variable names in scope
# of the given location *and* the rule names present in the scope (i.e. module)
find_names_in_scope(rule, location) := names if {
locals := find_names_in_local_scope(rule, location)

# parens below added by opa-fmt :)
names := (rule_names | imported_identifiers) | locals
}

# METADATA
# description: |
# find all variables declared via `some` declarations (and *not* `some .. in`)
# in the scope of the given location
find_some_decl_names_in_scope(rule, location) := {some_var.value |
some some_var in find_some_decl_vars(rule)
_before_location(some_var, location)
}

# METADATA
# description: |
# determine if var in ref (e.g. `x` in `input[x]`) is used as input or output
Expand Down
Loading

0 comments on commit 1191c41

Please sign in to comment.