Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rule: inconsistent-args #448

Merged
merged 1 commit into from
Nov 8, 2023
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ The following rules are currently available:
| Category | Title | Description |
|-----------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| bugs | [constant-condition](https://docs.styra.com/regal/rules/bugs/constant-condition) | Constant condition |
| bugs | [inconsistent-args](https://docs.styra.com/regal/rules/bugs/inconsistent-args) | Inconsistently named function arguments |
| bugs | [invalid-metadata-attribute](https://docs.styra.com/regal/rules/bugs/invalid-metadata-attribute) | Invalid attribute in metadata annotation |
| bugs | [not-equals-in-loop](https://docs.styra.com/regal/rules/bugs/not-equals-in-loop) | Use of != in loop |
| bugs | [rule-named-if](https://docs.styra.com/regal/rules/bugs/rule-named-if) | Rule named "if" |
Expand Down
2 changes: 2 additions & 0 deletions bundle/regal/config/provided/data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ rules:
bugs:
constant-condition:
level: error
inconsistent-args:
level: error
invalid-metadata-attribute:
level: error
not-equals-in-loop:
Expand Down
12 changes: 6 additions & 6 deletions bundle/regal/result.rego
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ _category_title_from_path(path) := [category, title] if ["regal", "rules", categ
_category_title_from_path(path) := [category, title] if ["custom", "regal", "rules", category, title] = path

# Provided rules, i.e. regal.rules.category.title
fail(chain, details) := violation if {
anderseknert marked this conversation as resolved.
Show resolved Hide resolved
is_array(chain) # from rego.metadata.chain()
fail(metadata, details) := violation if {
is_array(metadata) # from rego.metadata.chain()

some link in chain
some link in metadata
link.annotations.scope == "package"

some category, title
Expand All @@ -76,10 +76,10 @@ fail(chain, details) := violation if {
}

# Custom rules, i.e. custom.regal.rules.category.title
fail(chain, details) := violation if {
is_array(chain) # from rego.metadata.chain()
fail(metadata, details) := violation if {
is_array(metadata) # from rego.metadata.chain()

some link in chain
some link in metadata
link.annotations.scope == "package"

some category, title
Expand Down
63 changes: 63 additions & 0 deletions bundle/regal/rules/bugs/inconsistent_args.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# METADATA
# description: Inconsistently named function arguments
package regal.rules.bugs["inconsistent-args"]

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

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

report contains violation if {
count(ast.functions) > 0

# Comprehension indexing — as made obvious here it would be great
# to have block level scoped ignore directives...
function_args_by_name := {name: args_list |
some i

# regal ignore:prefer-some-in-iteration
name := ast.ref_to_string(ast.functions[i].head.ref)
args_list := [args |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd possibly get a perf boost by making args_list a set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we didn't need to account for primitives, we could then simply just have made a count(args_list) > 1 check 😄 .

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to iterate over it, so I don't think a set would perform better? 🤔

some j
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably won't make a dent on performance, but we could add a i != j here, right?


# regal ignore:prefer-some-in-iteration
ast.ref_to_string(ast.functions[j].head.ref) == name

# regal ignore:prefer-some-in-iteration
args := ast.functions[j].head.args
]
count(args_list) > 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't the arg list size always be > 1, as each rule is counted at least twice, once when iterating i and once for j?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the args_list only iterates on j.

}

some name, args_list in function_args_by_name

# "Partition" the args by their position
by_position := [s |
some i, _ in args_list[0]
s := [x | x := args_list[_][i]]
]

some position in by_position

inconsistent_args(position)

violation := result.fail(rego.metadata.chain(), result.location(find_function_by_name(name)))
}

inconsistent_args(position) if {
named_vars := {arg.value |
some arg in position
arg.type == "var"
not startswith(arg.value, "$")
}
count(named_vars) > 1
}

# Return the _second_ function found by name, as that
# is reasonably the location the inconsistency is found
find_function_by_name(name) := [fn |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless we can a pinpoint the first function with an actual argument name inconsistency, isn't it more clear to just point to the first function declaration.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly, but the second function is at least arguably more likely to be inconsistent than the first :P

some fn in ast.functions
ast.ref_to_string(fn.head.ref) == name
][1]
83 changes: 83 additions & 0 deletions bundle/regal/rules/bugs/inconsistent_args_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package regal.rules.bugs["inconsistent-args_test"]

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

import data.regal.ast

import data.regal.rules.bugs["inconsistent-args"] as rule

test_fail_inconsistent_args if {
module := ast.with_future_keywords(`
foo(a, b) if a == b
foo(b, a) if b > a

bar(b, a) if b > a
`)
r := rule.report with input as module
r == expected_with_location({"col": 2, "file": "policy.rego", "row": 10, "text": "\tfoo(b, a) if b > a"})
}

test_fail_nested_inconsistent_args if {
module := ast.with_future_keywords(`
a.b.foo(a, b) if a == b
a.b.foo(b, a) if b > a
`)
r := rule.report with input as module
r == expected_with_location({"col": 2, "file": "policy.rego", "row": 10, "text": "\ta.b.foo(b, a) if b > a"})
}

test_success_not_inconsistent_args if {
module := ast.with_future_keywords(`
foo(a, b) if a == b
foo(a, b) if a > b

bar(b, a) if b > a
bar(b, a) if b == a

qux(c, a) if c == a
`)
r := rule.report with input as module
r == set()
}

test_success_using_wildcard if {
module := ast.with_future_keywords(`
foo(a, b) if a == b
foo(_, b) if b.foo

qux(c, a) if c == a
`)
r := rule.report with input as module
r == set()
}

test_success_using_pattern_matching if {
module := ast.with_future_keywords(`
foo(a, b) if a == b
foo(a, "foo") if a.foo

qux(c, a) if c == a
`)
r := rule.report with input as module
r == set()
}

expected := {
"category": "bugs",
"description": "Inconsistently named function arguments",
"level": "error",
"related_resources": [{
"description": "documentation",
"ref": "https://docs.styra.com/regal/rules/bugs/inconsistent-args",
}],
"title": "inconsistent-args",
}

expected_with_location(location) := {object.union(expected, {"location": location})} if is_object(location)

expected_with_location(location) := {object.union(expected, {"location": loc}) |
some loc in location
} if {
is_set(location)
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ expected := {

expected_with_location(location) := {object.union(expected, {"location": location})} if is_object(location)

expected_with_location(locations) := {object.union(expected, {"location": location}) |
some location in locations
expected_with_location(location) := {object.union(expected, {"location": loc}) |
some loc in location
} if {
is_set(locations)
is_set(location)
}
86 changes: 86 additions & 0 deletions docs/rules/bugs/inconsistent-args.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# inconsistent-args

**Summary**: Inconsistently named function arguments

**Category**: Bugs

**Avoid**
```rego
package policy

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

find_vars(rule, node) if node in rule

# Order of arguments changed, or at least it looks like it
find_vars(node, rule) if {
walk(rule, [path, value])
# ...
}
```

**Prefer**
```rego
package policy

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

find_vars(rule, node) if node in rule

find_vars(rule, node) if {
walk(rule, [path, value])
# ...
}
```

## Rationale

Whenever a custom function declaration is repeated, the argument names should remain consistent in each declaration.

Inconsistently named function arguments is a likely source of bugs, and should be avoided.

## Exceptions

Using wildcards (`_`) in place of unused arguments is always allowed, and in fact enforced by the compiler:

```rego
package policy

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

find_vars(rule, node) if node in rule

# We don't use `node` here
find_vars(rule, _) if {
walk(rule, [path, value])
# ...
}
```

Using [pattern matching for equality](https://docs.styra.com/regal/rules/idiomatic/equals-pattern-matching) checks is
also allowed.

## Configuration Options

This linter rule provides the following configuration options:

```yaml
rules:
bugs:
inconsistent-args:
# one of "error", "warning", "ignore"
level: error
```

## Related Resources

- Regal Docs: [equals-pattern-matching](https://docs.styra.com/regal/rules/idiomatic/equals-pattern-matching)

## Community

If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules,
or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community
[Slack](https://communityinviter.com/apps/styracommunity/signup)!
8 changes: 8 additions & 0 deletions e2e/testdata/violations/most_violations.rego
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ unused_return_value {

zero_arity_function() := true

inconsistent_args(a, b) {
a == b
}

inconsistent_args(b, a) {
b == a
}

### Idiomatic ###

custom_has_key_construct(map, key) {
Expand Down
Loading