Skip to content

Commit

Permalink
Rule: missing-metadata
Browse files Browse the repository at this point in the history
This rule is not for everyone, and hence in the `custom` category.

For a public projects, libraries and reusable policies, this should
however be considered.

This uncovered a
[bug in OPA](open-policy-agent/opa#7050), which has
now been addressed. In order to make progress on this we're now depending on
OPA `main`. This should be changed as soon as OPA v0.69.0 is released.

Some unrelated fixes in this PR, fixing annoyances as I worked on it:
- add Regal specific attributes (`input.regal.*`) to the input created by the
  `eval:use-as-input` directive
- Add another metadata snippet to allow creating a `# METADATA` block
  with only description and no title. Tbh, I'm still not sure what to
  use `title` for, while `description` is pretty much mandatory.
- Set `ignore-if-sub-attribute: true` on default config for
  `prefer-some-in-iteration` rule. This was documented to be the case
  already, but seemed to have gone missing at some point.
- Add pprof option to language-server command, and pprof handlers
  to web server

Signed-off-by: Anders Eknert <anders@styra.com>
  • Loading branch information
anderseknert committed Sep 23, 2024
1 parent 4aa2fef commit a1ce54a
Show file tree
Hide file tree
Showing 90 changed files with 1,185 additions and 432 deletions.
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

0 comments on commit a1ce54a

Please sign in to comment.