Back to Opa

prefer-some-in-iteration

docs/projects/regal/rules/style/prefer-some-in-iteration.md

1.16.14.3 KB
Original Source

prefer-some-in-iteration

Summary: Prefer some .. in for iteration

Category: Style

Avoid

rego
package policy

engineering_roles := {"engineer", "dba", "developer"}

engineers contains employee if {
    employee := data.employees[_]
    employee.role in engineering_roles
}

Prefer

rego
package policy

engineering_roles := {"engineer", "dba", "developer"}

engineers contains employee if {
    some employee in data.employees
    employee.role in engineering_roles
}

Rationale

Using the some .. in construct for iteration removes ambiguity around iteration vs. membership checks, and is generally more pleasant to read. Consider the following example:

rego
some_condition if {
    other_rule[user]
    # ...
}

Are we iterating users over a partial "other_rule" here, or checking if the set contains a user defined elsewhere? Or is other_rule a map-generating rule, and we're checking for the existence of a key? We won't know without looking elsewhere in the code. Using some .. in removes this ambiguity, and makes the intent clear without having to jump around in the policy.

Improved readability is not the only benefit of using some .. in. The some keyword ensures that the bindings following the keyword are bound to the local scope, and modifications outside of e.g. a rule body won't affect how the variables are evaluated. Consider the following simplified example to iterate over the keys of a map:

rego
package policy

key_traversal if {
    map[key]
    # do something with key
}


key_traversal if {
    some key in object.keys(map)
    # do something with key
}

The two rules above are equivalent in that they both bind the variable key to the keys of map. The first example would however change behavior entirely if a rule named key was introduced in the package, as the expression would then mean "does map have key key?". While this isn't common, using some .. in means one less thing to worry about.

Exceptions

Deeply nested iteration is often easier to read using the more compact form.

rego
package policy

# These rules are equivalent, but the more compact form is arguably easier to read

any_user_is_admin if {
    some user in input.users
    some attribute in user.attributes
    some role in attribute.roles
    role == "admin"
}

any_user_is_admin if {
    input.users[_].attributes[_].roles[_] == "admin"
}

# Using "if", we may even omit the brackets for single line rules
any_user_is_admin if input.users[_].attributes[_].roles[_] == "admin"

The ignore-nesting-level configuration option allows setting the threshold for nesting. Any level of nesting equal or greater than the threshold won't be considered a violation. The default setting of 2 allows all nested iteration, but not e.g. my_array[x].

Note: not all nesting is iteration! The following example is considered to have a nesting level of 1, as only one of the variables (including wildcards: _) is an output variable bound in iteration:

rego
package policy

example_users contains user if {
    domain := "example.com"
    user := input.sites[domain].users[_]
}

Configuration Options

This linter rule provides the following configuration options:

yaml
rules:
  style:
    prefer-some-in-iteration:
      # one of "error", "warning", "ignore"
      level: error
      # except iteration if nested at or above the level i.e. setting of
      # '2' will allow `input[_].users[_]` but not `input[_]`
      ignore-nesting-level: 2
      # except iteration over items with sub-attributes, like
      # `name := input.users[_].name`
      # default is true
      ignore-if-sub-attribute: true