Skip to content
New issue

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

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

Already on GitHub? # to your account

Single file impossible-not #713

Merged
merged 1 commit into from
May 13, 2024
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
13 changes: 13 additions & 0 deletions bundle/regal/ast/imports.rego
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ imported_identifiers contains imported_identifier(imp) if {
imp.path.value[0].value in {"input", "data"}
}

# METADATA
# description: |
# map of all imported paths in the input module, keyed by their identifier or "namespace"
resolved_imports[identifier] := path if {
some _import in imports

_import.path.value[0].value == "data"
count(_import.path.value) > 1

identifier := imported_identifier(_import)
path := [part.value | some part in _import.path.value]
}

imported_identifier(imp) := imp.alias

imported_identifier(imp) := regal.last(imp.path.value).value if not imp.alias
Expand Down
110 changes: 57 additions & 53 deletions bundle/regal/rules/bugs/impossible_not.rego
Original file line number Diff line number Diff line change
Expand Up @@ -7,85 +7,93 @@ import rego.v1
import data.regal.ast
import data.regal.result

# regal ignore:rule-length
aggregate contains entry if {
package_path := [part.value | some part in input["package"].path]
package_path := [part.value | some part in input["package"].path]

imported_symbols := {symbol: path |
some _import in input.imports
multivalue_rules contains path if {
some rule in ast.rules

_import.path.value[0].value == "data"
count(_import.path.value) > 1
rule.head.key
not rule.head.value

symbol := imported_symbol(_import)
path := [part.value | some part in _import.path.value]
# ignore general ref head rules for now
every path in array.slice(rule.head.ref, 1, count(rule.head.ref)) {
path.type == "string"
}

multivalue_rules := {path |
some rule in ast.rules
path := concat(".", array.concat(package_path, [p |
some ref in rule.head.ref
p := ref.value
]))
}

rule.head.key
not rule.head.value
negated_refs contains negated_ref if {
some i, rule in input.rules

# ignore general ref head rules for now
every path in array.slice(rule.head.ref, 1, count(rule.head.ref)) {
path.type == "string"
}
walk(rule, [_, value])

path := concat(".", array.concat(package_path, [p |
some ref in rule.head.ref
p := ref.value
]))
}
value.negated

negated_refs := [negated_ref |
some i, rule in input.rules
# if terms is an array, it's a function call, and most likely not "impossible"
is_object(value.terms)
value.terms.type in {"ref", "var"}

walk(rule, [_, value])
ref := var_to_ref(value.terms)

value.negated
# for now, ignore ref if it has variable components
every path in array.slice(ref, 1, count(ref)) {
path.type == "string"
}

# if terms is an array, it's a function call, and most likely not "impossible"
is_object(value.terms)
value.terms.type in {"ref", "var"}
# ignore negated local vars
not ref[0].value in ast.function_arg_names(rule)
not ref[0].value in {var.value | some var in ast.find_vars_in_local_scope(rule, value.location)}

ref := var_to_ref(value.terms)
negated_ref := {
"ref": ref,
"resolved_path": resolve(ref, package_path, ast.resolved_imports),
}
}

# for now, ignore ref if it has variable components
every path in array.slice(ref, 1, count(ref)) {
path.type == "string"
}
aggregate contains result.aggregate(rego.metadata.chain(), {
"imported_symbols": ast.resolved_imports,
"multivalue_rules": multivalue_rules,
"negated_refs": negated_refs,
})

# ignore negated local vars
not ref[0].value in ast.function_arg_names(rule)
not ref[0].value in {var.value | some var in ast.find_vars_in_local_scope(rule, value.location)}
report contains violation if {
some entry in aggregate
some negated in entry.aggregate_data.negated_refs

negated.resolved_path in entry.aggregate_data.multivalue_rules

negated_ref := {
"ref": ref,
"resolved_path": resolve(ref, package_path, imported_symbols),
}
]
loc := object.union(result.location(negated.ref), {"location": {
"file": entry.aggregate_source.file,
# note that the "not" isn't present in the AST, so we'll add it manually to the text
# in the location to try and make it clear where the issue is (as opposed to just
# printing the ref)
"text": sprintf("not %s", [to_string(negated.ref)]),
}})

entry := result.aggregate(rego.metadata.chain(), {
"imported_symbols": imported_symbols,
"multivalue_rules": multivalue_rules,
"negated_refs": negated_refs,
})
violation := result.fail(rego.metadata.chain(), loc)
}

# METADATA
# schemas:
# - input: schema.regal.aggregate
aggregate_report contains violation if {
all_multivalue_refs := {path |
all_multivalue_refs := {{"path": path, "file": entry.aggregate_source.file} |
some entry in input.aggregate
some path in entry.aggregate_data.multivalue_rules
}

some entry in input.aggregate
some negated in entry.aggregate_data.negated_refs
some multivalue_ref in all_multivalue_refs

# violations in same file handled by non-aggregate "report"
multivalue_ref.file != entry.aggregate_source.file

negated.resolved_path in all_multivalue_refs
negated.resolved_path == multivalue_ref.path

loc := object.union(result.location(negated.ref), {"location": {
"file": entry.aggregate_source.file,
Expand All @@ -102,10 +110,6 @@ var_to_ref(terms) := [terms] if terms.type == "var"

var_to_ref(terms) := terms.value if terms.type == "ref"

imported_symbol(imp) := imp.alias

imported_symbol(imp) := regal.last(imp.path.value).value if not imp.alias

to_string(ref) := concat(".", [path |
some part in ref
path := part.value
Expand Down
38 changes: 32 additions & 6 deletions bundle/regal/rules/bugs/impossible_not_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,38 @@ test_success_multivalue_not_reference_invalidated_by_function_argument if {
r == set()
}

test_success_multivalue_not_reference_in_same_file_not_reported_in_aggregate_report if {
agg1 := rule.aggregate with input as regal.parse_module("p1.rego", `package foo

import rego.v1

partial contains "foo"

test_partial if {
not partial
}
`)

r := rule.aggregate_report with input as {"aggregate": agg1}
r == set()
}

test_fail_multivalue_not_reference_in_same_file_reported_in_normal_report if {
module := regal.parse_module("p1.rego", `package foo

import rego.v1

partial contains "foo"

test_partial if {
not partial
}
`)

r := rule.report with input as module
r == expected_with_location({"col": 7, "file": "p1.rego", "row": 8, "text": "not partial"})
}

expected := {
"category": "bugs",
"description": "Impossible `not` condition",
Expand All @@ -130,9 +162,3 @@ expected := {
}

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)
}
2 changes: 1 addition & 1 deletion docs/rules/bugs/impossible-not.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**Category**: Bugs

**Type**: Aggregate - only runs when more than one file is provided for linting
**Type**: Aggregate - runs both on single files as well as when more than one file is provided for linting

**Avoid**
```rego
Expand Down
Loading