Skip to content

Commit

Permalink
Add idiomatic category
Browse files Browse the repository at this point in the history
And two new rules:
- custom-has-key-construct
- custom-in-construct

Signed-off-by: Anders Eknert <anders@styra.com>
  • Loading branch information
anderseknert committed Jun 21, 2023
1 parent b57ac0e commit 2d3d89c
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 28 deletions.
57 changes: 29 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,34 +130,35 @@ The following rules are currently available:

<!-- RULES_TABLE_START -->

| Category | Title | Description |
|----------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| bugs | [constant-condition](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/constant-condition.md) | Constant condition |
| bugs | [top-level-iteration](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/top-level-iteration.md) | Iteration in top-level assignment |
| bugs | [unused-return-value](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/unused-return-value.md) | Non-boolean return value unused |
| bugs | [not-equals-in-loop](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/not-equals-in-loop.md) | Use of != in loop |
| bugs | [rule-shadows-builtin](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/rule-shadows-builtin.md) | Rule name shadows built-in |
| bugs | [rule-named-if](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/rule-named-if.md) | Rule named "if" |
| imports | [implicit-future-keywords](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/implicit-future-keywords.md) | Use explicit future keyword imports |
| imports | [avoid-importing-input](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/avoid-importing-input.md) | Avoid importing input |
| imports | [redundant-data-import](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/redundant-data-import.md) | Redundant import of data |
| imports | [import-shadows-import](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/import-shadows-import.md) | Import shadows another import |
| imports | [redundant-alias](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/redundant-alias.md) | Redundant alias |
| style | [use-in-operator](https://github.com/StyraInc/regal/blob/main/docs/rules/style/use-in-operator.md) | Use in to check for membership |
| style | [unconditional-assignment](https://github.com/StyraInc/regal/blob/main/docs/rules/style/unconditional-assignment.md) | Unconditional assignment in rule body |
| style | [line-length](https://github.com/StyraInc/regal/blob/main/docs/rules/style/line-length.md) | Line too long |
| style | [use-assignment-operator](https://github.com/StyraInc/regal/blob/main/docs/rules/style/use-assignment-operator.md) | Prefer := over = for assignment |
| style | [todo-comment](https://github.com/StyraInc/regal/blob/main/docs/rules/style/todo-comment.md) | Avoid TODO comments |
| style | [external-reference](https://github.com/StyraInc/regal/blob/main/docs/rules/style/external-reference.md) | Reference to input, data or rule ref in function body |
| style | [avoid-get-and-list-prefix](https://github.com/StyraInc/regal/blob/main/docs/rules/style/avoid-get-and-list-prefix.md) | Avoid get_ and list_ prefix for rules and functions |
| style | [prefer-snake-case](https://github.com/StyraInc/regal/blob/main/docs/rules/style/prefer-snake-case.md) | Prefer snake_case for names |
| style | [function-arg-return](https://github.com/StyraInc/regal/blob/main/docs/rules/style/function-arg-return.md) | Function argument used for return value |
| style | [opa-fmt](https://github.com/StyraInc/regal/blob/main/docs/rules/style/opa-fmt.md) | File should be formatted with `opa fmt` |
| testing | [file-missing-test-suffix](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/file-missing-test-suffix.md) | Files containing tests should have a _test.rego suffix |
| testing | [identically-named-tests](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/identically-named-tests.md) | Multiple tests with same name |
| testing | [todo-test](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/todo-test.md) | TODO test encountered |
| testing | [print-or-trace-call](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/print-or-trace-call.md) | Call to print or trace function |
| testing | [test-outside-test-package](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/test-outside-test-package.md) | Test outside of test package |
| Category | Title | Description |
|-----------|--------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| bugs | [constant-condition](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/constant-condition.md) | Constant condition |
| bugs | [not-equals-in-loop](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/not-equals-in-loop.md) | Use of != in loop |
| bugs | [rule-named-if](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/rule-named-if.md) | Rule named "if" |
| bugs | [rule-shadows-builtin](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/rule-shadows-builtin.md) | Rule name shadows built-in |
| bugs | [top-level-iteration](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/top-level-iteration.md) | Iteration in top-level assignment |
| bugs | [unused-return-value](https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/unused-return-value.md) | Non-boolean return value unused |
| idiomatic | [custom-has-key-construct](https://github.com/StyraInc/regal/blob/main/docs/rules/idiomatic/custom-has-key-construct.md) | Custom function may be replaced by `in` and `object.keys` |
| idiomatic | [custom-in-construct](https://github.com/StyraInc/regal/blob/main/docs/rules/idiomatic/custom-in-construct.md) | Custom function may be replaced by `in` keyword |
| imports | [redundant-data-import](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/redundant-data-import.md) | Redundant import of data |
| imports | [implicit-future-keywords](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/implicit-future-keywords.md) | Use explicit future keyword imports |
| imports | [import-shadows-import](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/import-shadows-import.md) | Import shadows another import |
| imports | [redundant-alias](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/redundant-alias.md) | Redundant alias |
| imports | [avoid-importing-input](https://github.com/StyraInc/regal/blob/main/docs/rules/imports/avoid-importing-input.md) | Avoid importing input |
| style | [prefer-snake-case](https://github.com/StyraInc/regal/blob/main/docs/rules/style/prefer-snake-case.md) | Prefer snake_case for names |
| style | [use-assignment-operator](https://github.com/StyraInc/regal/blob/main/docs/rules/style/use-assignment-operator.md) | Prefer := over = for assignment |
| style | [function-arg-return](https://github.com/StyraInc/regal/blob/main/docs/rules/style/function-arg-return.md) | Function argument used for return value |
| style | [line-length](https://github.com/StyraInc/regal/blob/main/docs/rules/style/line-length.md) | Line too long |
| style | [avoid-get-and-list-prefix](https://github.com/StyraInc/regal/blob/main/docs/rules/style/avoid-get-and-list-prefix.md) | Avoid get_ and list_ prefix for rules and functions |
| style | [todo-comment](https://github.com/StyraInc/regal/blob/main/docs/rules/style/todo-comment.md) | Avoid TODO comments |
| style | [unconditional-assignment](https://github.com/StyraInc/regal/blob/main/docs/rules/style/unconditional-assignment.md) | Unconditional assignment in rule body |
| style | [external-reference](https://github.com/StyraInc/regal/blob/main/docs/rules/style/external-reference.md) | Reference to input, data or rule ref in function body |
| style | [use-in-operator](https://github.com/StyraInc/regal/blob/main/docs/rules/style/use-in-operator.md) | Use in to check for membership |
| style | [opa-fmt](https://github.com/StyraInc/regal/blob/main/docs/rules/style/opa-fmt.md) | File should be formatted with `opa fmt` |
| testing | [identically-named-tests](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/identically-named-tests.md) | Multiple tests with same name |
| testing | [print-or-trace-call](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/print-or-trace-call.md) | Call to print or trace function |
| testing | [test-outside-test-package](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/test-outside-test-package.md) | Test outside of test package |
| testing | [file-missing-test-suffix](https://github.com/StyraInc/regal/blob/main/docs/rules/testing/file-missing-test-suffix.md) | Files containing tests should have a _test.rego suffix |

<!-- RULES_TABLE_END -->

Expand Down
13 changes: 13 additions & 0 deletions bundle/regal/rules/idiomatic/common_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package regal.rules.idiomatic.common_test

import future.keywords.if

import data.regal.ast
import data.regal.config
import data.regal.rules.idiomatic

report(snippet) := report if {
# regal ignore:external-reference
report := idiomatic.report with input as ast.policy(snippet)
with config.for_rule as {"level": "error"}
}
56 changes: 56 additions & 0 deletions bundle/regal/rules/idiomatic/custom_has_key_construct.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package regal.rules.idiomatic

import future.keywords.contains
import future.keywords.if
import future.keywords.in

import data.regal.ast
import data.regal.config
import data.regal.result

# METADATA
# title: custom-has-key-construct
# description: Custom function may be replaced by `in` and `object.keys`
# related_resources:
# - description: documentation
# ref: $baseUrl/$category/custom-has-key-construct
# custom:
# category: idiomatic
report contains violation if {
config.for_rule(rego.metadata.rule()).level != "ignore"

some rule in input.rules
rule.head.args

arg_names := [arg.value | some arg in rule.head.args]

count(rule.body) == 1

terms := rule.body[0].terms

terms[0].value[0].type == "var"
terms[0].value[0].value == "eq"

[var, ref] := normalize_eq_terms(terms)

ref.value[0].type == "var"
ref.value[0].value in arg_names
ref.value[1].type == "var"
ref.value[1].value in arg_names

violation := result.fail(rego.metadata.rule(), result.location(rule.head))
}

# METADATA
# description: Normalize var to always always be on the left hand side
normalize_eq_terms(terms) := [terms[1], terms[2]] if {
terms[1].type == "var"
terms[1].value == "$0"
terms[2].type == "ref"
}

normalize_eq_terms(terms) := [terms[2], terms[1]] if {
terms[1].type == "ref"
terms[2].type == "var"
terms[2].value == "$0"
}
25 changes: 25 additions & 0 deletions bundle/regal/rules/idiomatic/custom_has_key_construct_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package regal.rules.idiomatic_test

import future.keywords.if

import data.regal.ast
import data.regal.config
import data.regal.rules.idiomatic
import data.regal.rules.idiomatic.common_test.report

test_fail_unnecessary_construct_in if {
r := report(`has_key(name, coll) {
_ = coll[name]
}`)
r == {{
"category": "idiomatic",
"description": "Custom function may be replaced by `in` and `object.keys`",
"level": "error",
"location": {"col": 1, "file": "policy.rego", "row": 3, "text": "has_key(name, coll) {"},
"related_resources": [{
"description": "documentation",
"ref": config.docs.resolve_url("$baseUrl/$category/custom-has-key-construct", "idiomatic"),
}],
"title": "custom-has-key-construct",
}}
}
58 changes: 58 additions & 0 deletions bundle/regal/rules/idiomatic/custom_in_construct.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package regal.rules.idiomatic

import future.keywords.contains
import future.keywords.if
import future.keywords.in

import data.regal.ast
import data.regal.config
import data.regal.result

# METADATA
# title: custom-in-construct
# description: Custom function may be replaced by `in` keyword
# related_resources:
# - description: documentation
# ref: $baseUrl/$category/custom-in-construct
# custom:
# category: idiomatic
report contains violation if {
config.for_rule(rego.metadata.rule()).level != "ignore"

some rule in input.rules
rule.head.args

arg_names := [arg.value | some arg in rule.head.args]

# while there could be more convoluted ways of doing this
# we'll settle for the likely most common case (`item == coll[_]`)
count(rule.body) == 1

terms := rule.body[0].terms

terms[0].value[0].type == "var"
terms[0].value[0].value in {"eq", "equal"}

[var, ref] := normalize_eq_terms(terms)

var.value in arg_names
ref.value[0].value in arg_names
ref.value[1].type == "var"
ref.value[1].value == "$0"

violation := result.fail(rego.metadata.rule(), result.location(rule.head))
}

# METADATA
# description: Normalize var to always always be on the left hand side
normalize_eq_terms(terms) := [terms[1], terms[2]] if {
terms[1].type == "var"
terms[2].type == "ref"
terms[2].value[0].type == "var"
}

normalize_eq_terms(terms) := [terms[2], terms[1]] if {
terms[1].type == "ref"
terms[1].value[0].type == "var"
terms[2].type == "var"
}
40 changes: 40 additions & 0 deletions bundle/regal/rules/idiomatic/custom_in_construct_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package regal.rules.idiomatic_test

import future.keywords.if

import data.regal.ast
import data.regal.config
import data.regal.rules.idiomatic
import data.regal.rules.idiomatic.common_test.report

test_fail_unnecessary_construct_in if {
r := report(`has(item, coll) {
item == coll[_]
}`)
r == {{
"category": "idiomatic",
"description": "Custom function may be replaced by `in` keyword",
"level": "error",
"location": {"col": 1, "file": "policy.rego", "row": 3, "text": "has(item, coll) {"},
"related_resources": [{
"description": "documentation",
"ref": config.docs.resolve_url("$baseUrl/$category/custom-in-construct", "idiomatic"),
}],
"title": "custom-in-construct",
}}
}

test_fail_unnecessary_construct_in_reversed if {
r := report(`has(item, coll) { coll[_] == item }`)
r == {{
"category": "idiomatic",
"description": "Custom function may be replaced by `in` keyword",
"level": "error",
"location": {"col": 1, "file": "policy.rego", "row": 3, "text": "has(item, coll) { coll[_] == item }"},
"related_resources": [{
"description": "documentation",
"ref": config.docs.resolve_url("$baseUrl/$category/custom-in-construct", "idiomatic"),
}],
"title": "custom-in-construct",
}}
}
47 changes: 47 additions & 0 deletions docs/rules/idiomatic/custom-has-key-construct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# custom-has-key-construct

**Summary**: Custom function may be replaced by `in` and `object.keys`

**Category**: Idiomatic

**Avoid**
```rego
package policy
import future.keywords.if
mfa if has_key(input.claims, "mfa")
has_key(map, key) if {
_ = map[key]
}
```

**Prefer**
```rego
package policy
import future.keywords.if
import future.keywords.in
allow if "admin" in input.user.roles
```

## Rationale

The `in` keyword was introduced in OPA version X.XX. Prior to that, it was a common practice to create a custom helper
function that would iterate over values of an array in order to check if it contained a provided value. Since the
introduction of the `in` keyword, this is no longer necessary. The `in` keyword additionally supports sets and maps as
the collection type, so using it consistently is recommended.

## Configuration Options

This linter rule provides the following configuration options:

```yaml
rules:
idiomatic:
custom-in-construct:
# one of "error", "warning", "ignore"
level: error
```
49 changes: 49 additions & 0 deletions docs/rules/idiomatic/custom-in-construct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# custom-in-construct

**Summary**: Custom function may be replaced by `in` keyword

**Category**: Idiomatic

**Avoid**
```rego
package policy
import future.keywords.if
allow if has_value(input.user.roles, "admin")
# This custom function was commonly seen before the introduction
# of the `in` keyword. Avoid it now.
has_value(arr, item) if {
item == arr[_]
}
```

**Prefer**
```rego
package policy
import future.keywords.if
import future.keywords.in
allow if "admin" in input.user.roles
```

## Rationale

The `in` keyword was introduced in OPA version X.XX. Prior to that, it was a common practice to create a custom helper
function that would iterate over values of an array in order to check if it contained a provided value. Since the
introduction of the `in` keyword, this is no longer necessary. The `in` keyword additionally supports sets and maps as
the collection type, so using it consistently is recommended.

## Configuration Options

This linter rule provides the following configuration options:

```yaml
rules:
idiomatic:
custom-in-construct:
# one of "error", "warning", "ignore"
level: error
```

0 comments on commit 2d3d89c

Please sign in to comment.