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: missing-metadata #1131

Merged
merged 1 commit into from
Sep 24, 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
2 changes: 1 addition & 1 deletion .github/workflows/update-example-index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:

cat "$TEMP_DIR/sitemap.xml" | \
rq -i xml --indent " " | \
opa eval 'data.process.symbols' \
opa eval 'data.build.workflows.symbols' \
-d build/workflows/update-example-index/process.rego \
--format=pretty \
--stdin-input | \
Expand Down
15 changes: 15 additions & 0 deletions .regal/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,18 @@ ignore:
files:
- e2e/*
- pkg/*

rules:
custom:
missing-metadata:
level: error
except-rule-path-pattern: \.report$
# TODO: this should be in the default config, but it seems
# like the ignore attribute isn't read from there
ignore:
files:
- "*_test.rego"
style:
line-length:
level: error
non-breakable-word-threshold: 100
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ The following rules are currently available:
| bugs | [var-shadows-builtin](https://docs.styra.com/regal/rules/bugs/var-shadows-builtin) | Variable name shadows built-in |
| bugs | [zero-arity-function](https://docs.styra.com/regal/rules/bugs/zero-arity-function) | Avoid functions without args |
| custom | [forbidden-function-call](https://docs.styra.com/regal/rules/custom/forbidden-function-call) | Forbidden function call |
| custom | [missing-metadata](https://docs.styra.com/regal/rules/custom/missing-metadata) | Package or rule missing metadata |
| custom | [naming-convention](https://docs.styra.com/regal/rules/custom/naming-convention) | Naming convention violation |
| custom | [one-liner-rule](https://docs.styra.com/regal/rules/custom/one-liner-rule) | Rule body could be made a one-liner |
| custom | [prefer-value-in-head](https://docs.styra.com/regal/rules/custom/prefer-value-in-head) | Prefer value in rule head |
Expand Down
36 changes: 22 additions & 14 deletions build/simplecov/simplecov.rego
Original file line number Diff line number Diff line change
@@ -1,53 +1,61 @@
# METADATA
# description: |
# transforms OPA's JSON test coverage format to equivalent
# simplecov JSON, to be used for Codecov reports, et. al.
package build.simplecov

import rego.v1

from_opa := {"coverage": coverage}
# METADATA
# entrypoint: true
from_opa := {"coverage": _coverage}

coverage[file] := {"lines": to_lines(report)} if some file, report in input.files
_coverage[file] := {"lines": _to_lines(report)} if some file, report in input.files

covered_map(report) := cm if {
_covered_map(report) := cm if {
covered := object.get(report, "covered", [])
cm := {line: 1 |
some item in covered
some line in numbers.range(item.start.row, item.end.row)
}
}

not_covered_map(report) := ncm if {
_not_covered_map(report) := ncm if {
not_covered := object.get(report, "not_covered", [])
ncm := {line: 0 |
some item in not_covered
some line in numbers.range(item.start.row, item.end.row)
}
}

to_lines(report) := lines if {
cm := covered_map(report)
ncm := not_covered_map(report)
_to_lines(report) := lines if {
cm := _covered_map(report)
ncm := _not_covered_map(report)
keys := sort([line | some line, _ in object.union(cm, ncm)])
last := keys[count(keys) - 1]

lines := [value |
some i in numbers.range(1, last)
value := to_value(cm, ncm, i)
value := _to_value(cm, ncm, i)
]
}

to_value(cm, _, line) := 1 if cm[line]
_to_value(cm, _, line) := 1 if cm[line]

to_value(_, ncm, line) := 0 if ncm[line]
_to_value(_, ncm, line) := 0 if ncm[line]

to_value(cm, ncm, line) := null if {
_to_value(cm, ncm, line) := null if {
not cm[line]
not ncm[line]
}

# utility rule to evaluate when only the
# lines not covered are of interest
# invoke like:
# METADATA
# description: |
# utility rule to evaluate when only the lines not covered are of interest
# invoke like:
# regal test --coverage bundle \
# | opa eval -f pretty -I -d build/simplecov/simplecov.rego 'data.build.simplecov.not_covered'
# entrypoint: true
not_covered[file] := info.not_covered if {
some file, info in input.files
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# regal ignore:directory-package-mismatch
package process
# METADATA
# description: updates the rego by example index page
# related_resources:
# - description: documentation
# ref: http://docs.styra.com/opa/rego-by-example
# - description: workflow
# ref: file:///./../../.github/workflows/update-example-index.yaml
package build.workflows

import rego.v1

# METADATA
# entrypoint: true
symbols := {"keywords": _keywords, "builtins": _builtins}

_keywords[name] := path if {
Expand Down
94 changes: 83 additions & 11 deletions bundle/regal/ast/ast.rego
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# METADATA
# description: |
# the 'ast' package provides the base functionality for working
# with OPA's AST, more recently in the form of RoAST
package regal.ast

import rego.v1

import data.regal.config
import data.regal.util

# METADATA
# description: set of Rego's scalar type
scalar_types := {"boolean", "null", "number", "string"}

# METADATA
# description: set containing names of all built-in functions counting as operators
operators := {
"and",
"assign",
Expand All @@ -27,18 +35,28 @@ operators := {
"rem",
}

# regal ignore:external-reference
is_constant(value) if value.type in scalar_types
# METADATA
# description: |
# returns true if provided term is either a scalar or a collection of ground values
# scope: document
is_constant(term) if term.type in scalar_types # regal ignore:external-reference

is_constant(value) if {
value.type in {"array", "object"}
not has_term_var(value.value)
is_constant(term) if {
term.type in {"array", "object"}
not has_term_var(term.value)
}

default builtin_names := set()

# METADATA
# description: set containing the name of all built-in functions (given the active capabilities)
# scope: document
builtin_names := object.keys(config.capabilities.builtins)

# METADATA
# description: |
# set containing the namespaces of all built-in functions (given the active capabilities),
# like "http" in `http.send` or "sum" in `sum``
builtin_namespaces contains namespace if {
some name in builtin_names
namespace := split(name, ".")[0]
Expand All @@ -59,16 +77,18 @@ package_path := [path.value |
# input policy, so "package foo.bar" would return "foo.bar"
package_name := concat(".", package_path)

named_refs(refs) := [ref |
some i, ref in refs
_is_name(ref, i)
# METADATA
# description: provides all static string values from ref
named_refs(ref) := [term |
some i, term in ref
_is_name(term, i)
]

_is_name(ref, 0) if ref.type == "var"
_is_name(term, 0) if term.type == "var"

_is_name(ref, pos) if {
_is_name(term, pos) if {
pos > 0
ref.type == "string"
term.type == "string"
}

# METADATA
Expand All @@ -94,6 +114,30 @@ functions := [rule |
rule.head.args
]

# METADATA
# description: |
# all rules and functions in the input AST not denoted as private, i.e. excluding
# any rule/function with a `_` prefix. it's not unthinkable that more ways to denote
# private rules (or even packages), so using this rule should be preferred over
# manually checking for this using the rule ref
public_rules_and_functions := [rule |
some rule in input.rules

count([part |
some i, part in rule.head.ref

_private_rule(i, part)
]) == 0
]

_private_rule(0, part) if startswith(part.value, "_")

_private_rule(i, part) if {
i > 0
part.type == "string"
startswith(part.value, "_")
}

# METADATA
# description: a list of the argument names for the given rule (if function)
function_arg_names(rule) := [arg.value | some arg in rule.head.args]
Expand Down Expand Up @@ -213,6 +257,8 @@ _trim_from_var(ref_str, vars) := ref_str if {
count(vars) == 0
} else := substring(ref_str, 0, indexof(ref_str, vars[0]))

# METADATA
# description: true if ref contains only static parts
static_ref(ref) if every t in array.slice(ref.value, 1, count(ref.value)) {
t.type != "var"
}
Expand Down Expand Up @@ -245,8 +291,12 @@ function_decls(rules) := {rule_name: decl |
decl := {"decl": {"args": args, "result": {"type": "any"}}}
}

# METADATA
# description: returns the args for function past the expected number of args
function_ret_args(fn_name, terms) := array.slice(terms, count(all_functions[fn_name].decl.args) + 1, count(terms))

# METADATA
# description: true if last argument of function is a return assignment
function_ret_in_args(fn_name, terms) if {
rest := array.slice(terms, 1, count(terms))

Expand All @@ -268,6 +318,8 @@ implicit_boolean_assignment(rule) if {
# or sometimes, like this...
implicit_boolean_assignment(rule) if rule.head.value.location == rule.head.location

implicit_boolean_assignment(rule) if util.to_location_object(rule.head.value.location).col == 1

# METADATA
# description: |
# object containing all available built-in and custom functions in the
Expand All @@ -280,6 +332,8 @@ all_functions := object.union(config.capabilities.builtins, function_decls(input
# scope of the input AST
all_function_names := object.keys(all_functions)

# METADATA
# description: set containing all negated expressions in input AST
negated_expressions[rule] contains value if {
some rule in input.rules

Expand All @@ -305,8 +359,26 @@ is_chained_rule_body(rule, lines) if {
startswith(col_text, "{")
}

# METADATA
# description: returns the terms in an assignment expression, or undefined if not assignment
assignment_terms(expr) := [expr.terms[1], expr.terms[2]] if {
expr.terms[0].type == "ref"
expr.terms[0].value[0].type == "var"
expr.terms[0].value[0].value == "assign"
}

# METADATA
# description: |
# For a given rule head name, this rule contains a list of locations where
# there is a rule head with that name.
rule_head_locations[name] contains {"row": loc.row, "col": loc.col} if {
some rule in input.rules

name := concat(".", [
"data",
package_name,
ref_static_to_string(rule.head.ref),
])

loc := util.to_location_object(rule.head.location)
}
46 changes: 46 additions & 0 deletions bundle/regal/ast/ast_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,49 @@ test_ref_static_to_string if {
{"type": "string", "value": "completion_test"},
]) == `data.regal.lsp.completion_test`
}

test_rule_head_locations if {
policy := `package policy

import rego.v1

default allow := false

allow if true

reasons contains "foo"
reasons contains "bar"

default my_func(_) := false
my_func(1) := true

ref_rule[foo] := true if {
some foo in [1,2,3]
}
`
result := ast.rule_head_locations with input as regal.parse_module("p.rego", policy)

result == {
"data.policy.allow": {{"col": 9, "row": 5}, {"col": 1, "row": 7}},
"data.policy.reasons": {{"col": 1, "row": 9}, {"col": 1, "row": 10}},
"data.policy.my_func": {{"col": 9, "row": 12}, {"col": 1, "row": 13}},
"data.policy.ref_rule": {{"col": 1, "row": 15}},
}
}

test_public_rules_and_functions if {
module := regal.parse_module("p.rego", `package p

foo := true

_bar := false

x.y := true

x._z := false
`)

public := ast.public_rules_and_functions with input as module

{ast.ref_to_string(rule.head.ref) | some rule in public} == {"foo", "x.y"}
}
2 changes: 2 additions & 0 deletions bundle/regal/ast/comments.rego
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ comments["metadata_attributes"] := {
"custom",
}

# METADATA
# description: true if comment matches a metadata annotation attribute
comments["annotation_match"](str) if regex.match(
`^(scope|title|description|related_resources|authors|organizations|schemas|entrypoint|custom)\s*:`,
str,
Expand Down
3 changes: 3 additions & 0 deletions bundle/regal/ast/keywords.rego
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import rego.v1

import data.regal.util

# METADATA
# description: collects keywords from input module by the line that they appear on
# scope: document
keywords[row] contains keyword if {
some idx, line in input.regal.file.lines

Expand Down
21 changes: 0 additions & 21 deletions bundle/regal/ast/rule_head_locations.rego

This file was deleted.

Loading