Skip to content

Commit aba5ef4

Browse files
authored
Rule: rule-assigns-default (#1243)
Flags rules where the default value is used as the assigned value in any other incremental definition. Fixes #1018 (unrelated changes mostly adding some ignore directives in Go code is from me having update golangci-lint which led to a few new issues getting reported) Signed-off-by: Anders Eknert <[email protected]>
1 parent 73c27a0 commit aba5ef4

File tree

15 files changed

+178
-7
lines changed

15 files changed

+178
-7
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ The following rules are currently available:
214214
| bugs | [leaked-internal-reference](https://docs.styra.com/regal/rules/bugs/leaked-internal-reference) | Outside reference to internal rule or function |
215215
| bugs | [not-equals-in-loop](https://docs.styra.com/regal/rules/bugs/not-equals-in-loop) | Use of != in loop |
216216
| bugs | [redundant-existence-check](https://docs.styra.com/regal/rules/bugs/redundant-existence-check) | Redundant existence check |
217+
| bugs | [rule-assigns-default](https://docs.styra.com/regal/rules/bugs/rule-assigns-default) | Rule assigned its default value |
217218
| bugs | [rule-named-if](https://docs.styra.com/regal/rules/bugs/rule-named-if) | Rule named "if" |
218219
| bugs | [rule-shadows-builtin](https://docs.styra.com/regal/rules/bugs/rule-shadows-builtin) | Rule name shadows built-in |
219220
| bugs | [sprintf-arguments-mismatch](https://docs.styra.com/regal/rules/bugs/sprintf-arguments-mismatch) | Mismatch in `sprintf` arguments count |

bundle/regal/config/provided/data.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ rules:
3232
level: error
3333
redundant-existence-check:
3434
level: error
35+
rule-assigns-default:
36+
level: error
3537
rule-named-if:
3638
level: error
3739
rule-shadows-builtin:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# regal eval:use-as-input
2+
# METADATA
3+
# description: Rule assigned its default value
4+
package regal.rules.bugs["rule-assigns-default"]
5+
6+
import rego.v1
7+
8+
import data.regal.ast
9+
import data.regal.result
10+
11+
report contains violation if {
12+
some rule in input.rules
13+
14+
not rule["default"]
15+
16+
ref := ast.ref_to_string(rule.head.ref)
17+
18+
_default_rule_values[ref] == rule.head.value.value
19+
20+
violation := result.fail(rego.metadata.chain(), result.location(rule.head.value))
21+
}
22+
23+
_default_rule_values[ref] := rule.head.value.value if {
24+
some rule in input.rules
25+
rule["default"]
26+
27+
ref := ast.ref_to_string(rule.head.ref)
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package regal.rules.bugs["rule-assigns-default_test"]
2+
3+
import rego.v1
4+
5+
import data.regal.ast
6+
import data.regal.config
7+
8+
import data.regal.rules.bugs["rule-assigns-default"] as rule
9+
10+
test_fail_rule_assigned_default_value if {
11+
module := ast.with_rego_v1(`
12+
13+
default allow := false
14+
15+
allow := false if {
16+
some conditions in policy
17+
}
18+
`)
19+
r := rule.report with input as module
20+
21+
r == {{
22+
"category": "bugs",
23+
"description": "Rule assigned its default value",
24+
"level": "error",
25+
"location": {
26+
"col": 11,
27+
"end": {
28+
"col": 16,
29+
"row": 9,
30+
},
31+
"file": "policy.rego",
32+
"row": 9,
33+
"text": "\tallow := false if {",
34+
},
35+
"related_resources": [{
36+
"description": "documentation",
37+
"ref": config.docs.resolve_url("$baseUrl/$category/rule-assigns-default", "bugs"),
38+
}],
39+
"title": "rule-assigns-default",
40+
}}
41+
}
42+
43+
test_success_rule_not_assigned_default_value if {
44+
module := ast.with_rego_v1(`
45+
46+
default allow := false
47+
48+
allow := true if {
49+
some conditions in policy
50+
}
51+
`)
52+
r := rule.report with input as module
53+
54+
r == set()
55+
}
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# rule-assigns-default
2+
3+
**Summary**: Rule assigned its default value
4+
5+
**Category**: Bugs
6+
7+
**Avoid**
8+
```rego
9+
package policy
10+
11+
import rego.v1
12+
13+
default allow := false
14+
15+
# this rule assigns the same value as the default
16+
# and the policy would work the same without it
17+
allow := false if {
18+
not "admin" in input.user.roles
19+
}
20+
```
21+
22+
**Prefer**
23+
```rego
24+
package policy
25+
26+
import rego.v1
27+
28+
default allow := false
29+
30+
# or just `allow if {` as `true` is implicit
31+
allow := true if {
32+
"admin" in input.user.roles
33+
}
34+
```
35+
36+
## Rationale
37+
38+
When a default value is used for a rule, assigning the same value anywhere else to that rule is pointless, as the rule
39+
would evaluate to the same value with or without the assignment.
40+
41+
## Configuration Options
42+
43+
This linter rule provides the following configuration options:
44+
45+
```yaml
46+
rules:
47+
bugs:
48+
rule-assigns-default:
49+
# one of "error", "warning", "ignore"
50+
level: error
51+
```
52+
53+
## Related Resources
54+
55+
- GitHub: [Source Code](https://github.com/StyraInc/regal/blob/main/bundle/regal/rules/bugs/rule-assigns-default/rule_assigns_default.rego)
56+
57+
## Community
58+
59+
If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules,
60+
or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community
61+
[Slack](https://communityinviter.com/apps/styracommunity/signup)!

e2e/cli_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,10 @@ func TestLintAggregateIgnoreDirective(t *testing.T) {
456456
t.Errorf("expected 2 violations, got %d", rep.Summary.NumViolations)
457457
}
458458

459+
if rep.Summary.NumViolations == 0 {
460+
t.Fatal("expected violations, got none")
461+
}
462+
459463
if rep.Violations[0].Title != "no-defined-entrypoint" {
460464
t.Errorf("expected violation 'no-defined-entrypoint', got %q", rep.Violations[0].Title)
461465
}

e2e/testdata/violations/most_violations.rego

+16-6
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ top_level_iteration := input[_]
6868

6969
unassigned_return_value if indexof("foo", "o")
7070

71-
zero_arity_function() := true
71+
zero_arity_function := true
7272

7373
inconsistent_args(a, b) if {
7474
a == b
@@ -93,9 +93,9 @@ impossible_not if {
9393
not partial
9494
}
9595

96-
argument_always_wildcard(_) if true
96+
argument_always_wildcard(_) = true
9797

98-
argument_always_wildcard(_) if true
98+
argument_always_wildcard(_) = true
9999

100100
# title: annotation without metadata
101101
some_rule := true
@@ -107,6 +107,12 @@ unused_output_variable if {
107107
input[x]
108108
}
109109

110+
default rule_assigns_default := false
111+
112+
rule_assigns_default := false if {
113+
input.yes
114+
}
115+
110116
### Idiomatic ###
111117

112118
custom_has_key_construct(map, key) if {
@@ -164,7 +170,7 @@ line_length := "should be no longer than 120 characters but this line is really
164170

165171
#no-whitespace-comment
166172

167-
opa_fmt := "fail"
173+
opa_fmt := "fail"
168174

169175
preferSnakeCase := "fail"
170176

@@ -178,7 +184,9 @@ use_assignment = "oparator"
178184

179185
chained_rule_body if {
180186
input.x
181-
} {
187+
}
188+
189+
chained_rule_body if {
182190
input.y
183191
}
184192

@@ -243,6 +251,7 @@ pointless_reassignment := yoda_condition
243251

244252
# this will also trigger the test-outside-test-package rule
245253
test_identically_named_tests := true
254+
246255
test_identically_named_tests := false
247256

248257
todo_test_bad if {
@@ -253,7 +262,7 @@ print_or_trace_call if {
253262
print("forbidden!")
254263
}
255264

256-
abc if true
265+
abc = true
257266

258267
# metasyntactic variable
259268
foo := "bar"
@@ -271,6 +280,7 @@ y if {
271280

272281
# double negation
273282
not_fine := true
283+
274284
fine if not not_fine
275285

276286
# rule name repeats package name

internal/io/io.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func FindInput(file string, workspacePath string) (string, io.Reader) {
100100
relative := strings.TrimPrefix(file, workspacePath)
101101
components := strings.Split(filepath.Dir(relative), string(filepath.Separator))
102102

103-
for i := range len(components) {
103+
for i := range components {
104104
inputPath := filepath.Join(workspacePath, filepath.Join(components[:len(components)-i]...), "input.json")
105105

106106
f, err := os.Open(inputPath)

internal/lsp/diff.go

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type operation struct {
3737

3838
// operations returns the list of operations to convert a into b, consolidating
3939
// operations for multiple lines and not including equal lines.
40+
// nolint:gosec
4041
func operations(a, b []string) []*operation {
4142
if len(a) == 0 && len(b) == 0 {
4243
return nil

internal/lsp/documentsymbol.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func documentSymbols(
2424

2525
lines := strings.Split(contents, "\n")
2626

27+
// nolint:gosec
2728
pkgRange := types.Range{
2829
Start: types.Position{Line: 0, Character: 0},
2930
End: types.Position{Line: uint(len(lines) - 1), Character: uint(len(lines[len(lines)-1]))},
@@ -128,6 +129,7 @@ func documentSymbols(
128129
return docSymbols
129130
}
130131

132+
// nolint:gosec
131133
func locationToRange(location *ast.Location) types.Range {
132134
lines := bytes.Split(location.Text, []byte("\n"))
133135

internal/lsp/foldingrange.go

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func (s stack) Pop() (stack, scanner.Position) {
2222
return s[:l-1], s[l-1]
2323
}
2424

25+
// nolint:gosec
2526
func TokenFoldingRanges(policy string) []types.FoldingRange {
2627
scn, err := scanner.New(strings.NewReader(policy))
2728
if err != nil {
@@ -98,6 +99,7 @@ func TokenFoldingRanges(policy string) []types.FoldingRange {
9899
return foldingRanges
99100
}
100101

102+
// nolint:gosec
101103
func findFoldingRanges(text string, module *ast.Module) []types.FoldingRange {
102104
uintZero := uint(0)
103105

internal/lsp/hover/hover.go

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ func UpdateBuiltinPositions(cache *cache.Cache, uri string, builtins map[string]
143143

144144
builtinsOnLine := map[uint][]types2.BuiltinPosition{}
145145

146+
// nolint:gosec
146147
for _, call := range rego.AllBuiltinCalls(module, builtins) {
147148
line := uint(call.Location.Row)
148149

internal/lsp/lint.go

+2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func updateParse(
117117
link = "https://docs.styra.com/opa/errors/" + hints[0]
118118
}
119119

120+
// nolint:gosec
120121
diags = append(diags, types.Diagnostic{
121122
Severity: 1, // parse errors are the only error Diagnostic the server sends
122123
Range: types.Range{
@@ -322,6 +323,7 @@ type astError struct {
322323
Message string `json:"message"`
323324
}
324325

326+
// nolint:gosec
325327
func getRangeForViolation(item report.Violation) types.Range {
326328
start := types.Position{
327329
Line: uint(max(item.Location.Row-1, 0)),

internal/lsp/rego/rego.go

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type KeywordUseLocation struct {
3838
}
3939

4040
func PositionFromLocation(loc *ast.Location) types.Position {
41+
// nolint:gosec
4142
return types.Position{
4243
Line: uint(loc.Row - 1),
4344
Character: uint(loc.Col - 1),

internal/lsp/server.go

+1
Original file line numberDiff line numberDiff line change
@@ -1855,6 +1855,7 @@ func (l *LanguageServer) handleTextDocumentDefinition(
18551855
return nil, nil
18561856
}
18571857

1858+
// nolint:gosec
18581859
loc := types.Location{
18591860
URI: uri.FromPath(l.clientIdentifier, definition.Result.File),
18601861
Range: types.Range{

0 commit comments

Comments
 (0)