From 2dd760922295e13f9d8bca5f80b36bb4d011d615 Mon Sep 17 00:00:00 2001 From: Anders Eknert Date: Tue, 24 Sep 2024 16:08:50 +0200 Subject: [PATCH] Rule: `missing-metadata` (#1131) 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](https://github.com/open-policy-agent/opa/issues/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 --- .github/workflows/update-example-index.yaml | 2 +- .regal/config.yaml | 15 ++ README.md | 1 + build/simplecov/simplecov.rego | 36 ++-- ...process.rego => update_example_index.rego} | 12 +- bundle/regal/ast/ast.rego | 94 ++++++++-- bundle/regal/ast/ast_test.rego | 46 +++++ bundle/regal/ast/comments.rego | 2 + bundle/regal/ast/keywords.rego | 3 + bundle/regal/ast/rule_head_locations.rego | 21 --- .../regal/ast/rule_head_locations_test.rego | 34 ---- bundle/regal/ast/search.rego | 14 +- bundle/regal/capabilities/capabilities.rego | 17 ++ bundle/regal/config/config.rego | 38 +++-- bundle/regal/config/exclusion.rego | 36 ++-- bundle/regal/config/exclusion_test.rego | 8 +- bundle/regal/config/provided/data.yaml | 5 +- bundle/regal/lsp/codelens/codelens.rego | 2 + bundle/regal/lsp/completion/kind/kind.rego | 56 ++++++ bundle/regal/lsp/completion/main.rego | 2 +- .../providers/booleans/booleans.rego | 4 + .../providers/commonrule/commonrule.rego | 9 +- .../completion/providers/default/default.rego | 5 + .../completion/providers/import/import.rego | 4 + .../providers/inputdotjson/inputdotjson.rego | 14 +- .../completion/providers/locals/locals.rego | 7 +- .../providers/locals/locals_test.rego | 24 ++- .../completion/providers/package/package.rego | 4 + .../providers/packagename/packagename.rego | 14 +- .../packagename/packagename_test.rego | 14 +- .../completion/providers/regov1/regov1.rego | 6 + .../providers/rulerefs/rulerefs.rego | 80 ++++----- .../completion/providers/snippet/snippet.rego | 41 ++++- .../providers/snippet/snippet_test.rego | 39 +++-- .../providers/test_utils/test_utils.rego | 22 +-- bundle/regal/lsp/completion/ref_names.rego | 7 +- bundle/regal/lsp/util/location/location.rego | 2 + bundle/regal/main/main.rego | 61 +++++-- bundle/regal/main/main_test.rego | 11 +- bundle/regal/result/result.rego | 6 + .../bugs/duplicate-rule/duplicate_rule.rego | 16 +- .../bugs/impossible-not/impossible_not.rego | 30 ++-- .../inconsistent-args/inconsistent_args.rego | 14 +- .../top_level_iteration.rego | 12 +- .../missing-metadata/missing_metadata.rego | 132 ++++++++++++++ .../missing_metadata_test.rego | 161 ++++++++++++++++++ .../naming-convention/naming_convention.rego | 16 +- .../custom/one-liner-rule/one_liner_rule.rego | 19 +-- .../prefer_value_in_head.rego | 14 +- .../no_defined_entrypoint.rego | 3 + .../prefer_set_or_object_rule.rego | 6 +- .../use-in-operator/use_in_operator.rego | 18 +- .../use_some_for_output_vars.rego | 4 +- .../circular-import/circular_import.rego | 44 ++--- .../circular-import/circular_import_test.rego | 18 +- .../ignored-import/ignored_import.rego | 4 +- .../import_shadows_builtin.rego | 6 +- .../prefer_package_imports.rego | 10 +- .../unresolved-import/unresolved_import.rego | 2 + .../default-over-else/default_over_else.rego | 10 +- .../detached-metadata/detached_metadata.rego | 4 +- .../double-negative/double_negative.rego | 14 +- .../external_reference.rego | 11 +- .../rules/style/file-length/file_length.rego | 4 +- .../rules/style/line-length/line_length.rego | 12 +- .../prefer_some_in_iteration.rego | 32 ++-- .../rules/style/rule-length/rule_length.rego | 14 +- .../style/todo-comment/todo_comment.rego | 11 +- .../trailing_default_rule.rego | 4 +- .../unconditional_assignment.rego | 6 +- .../unnecessary-some/unnecessary_some.rego | 6 +- .../use_assignment_operator.rego | 1 + .../use_assignment_operator_test.rego | 5 + .../style/yoda-condition/yoda_condition.rego | 4 +- .../identically_named_tests.rego | 4 +- .../metasyntactic_variable.rego | 6 +- bundle/regal/util/util.rego | 18 ++ cmd/languageserver.go | 2 + docs/rules/custom/missing-metadata.md | 86 ++++++++++ e2e/cli_test.go | 17 +- .../aggregates/ignore_directive/first.rego | 2 + .../aggregates/three_policies/policy_1.rego | 2 + .../aggregates/three_policies/policy_2.rego | 2 + .../aggregates/three_policies/policy_3.rego | 2 + .../aggregates/two_policies/policy_1.rego | 2 + .../aggregates/two_policies/policy_2.rego | 2 + e2e/testdata/violations/most_violations.rego | 2 +- internal/lsp/server.go | 9 +- internal/update/update.rego | 12 +- internal/web/server.go | 7 + pkg/linter/linter.go | 6 +- 91 files changed, 1230 insertions(+), 436 deletions(-) rename build/workflows/{update-example-index/process.rego => update_example_index.rego} (71%) delete mode 100644 bundle/regal/ast/rule_head_locations.rego delete mode 100644 bundle/regal/ast/rule_head_locations_test.rego create mode 100644 bundle/regal/rules/custom/missing-metadata/missing_metadata.rego create mode 100644 bundle/regal/rules/custom/missing-metadata/missing_metadata_test.rego create mode 100644 docs/rules/custom/missing-metadata.md diff --git a/.github/workflows/update-example-index.yaml b/.github/workflows/update-example-index.yaml index a45f2f4a..e21e0c71 100644 --- a/.github/workflows/update-example-index.yaml +++ b/.github/workflows/update-example-index.yaml @@ -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 | \ diff --git a/.regal/config.yaml b/.regal/config.yaml index 5c0c8f2d..1025234a 100644 --- a/.regal/config.yaml +++ b/.regal/config.yaml @@ -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 diff --git a/README.md b/README.md index d2f36d52..10c49287 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/build/simplecov/simplecov.rego b/build/simplecov/simplecov.rego index df16ba44..91942cf6 100644 --- a/build/simplecov/simplecov.rego +++ b/build/simplecov/simplecov.rego @@ -1,12 +1,18 @@ +# 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 @@ -14,7 +20,7 @@ covered_map(report) := cm if { } } -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 @@ -22,32 +28,34 @@ not_covered_map(report) := ncm if { } } -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 } diff --git a/build/workflows/update-example-index/process.rego b/build/workflows/update_example_index.rego similarity index 71% rename from build/workflows/update-example-index/process.rego rename to build/workflows/update_example_index.rego index 275e5e1c..57581bd1 100644 --- a/build/workflows/update-example-index/process.rego +++ b/build/workflows/update_example_index.rego @@ -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 { diff --git a/bundle/regal/ast/ast.rego b/bundle/regal/ast/ast.rego index 9d8d8431..2f44e5d9 100644 --- a/bundle/regal/ast/ast.rego +++ b/bundle/regal/ast/ast.rego @@ -1,3 +1,7 @@ +# 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 @@ -5,8 +9,12 @@ 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", @@ -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] @@ -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 @@ -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] @@ -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" } @@ -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)) @@ -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 @@ -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 @@ -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) +} diff --git a/bundle/regal/ast/ast_test.rego b/bundle/regal/ast/ast_test.rego index c5e0c89c..20d6996f 100644 --- a/bundle/regal/ast/ast_test.rego +++ b/bundle/regal/ast/ast_test.rego @@ -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"} +} diff --git a/bundle/regal/ast/comments.rego b/bundle/regal/ast/comments.rego index 92d14abe..52435e09 100644 --- a/bundle/regal/ast/comments.rego +++ b/bundle/regal/ast/comments.rego @@ -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, diff --git a/bundle/regal/ast/keywords.rego b/bundle/regal/ast/keywords.rego index 4058ad55..724cc5a7 100644 --- a/bundle/regal/ast/keywords.rego +++ b/bundle/regal/ast/keywords.rego @@ -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 diff --git a/bundle/regal/ast/rule_head_locations.rego b/bundle/regal/ast/rule_head_locations.rego deleted file mode 100644 index 17cd582b..00000000 --- a/bundle/regal/ast/rule_head_locations.rego +++ /dev/null @@ -1,21 +0,0 @@ -package regal.ast - -import rego.v1 - -import data.regal.util - -# 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) -} diff --git a/bundle/regal/ast/rule_head_locations_test.rego b/bundle/regal/ast/rule_head_locations_test.rego deleted file mode 100644 index 5bb8f484..00000000 --- a/bundle/regal/ast/rule_head_locations_test.rego +++ /dev/null @@ -1,34 +0,0 @@ -package regal.ast_test - -import rego.v1 - -import data.regal.ast - -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}}, - } -} diff --git a/bundle/regal/ast/search.rego b/bundle/regal/ast/search.rego index 175bd347..5f80f29e 100644 --- a/bundle/regal/ast/search.rego +++ b/bundle/regal/ast/search.rego @@ -52,8 +52,10 @@ _find_some_in_decl_vars(value) := vars if { ] } -# find vars like input[x].foo[y] where x and y are vars -# note: value.type == "ref" check must have been done before calling this function +# METADATA +# description: | +# find vars like input[x].foo[y] where x and y are vars +# note: value.type == "ref" check must have been done before calling this function find_ref_vars(value) := [var | some i, var in value.value @@ -201,6 +203,8 @@ found.vars[rule_index][context] contains var if { some var in vars } +# METADATA +# description: all refs foundd in module found.refs[rule_index] contains value if { some i, rule in _rules @@ -212,6 +216,8 @@ found.refs[rule_index] contains value if { is_ref(value) } +# METADATA +# description: all symbols foundd in module found.symbols[rule_index] contains value.symbols if { some i, rule in _rules @@ -221,6 +227,8 @@ found.symbols[rule_index] contains value.symbols if { walk(rule, [_, value]) } +# METADATA +# description: all comprehensions foundd in module found.comprehensions[rule_index] contains value if { some i, rule in _rules @@ -314,6 +322,8 @@ find_some_decl_names_in_scope(rule, location) := {some_var.value | _before_location(rule, some_var, location) } +# METADATA +# description: all expressions in module exprs[rule_index][expr_index] := expr if { some rule_index, rule in input.rules some expr_index, expr in rule.body diff --git a/bundle/regal/capabilities/capabilities.rego b/bundle/regal/capabilities/capabilities.rego index 856d9545..a8338144 100644 --- a/bundle/regal/capabilities/capabilities.rego +++ b/bundle/regal/capabilities/capabilities.rego @@ -1,3 +1,8 @@ +# METADATA +# description: | +# the capabilities package unsurprisingly helps rule authors deal +# with capabilities, which may have been provided by either Regal, +# or customized by end-users via configuration package regal.capabilities import data.regal.config @@ -14,17 +19,29 @@ default provided := {} # scope: document provided := data.internal.capabilities +# METADATA +# description: true if `object.keys` is available has_object_keys if "object.keys" in object.keys(config.capabilities.builtins) +# METADATA +# description: true if `strings.count` is available has_strings_count if "strings.count" in object.keys(config.capabilities.builtins) # if if if! +# METADATA +# description: true if the `if` keyword is available +# scope: document has_if if "if" in config.capabilities.future_keywords has_if if has_rego_v1_feature +# METADATA +# description: true if the `contains` keyword is available +# scope: document has_contains if "contains" in config.capabilities.future_keywords has_contains if has_rego_v1_feature +# METADATA +# description: true if `rego.v1` is available has_rego_v1_feature if "rego_v1_import" in config.capabilities.features diff --git a/bundle/regal/config/config.rego b/bundle/regal/config/config.rego index 3a447f18..1e73c8a4 100644 --- a/bundle/regal/config/config.rego +++ b/bundle/regal/config/config.rego @@ -1,19 +1,33 @@ +# METADATA +# description: | +# base modules for working with Regal's configuration in Rego +# this includes responsibilities like providing capabilities, or +# to determine which rules to enable/disable, and what files to +# ignore package regal.config import rego.v1 +# METADATA +# description: the base URL for documentation of linter rules docs["base_url"] := "https://docs.styra.com/regal/rules" +# METADATA +# description: returns the canonical URL for documentation of the given rule docs["resolve_url"](url, category) := replace( replace(url, "$baseUrl", docs.base_url), "$category", category, ) +# METADATA +# description: the default configuration with user config merged on top (if provided) merged_config := data.internal.combined_config -capabilities := object.union(merged_config.capabilities, {"special": special}) +# METADATA +# description: the resolved capabilities sourced from Regal and user configuration +capabilities := object.union(merged_config.capabilities, {"special": _special}) -special contains "no_filename" if input.regal.file.name == "stdin" +_special contains "no_filename" if input.regal.file.name == "stdin" default for_rule(_, _) := {"level": "error"} @@ -24,9 +38,9 @@ default for_rule(_, _) := {"level": "error"} # to the rule matching the category and title. # scope: document for_rule(category, title) := _with_level(category, title, "ignore") if { - force_disabled(category, title) + _force_disabled(category, title) } else := _with_level(category, title, "error") if { - force_enabled(category, title) + _force_enabled(category, title) } else := c if { # regal ignore:external-reference m := merged_config.rules[category][title] @@ -39,14 +53,16 @@ _with_level(category, title, level) := c if { c := object.union(m, {"level": level}) } else := {"level": level} +# METADATA +# description: returns the level set for rule, otherwise "error" +# scope: document default rule_level(_) := "error" rule_level(cfg) := cfg.level -# regal ignore:external-reference -force_disabled(_, title) if title in data.eval.params.disable +_force_disabled(_, title) if title in data.eval.params.disable # regal ignore:external-reference -force_disabled(category, title) if { +_force_disabled(category, title) if { # regal ignore:external-reference params := data.eval.params @@ -55,7 +71,7 @@ force_disabled(category, title) if { not title in params.enable } -force_disabled(category, title) if { +_force_disabled(category, title) if { # regal ignore:external-reference params := data.eval.params @@ -64,9 +80,9 @@ force_disabled(category, title) if { } # regal ignore:external-reference -force_enabled(_, title) if title in data.eval.params.enable +_force_enabled(_, title) if title in data.eval.params.enable -force_enabled(category, title) if { +_force_enabled(category, title) if { # regal ignore:external-reference params := data.eval.params @@ -75,7 +91,7 @@ force_enabled(category, title) if { not title in params.disable } -force_enabled(category, title) if { +_force_enabled(category, title) if { # regal ignore:external-reference params := data.eval.params diff --git a/bundle/regal/config/exclusion.rego b/bundle/regal/config/exclusion.rego index ad5eeab0..aa44aac7 100644 --- a/bundle/regal/config/exclusion.rego +++ b/bundle/regal/config/exclusion.rego @@ -2,43 +2,45 @@ package regal.config import rego.v1 +# METADATA +# description: determines if file should be excluded by the given rule excluded_file(category, title, file) if { - force_exclude_file(file) + _force_exclude_file(file) } else if { rule_config := for_rule(category, title) ex := rule_config.ignore.files is_array(ex) some pattern in ex - exclude(pattern, file) + _exclude(pattern, file) } else := false -force_exclude_file(file) if { +_force_exclude_file(file) if { # regal ignore:external-reference - some pattern in global_ignore_patterns - exclude(pattern, file) + some pattern in _global_ignore_patterns + _exclude(pattern, file) } -global_ignore_patterns := merged_config.ignore.files if { +_global_ignore_patterns := merged_config.ignore.files if { not data.eval.params.ignore_files } else := data.eval.params.ignore_files # exclude imitates Gits .gitignore pattern matching as best it can # Ref: https://git-scm.com/docs/gitignore#_pattern_format -exclude(pattern, file) if { - patterns := pattern_compiler(pattern) +_exclude(pattern, file) if { + patterns := _pattern_compiler(pattern) some p in patterns glob.match(p, ["/"], file) } else := false # pattern_compiler transforms a glob pattern into a set of glob patterns to make the # combined set behave as Gits .gitignore -pattern_compiler(pattern) := ps1 if { - p := internal_slashes(pattern) - p1 := leading_slash(p) - ps := leading_doublestar_pattern(p1) +_pattern_compiler(pattern) := ps1 if { + p := _internal_slashes(pattern) + p1 := _leading_slash(p) + ps := _leading_doublestar_pattern(p1) ps1 := {pat | some _p, _ in ps - nps := trailing_slash(_p) + nps := _trailing_slash(_p) some pat, _ in nps } } @@ -48,14 +50,14 @@ pattern_compiler(pattern) := ps1 if { # # myfiledir and mydir/ turns into **/myfiledir and **/mydir/ # mydir/p and mydir/d/ are returned as is -internal_slashes(pattern) := pattern if { +_internal_slashes(pattern) := pattern if { s := substring(pattern, 0, count(pattern) - 1) contains(s, "/") } else := concat("", ["**/", pattern]) # **/pattern might match my/dir/pattern and pattern # So we branch it into itself and one with the leading **/ removed -leading_doublestar_pattern(pattern) := {pattern, p} if { +_leading_doublestar_pattern(pattern) := {pattern, p} if { startswith(pattern, "**/") p := substring(pattern, 3, -1) } else := {pattern} @@ -63,7 +65,7 @@ leading_doublestar_pattern(pattern) := {pattern, p} if { # If a pattern does not end with a "/", then it can both # - match a folder => pattern + "/**" # - match a file => pattern -trailing_slash(pattern) := {pattern, np} if { +_trailing_slash(pattern) := {pattern, np} if { not endswith(pattern, "/") not endswith(pattern, "**") np := concat("", [pattern, "/**"]) @@ -74,6 +76,6 @@ trailing_slash(pattern) := {pattern, np} if { # If a pattern starts with a "/", the leading slash is ignored but according to # the .gitignore rule of internal slashes, it is relative to root -leading_slash(pattern) := substring(pattern, 1, -1) if { +_leading_slash(pattern) := substring(pattern, 1, -1) if { startswith(pattern, "/") } else := pattern diff --git a/bundle/regal/config/exclusion_test.rego b/bundle/regal/config/exclusion_test.rego index 2726fc63..67fd7fe8 100644 --- a/bundle/regal/config/exclusion_test.rego +++ b/bundle/regal/config/exclusion_test.rego @@ -48,7 +48,7 @@ test_all_cases_are_as_expected if { some pattern, subcases in cases res := {file | some file, exp in subcases - act := config.exclude(pattern, file) + act := config._exclude(pattern, file) # regal ignore:leaked-internal-reference exp != act } count(res) > 0 @@ -97,7 +97,7 @@ test_excluded_file_cli_overrides_config if { } test_trailing_slash if { - config.trailing_slash("foo/**/bar") == {"foo/**/bar", "foo/**/bar/**"} - config.trailing_slash("foo") == {"foo", "foo/**"} - config.trailing_slash("foo/**") == {"foo/**"} + config._trailing_slash("foo/**/bar") == {"foo/**/bar", "foo/**/bar/**"} # regal ignore:leaked-internal-reference + config._trailing_slash("foo") == {"foo", "foo/**"} # regal ignore:leaked-internal-reference + config._trailing_slash("foo/**") == {"foo/**"} # regal ignore:leaked-internal-reference } diff --git a/bundle/regal/config/provided/data.yaml b/bundle/regal/config/provided/data.yaml index b7813757..245e91ee 100644 --- a/bundle/regal/config/provided/data.yaml +++ b/bundle/regal/config/provided/data.yaml @@ -53,6 +53,8 @@ rules: forbidden-function-call: forbidden-functions: [] level: ignore + missing-metadata: + level: ignore naming-convention: level: ignore one-liner-rule: @@ -70,8 +72,8 @@ rules: custom-in-construct: level: error directory-package-mismatch: - level: error exclude-test-suffix: true + level: error equals-pattern-matching: level: error no-defined-entrypoint: @@ -158,6 +160,7 @@ rules: level: error prefer-some-in-iteration: ignore-nesting-level: 2 + ignore-if-sub-attribute: true level: error rule-length: count-comments: false diff --git a/bundle/regal/lsp/codelens/codelens.rego b/bundle/regal/lsp/codelens/codelens.rego index 8587b943..7fcfc0ac 100644 --- a/bundle/regal/lsp/codelens/codelens.rego +++ b/bundle/regal/lsp/codelens/codelens.rego @@ -15,6 +15,8 @@ import data.regal.lsp.util.location # code lenses are displayed in the order they come back in the returned # array, and 'evaluate' somehow feels better to the left of 'debug' +# METADATA +# description: contains code lenses determined for module lenses := array.concat( [l | some l in _eval_lenses], [l | some l in _debug_lenses], diff --git a/bundle/regal/lsp/completion/kind/kind.rego b/bundle/regal/lsp/completion/kind/kind.rego index b4f020e7..6ede5b69 100644 --- a/bundle/regal/lsp/completion/kind/kind.rego +++ b/bundle/regal/lsp/completion/kind/kind.rego @@ -1,53 +1,109 @@ +# METADATA +# description: | +# completion kinds, as defined in the LSP specification +# related_resources: +# - description: documentation +# ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind package regal.lsp.completion.kind import rego.v1 +# METADATA +# description: text text := 1 +# METADATA +# description: method method := 2 +# METADATA +# description: function function := 3 +# METADATA +# description: constructor constructor := 4 +# METADATA +# description: field field := 5 +# METADATA +# description: variable variable := 6 +# METADATA +# description: class class := 7 +# METADATA +# description: interface interface := 8 +# METADATA +# description: module module := 9 +# METADATA +# description: property property := 10 +# METADATA +# description: unit unit := 11 +# METADATA +# description: value value := 12 +# METADATA +# description: enum enum := 13 +# METADATA +# description: keyword keyword := 14 +# METADATA +# description: snippet snippet := 15 +# METADATA +# description: color color := 16 +# METADATA +# description: file file := 17 +# METADATA +# description: reference reference := 18 +# METADATA +# description: folder folder := 19 +# METADATA +# description: enum number enum_member := 20 +# METADATA +# description: constant constant := 21 +# METADATA +# description: struct struct := 22 +# METADATA +# description: event event := 23 +# METADATA +# description: operator operator := 24 +# METADATA +# description: type parameter type_parameter := 25 diff --git a/bundle/regal/lsp/completion/main.rego b/bundle/regal/lsp/completion/main.rego index 1651f86d..745f210d 100644 --- a/bundle/regal/lsp/completion/main.rego +++ b/bundle/regal/lsp/completion/main.rego @@ -1,7 +1,7 @@ # METADATA # description: | # base package for completion suggestion provider policies, and acts -# like a router that'll collection suggestions from all provider policies +# like a router that collects suggestions from all provider policies # under regal.lsp.completion.providers package regal.lsp.completion diff --git a/bundle/regal/lsp/completion/providers/booleans/booleans.rego b/bundle/regal/lsp/completion/providers/booleans/booleans.rego index 8e72743b..5a4b5bf4 100644 --- a/bundle/regal/lsp/completion/providers/booleans/booleans.rego +++ b/bundle/regal/lsp/completion/providers/booleans/booleans.rego @@ -1,3 +1,5 @@ +# METADATA +# description: the boolean provider suggests `true`/`false` values where appropriate package regal.lsp.completion.providers.booleans import rego.v1 @@ -5,6 +7,8 @@ import rego.v1 import data.regal.lsp.completion.kind import data.regal.lsp.completion.location +# METADATA +# description: completion suggestions for true/false items contains item if { position := location.to_position(input.regal.context.location) diff --git a/bundle/regal/lsp/completion/providers/commonrule/commonrule.rego b/bundle/regal/lsp/completion/providers/commonrule/commonrule.rego index 88d776c2..99515266 100644 --- a/bundle/regal/lsp/completion/providers/commonrule/commonrule.rego +++ b/bundle/regal/lsp/completion/providers/commonrule/commonrule.rego @@ -1,3 +1,6 @@ +# METADATA +# description: | +# provides completions for common rule names, like 'allow' or 'deny' package regal.lsp.completion.providers.commonrule import rego.v1 @@ -5,17 +8,19 @@ import rego.v1 import data.regal.lsp.completion.kind import data.regal.lsp.completion.location -suggested_names := { +_suggested_names := { "allow", "authorized", "deny", } +# METADATA +# description: all completion suggestions for common rule names items contains item if { position := location.to_position(input.regal.context.location) line := input.regal.file.lines[position.line] - some label in suggested_names + some label in _suggested_names startswith(label, line) diff --git a/bundle/regal/lsp/completion/providers/default/default.rego b/bundle/regal/lsp/completion/providers/default/default.rego index 897dd00a..52900094 100644 --- a/bundle/regal/lsp/completion/providers/default/default.rego +++ b/bundle/regal/lsp/completion/providers/default/default.rego @@ -1,3 +1,5 @@ +# METADATA +# description: provides completion suggestions for the `default` keyword where applicable package regal.lsp.completion.providers["default"] import rego.v1 @@ -7,6 +9,9 @@ import data.regal.ast import data.regal.lsp.completion.kind import data.regal.lsp.completion.location +# METADATA +# description: all completion suggestions for default keyword +# scope: document items contains item if { position := location.to_position(input.regal.context.location) line := input.regal.file.lines[position.line] diff --git a/bundle/regal/lsp/completion/providers/import/import.rego b/bundle/regal/lsp/completion/providers/import/import.rego index 79374eab..49e24df0 100644 --- a/bundle/regal/lsp/completion/providers/import/import.rego +++ b/bundle/regal/lsp/completion/providers/import/import.rego @@ -1,3 +1,5 @@ +# METADATA +# description: provides completion suggestions for the `import` keyword where applicable package regal.lsp.completion.providers["import"] import rego.v1 @@ -5,6 +7,8 @@ import rego.v1 import data.regal.lsp.completion.kind import data.regal.lsp.completion.location +# METADATA +# description: all completion suggestions for the import keyword items contains item if { position := location.to_position(input.regal.context.location) line := input.regal.file.lines[position.line] diff --git a/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson.rego b/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson.rego index c5aa78d7..e59cbcd3 100644 --- a/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson.rego +++ b/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson.rego @@ -1,3 +1,15 @@ +# METADATA +# description: | +# the `inputdotjson` provider returns suggestions based on the `input.json` +# data structure (if such a file is found), so that e.g. content like: +# ```json +# { +# "user": {"roles": ["admin"]}, +# "request": {"method": "GEt"} +# } +# ``` +# would suggest `input.user`, `input.user.roles`, `input.request`, +# `input.request.method, and so on package regal.lsp.completion.providers.inputdotjson import rego.v1 @@ -6,7 +18,7 @@ import data.regal.lsp.completion.kind import data.regal.lsp.completion.location # METADATA -# description: returns suggestions based on input.json structure (if found) +# description: items contains found suggestions from `input.json`` items contains item if { input.regal.context.input_dot_json_path diff --git a/bundle/regal/lsp/completion/providers/locals/locals.rego b/bundle/regal/lsp/completion/providers/locals/locals.rego index 418269ff..553aa108 100644 --- a/bundle/regal/lsp/completion/providers/locals/locals.rego +++ b/bundle/regal/lsp/completion/providers/locals/locals.rego @@ -1,3 +1,5 @@ +# METADATA +# description: provides completion suggestions for local symbols in scope package regal.lsp.completion.providers.locals import rego.v1 @@ -5,8 +7,8 @@ import rego.v1 import data.regal.lsp.completion.kind import data.regal.lsp.completion.location -parsed_current_file := data.workspace.parsed[input.regal.file.uri] - +# METADATA +# description: completion suggestions for local symbols items contains item if { position := location.to_position(input.regal.context.location) @@ -18,6 +20,7 @@ items contains item if { not _excluded(line, position) word := location.word_at(line, input.regal.context.location.col) + parsed_current_file := data.workspace.parsed[input.regal.file.uri] some local in location.find_locals( parsed_current_file.rules, diff --git a/bundle/regal/lsp/completion/providers/locals/locals_test.rego b/bundle/regal/lsp/completion/providers/locals/locals_test.rego index a13007ce..78441af4 100644 --- a/bundle/regal/lsp/completion/providers/locals/locals_test.rego +++ b/bundle/regal/lsp/completion/providers/locals/locals_test.rego @@ -60,8 +60,8 @@ function(bar) if { items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 2 - utils.expect_item(items, "bar", {"end": {"character": 9, "line": 8}, "start": {"character": 8, "line": 8}}) - utils.expect_item(items, "baz", {"end": {"character": 9, "line": 8}, "start": {"character": 8, "line": 8}}) + _expect_item(items, "bar", {"end": {"character": 9, "line": 8}, "start": {"character": 8, "line": 8}}) + _expect_item(items, "baz", {"end": {"character": 9, "line": 8}, "start": {"character": 8, "line": 8}}) } test_locals_in_completion_items_function_call if { @@ -92,8 +92,8 @@ function(bar) if { items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 2 - utils.expect_item(items, "bar", {"end": {"character": 24, "line": 8}, "start": {"character": 23, "line": 8}}) - utils.expect_item(items, "baz", {"end": {"character": 24, "line": 8}, "start": {"character": 23, "line": 8}}) + _expect_item(items, "bar", {"end": {"character": 24, "line": 8}, "start": {"character": 23, "line": 8}}) + _expect_item(items, "baz", {"end": {"character": 24, "line": 8}, "start": {"character": 23, "line": 8}}) } test_locals_in_completion_items_rule_head_assignment if { @@ -120,7 +120,7 @@ function(bar) := f if { items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 1 - utils.expect_item(items, "foo", {"end": {"character": 18, "line": 4}, "start": {"character": 17, "line": 4}}) + _expect_item(items, "foo", {"end": {"character": 18, "line": 4}, "start": {"character": 17, "line": 4}}) } test_no_locals_in_completion_items_function_args if { @@ -148,3 +148,17 @@ function() if { count(items) == 0 } + +_expect_item(items, label, range) if { + expected := {"detail": "local variable", "kind": 6} + + item := object.union(expected, { + "label": label, + "textEdit": { + "newText": label, + "range": range, + }, + }) + + item in items +} diff --git a/bundle/regal/lsp/completion/providers/package/package.rego b/bundle/regal/lsp/completion/providers/package/package.rego index d72c124e..bd722ee2 100644 --- a/bundle/regal/lsp/completion/providers/package/package.rego +++ b/bundle/regal/lsp/completion/providers/package/package.rego @@ -1,3 +1,5 @@ +# METADATA +# description: provides completion suggestions for the `package` keyword where applicable package regal.lsp.completion.providers["package"] import rego.v1 @@ -5,6 +7,8 @@ import rego.v1 import data.regal.lsp.completion.kind import data.regal.lsp.completion.location +# METADATA +# description: completion suggestions for package keyword items contains item if { not strings.any_prefix_match(input.regal.file.lines, "package ") diff --git a/bundle/regal/lsp/completion/providers/packagename/packagename.rego b/bundle/regal/lsp/completion/providers/packagename/packagename.rego index 76b3bb2f..81619191 100644 --- a/bundle/regal/lsp/completion/providers/packagename/packagename.rego +++ b/bundle/regal/lsp/completion/providers/packagename/packagename.rego @@ -1,3 +1,7 @@ +# METADATA +# description: | +# the `packagename` providers suggests completions for package +# name based on the directory structure whre the file is located package regal.lsp.completion.providers.packagename import rego.v1 @@ -5,6 +9,8 @@ import rego.v1 import data.regal.lsp.completion.kind import data.regal.lsp.completion.location +# METADATA +# description: set of suggested package names items contains item if { position := location.to_position(input.regal.context.location) line := input.regal.file.lines[position.line] @@ -14,13 +20,13 @@ items contains item if { ps := input.regal.context.path_separator - abs_dir := base(input.regal.file.name) + abs_dir := _base(input.regal.file.name) rel_dir := trim_prefix(abs_dir, input.regal.context.workspace_root) fix_dir := replace(replace(trim_prefix(rel_dir, ps), ".", "_"), ps, ".") word := location.ref_at(line, input.regal.context.location.col) - some suggestion in suggestions(fix_dir, word) + some suggestion in _suggestions(fix_dir, word) item := { "label": suggestion, @@ -33,9 +39,9 @@ items contains item if { } } -base(path) := substring(path, 0, regal.last(indexof_n(path, "/"))) +_base(path) := substring(path, 0, regal.last(indexof_n(path, "/"))) -suggestions(dir, word) := [path | +_suggestions(dir, word) := [path | parts := split(dir, ".") len_p := count(parts) some n in numbers.range(0, len_p) diff --git a/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego b/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego index 780a68e6..207cedb5 100644 --- a/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego +++ b/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego @@ -140,13 +140,19 @@ test_package_name_completion_on_typing_multiple_suggestions_when_invoked if { } test_build_suggestions if { - provider.suggestions("foo.bar.baz", {"text": "foo"}) == ["foo.bar.baz"] - provider.suggestions("foo.bar.baz", {"text": "bar"}) == ["bar.baz"] - provider.suggestions("foo.bar.baz", {"text": "ba"}) == ["bar.baz", "baz"] + # regal ignore:leaked-internal-reference + provider._suggestions("foo.bar.baz", {"text": "foo"}) == ["foo.bar.baz"] + + # regal ignore:leaked-internal-reference + provider._suggestions("foo.bar.baz", {"text": "bar"}) == ["bar.baz"] + + # regal ignore:leaked-internal-reference + provider._suggestions("foo.bar.baz", {"text": "ba"}) == ["bar.baz", "baz"] } test_build_suggestions_invoked if { - provider.suggestions("foo.bar.baz", {"text": ""}) == [ + # regal ignore:leaked-internal-reference + provider._suggestions("foo.bar.baz", {"text": ""}) == [ "foo.bar.baz", "bar.baz", "baz", diff --git a/bundle/regal/lsp/completion/providers/regov1/regov1.rego b/bundle/regal/lsp/completion/providers/regov1/regov1.rego index 95d9d11e..e74d761d 100644 --- a/bundle/regal/lsp/completion/providers/regov1/regov1.rego +++ b/bundle/regal/lsp/completion/providers/regov1/regov1.rego @@ -1,3 +1,7 @@ +# METADATA +# description: | +# the `regov1`` provider provides completion suggestions for +# `rego.v1` following an `import` declaration package regal.lsp.completion.providers.regov1 import rego.v1 @@ -5,6 +9,8 @@ import rego.v1 import data.regal.lsp.completion.kind import data.regal.lsp.completion.location +# METADATA +# description: completion suggestion for rego.v1 items contains item if { not strings.any_prefix_match(input.regal.file.lines, "import rego.v1") diff --git a/bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego b/bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego index 7cb6027f..d296d92f 100644 --- a/bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego +++ b/bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego @@ -1,3 +1,5 @@ +# METADATA +# description: provides completion suggestions for rules in the workspace package regal.lsp.completion.providers.rulerefs import rego.v1 @@ -7,63 +9,63 @@ import data.regal.ast import data.regal.lsp.completion.kind import data.regal.lsp.completion.location -ref_is_internal(ref) if contains(ref, "._") +_ref_is_internal(ref) if contains(ref, "._") -position := location.to_position(input.regal.context.location) +_position := location.to_position(input.regal.context.location) -line := input.regal.file.lines[position.line] +_line := input.regal.file.lines[_position.line] -word := location.ref_at(line, input.regal.context.location.col) +_word := location.ref_at(_line, input.regal.context.location.col) -workspace_rule_refs contains ref if { +_workspace_rule_refs contains ref if { some refs in data.workspace.defined_refs some ref in refs } -parsed_current_file := data.workspace.parsed[input.regal.file.uri] +_parsed_current_file := data.workspace.parsed[input.regal.file.uri] -current_file_package := ast.ref_to_string(parsed_current_file["package"].path) +_current_file_package := ast.ref_to_string(_parsed_current_file["package"].path) -current_file_imports contains ref if { - some imp in parsed_current_file.imports +_current_file_imports contains ref if { + some imp in _parsed_current_file.imports ref := ast.ref_to_string(imp.path.value) } -current_package_refs contains ref if { - some ref in workspace_rule_refs +_current_package_refs contains ref if { + some ref in _workspace_rule_refs - startswith(ref, current_file_package) + startswith(ref, _current_file_package) } -imported_package_refs contains ref if { - some ref in workspace_rule_refs +_imported_package_refs contains ref if { + some ref in _workspace_rule_refs - not ref_is_internal(ref) + not _ref_is_internal(ref) - strings.any_prefix_match(ref, current_file_imports) + strings.any_prefix_match(ref, _current_file_imports) } -other_package_refs contains ref if { - some ref in workspace_rule_refs +_other_package_refs contains ref if { + some ref in _workspace_rule_refs - not ref in imported_package_refs - not ref in current_package_refs + not ref in _imported_package_refs + not ref in _current_package_refs - not ref_is_internal(ref) + not _ref_is_internal(ref) } # from the current package -rule_ref_suggestions contains pkg_ref if { - some ref in current_package_refs +_rule_ref_suggestions contains pkg_ref if { + some ref in _current_package_refs - pkg_ref := trim_prefix(ref, sprintf("%s.", [current_file_package])) + pkg_ref := trim_prefix(ref, sprintf("%s.", [_current_file_package])) } # from imported packages -rule_ref_suggestions contains pkg_ref if { - some ref in imported_package_refs - some imported_package in current_file_imports +_rule_ref_suggestions contains pkg_ref if { + some ref in _imported_package_refs + some imported_package in _current_file_imports startswith(ref, imported_package) @@ -72,40 +74,42 @@ rule_ref_suggestions contains pkg_ref if { } # from any other package -rule_ref_suggestions contains ref if some ref in other_package_refs +_rule_ref_suggestions contains ref if some ref in _other_package_refs # also suggest the unimported packages themselves # e.g. data.foo.rule will also generate data.foo as a suggestion -rule_ref_suggestions contains pkg if { - some ref in other_package_refs +_rule_ref_suggestions contains pkg if { + some ref in _other_package_refs pkg := regex.replace(ref, `\.[^\.]+$`, "") } -matching_rule_ref_suggestions contains ref if { - line != "" - location.in_rule_body(line) +_matching_rule_ref_suggestions contains ref if { + _line != "" + location.in_rule_body(_line) # \W is used here to match ( in the case of func() := ..., as well as the space in the case of rule := ... - first_word := regex.split(`\W+`, trim_space(line))[0] + first_word := regex.split(`\W+`, trim_space(_line))[0] - some ref in rule_ref_suggestions + some ref in _rule_ref_suggestions - startswith(ref, word.text) + startswith(ref, _word.text) # this is to avoid suggesting a recursive rule, e.g. rule := rule, or func() := func() ref != first_word } +# METADATA +# description: set of completion suggestions for references to rules items contains item if { - some ref in matching_rule_ref_suggestions + some ref in _matching_rule_ref_suggestions item := { "label": ref, "kind": kind.variable, "detail": "reference", "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(_word, _position), "newText": ref, }, } diff --git a/bundle/regal/lsp/completion/providers/snippet/snippet.rego b/bundle/regal/lsp/completion/providers/snippet/snippet.rego index 5fe3b480..81869f0a 100644 --- a/bundle/regal/lsp/completion/providers/snippet/snippet.rego +++ b/bundle/regal/lsp/completion/providers/snippet/snippet.rego @@ -1,3 +1,11 @@ +# METADATA +# description: | +# provides completion suggestions for **snippets** in locations where +# it makes sense to do so. note that while snippets originally were specific +# to VS Code, they are also supported by e.g. Zed and other editors +# related_resources: +# - description: documentation +# ref: https://code.visualstudio.com/docs/editor/userdefinedsnippets package regal.lsp.completion.providers.snippet import rego.v1 @@ -5,6 +13,9 @@ import rego.v1 import data.regal.lsp.completion.kind import data.regal.lsp.completion.location +# METADATA +# description: all completion suggestions for snippets +# scope: document items contains item if { position := location.to_position(input.regal.context.location) line := input.regal.file.lines[position.line] @@ -39,16 +50,30 @@ items contains item if { word := location.word_at(line, input.regal.context.location.col) - item := { - "label": "metadata annotation (snippet)", - "kind": kind.snippet, - "detail": "metadata annotation", - "textEdit": { - "range": location.word_range(word, position), - "newText": "# METADATA\n# title: ${1:title}\n# description: ${2:description}", + items := { + { + "label": "metadata annotation [title, description] (snippet)", + "kind": kind.snippet, + "detail": "metadata annotation", + "textEdit": { + "range": location.word_range(word, position), + "newText": "# METADATA\n# title: ${1:title}\n# description: ${2:description}", + }, + "insertTextFormat": 2, # snippet + }, + { + "label": "metadata annotation [description] (snippet)", + "kind": kind.snippet, + "detail": "metadata annotation", + "textEdit": { + "range": location.word_range(word, position), + "newText": "# METADATA\n# description: ${1:description}", + }, + "insertTextFormat": 2, # snippet }, - "insertTextFormat": 2, # snippet } + + some item in items } _snippets := { diff --git a/bundle/regal/lsp/completion/providers/snippet/snippet_test.rego b/bundle/regal/lsp/completion/providers/snippet/snippet_test.rego index 827aad30..959982a0 100644 --- a/bundle/regal/lsp/completion/providers/snippet/snippet_test.rego +++ b/bundle/regal/lsp/completion/providers/snippet/snippet_test.rego @@ -85,7 +85,6 @@ allow if { } } -# regal ignore:rule-length test_snippet_completion_on_typing_no_repeat if { policy := `package policy @@ -163,6 +162,7 @@ allow if ` } } +# regal ignore:rule-length test_metadata_snippet_completion if { policy := `package policy @@ -171,17 +171,32 @@ import rego.v1 ` items := provider.items with input as util.input_with_location(policy, {"row": 5, "col": 1}) - items == {{ - "detail": "metadata annotation", - "insertTextFormat": 2, - "kind": 15, - "label": "metadata annotation (snippet)", - "textEdit": { - "newText": "# METADATA\n# title: ${1:title}\n# description: ${2:description}", - "range": { - "end": {"character": 0, "line": 4}, - "start": {"character": 0, "line": 4}, + items == { + { + "detail": "metadata annotation", + "insertTextFormat": 2, + "kind": 15, + "label": "metadata annotation [description] (snippet)", + "textEdit": { + "newText": "# METADATA\n# description: ${1:description}", + "range": { + "end": {"character": 0, "line": 4}, + "start": {"character": 0, "line": 4}, + }, }, }, - }} + { + "detail": "metadata annotation", + "insertTextFormat": 2, + "kind": 15, + "label": "metadata annotation [title, description] (snippet)", + "textEdit": { + "newText": "# METADATA\n# title: ${1:title}\n# description: ${2:description}", + "range": { + "end": {"character": 0, "line": 4}, + "start": {"character": 0, "line": 4}, + }, + }, + }, + } } diff --git a/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego b/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego index 461f9e21..9e59124e 100644 --- a/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego +++ b/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego @@ -1,26 +1,18 @@ +# METADATA +# description: various helpers to be used for testing completions providers package regal.lsp.completion.providers.test_utils import rego.v1 +# METADATA +# description: returns a map of all parsed modules in the workspace parsed_modules(workspace) := {file_uri: parsed_module | some file_uri, contents in workspace parsed_module := regal.parse_module(file_uri, contents) } -expect_item(items, label, range) if { - expected := {"detail": "local variable", "kind": 6} - - item := object.union(expected, { - "label": label, - "textEdit": { - "newText": label, - "range": range, - }, - }) - - item in items -} - +# METADATA +# description: adds location metadata to provided module, to be used as input input_module_with_location(module, policy, location) := object.union(module, {"regal": { "file": { "name": "p.rego", @@ -29,6 +21,8 @@ input_module_with_location(module, policy, location) := object.union(module, {"r "context": {"location": location}, }}) +# METADATA +# description: same as input_module_with_location, but accepts text content rather than a module input_with_location(policy, location) := {"regal": { "file": { "name": "p.rego", diff --git a/bundle/regal/lsp/completion/ref_names.rego b/bundle/regal/lsp/completion/ref_names.rego index 527cbb57..5bbf471d 100644 --- a/bundle/regal/lsp/completion/ref_names.rego +++ b/bundle/regal/lsp/completion/ref_names.rego @@ -4,8 +4,11 @@ import rego.v1 import data.regal.ast -# ref_names returns a list of ref names that are used in the module. -# built-in functions are not included as they are provided by another completions provider. +# METADATA +# description: | +# returns a list of ref names that are used in the module +# built-in functions are not included as they are provided by another completions provider +# scope: document ref_names contains name if { name := ast.ref_static_to_string(ast.found.refs[_][_].value) diff --git a/bundle/regal/lsp/util/location/location.rego b/bundle/regal/lsp/util/location/location.rego index c3c0a2ad..edcae9da 100644 --- a/bundle/regal/lsp/util/location/location.rego +++ b/bundle/regal/lsp/util/location/location.rego @@ -1,3 +1,5 @@ +# METADATA +# description: utility functions for dealing with location data in the LSP package regal.lsp.util.location import rego.v1 diff --git a/bundle/regal/main/main.rego b/bundle/regal/main/main.rego index af2625d8..b86cfa78 100644 --- a/bundle/regal/main/main.rego +++ b/bundle/regal/main/main.rego @@ -1,3 +1,11 @@ +# METADATA +# description: | +# the `main` package contains the entrypoints for linting, and routes +# requests for linting to linter rules based on the active configuration +# --- +# linter rules either **aggregate** data or **report** violations, where +# the former is a way to find violations that can't be determined in the +# scope of a single file package regal.main import rego.v1 @@ -6,17 +14,27 @@ import data.regal.ast import data.regal.config import data.regal.util -lint.notices := notices +# METADATA +# description: set of all notices returned from linter rules +lint.notices := _notices +# METADATA +# description: map of all aggregated data from aggregate rules, keyed by category/title lint.aggregates := aggregate +# METADATA +# description: map of all ignore directives encountered when linting lint.ignore_directives[input.regal.file.name] := ast.ignore_directives +# METADATA +# description: all violations from aggregate rules lint_aggregate.violations := aggregate_report +# METADATA +# description: all violations from non-aggregate rules lint.violations := report -rules_to_run[category] contains title if { +_rules_to_run[category] contains title if { some category, title config.merged_config.rules[category][title] @@ -24,11 +42,11 @@ rules_to_run[category] contains title if { not config.excluded_file(category, title, input.regal.file.name) } -notices contains grouped_notices[_][_][_] +_notices contains _grouped_notices[_][_][_] -grouped_notices[category][title] contains notice if { +_grouped_notices[category][title] contains notice if { some category, title - rules_to_run[category][title] + _rules_to_run[category][title] some notice in data.regal.rules[category][title].notices } @@ -61,13 +79,13 @@ report contains violation if { # Check bundled rules report contains violation if { some category, title - rules_to_run[category][title] + _rules_to_run[category][title] - count(object.get(grouped_notices, [category, title], [])) == 0 + count(object.get(_grouped_notices, [category, title], [])) == 0 some violation in data.regal.rules[category][title].report - not ignored(violation, ast.ignore_directives) + not _ignored(violation, ast.ignore_directives) } # Check custom rules @@ -78,20 +96,24 @@ report contains violation if { config.for_rule(category, title).level != "ignore" not config.excluded_file(category, title, input.regal.file.name) - not ignored(violation, ast.ignore_directives) + not _ignored(violation, ast.ignore_directives) } -# Collect aggregates in bundled rules +# METADATA +# description: collects aggregates in bundled rules +# scope: rule aggregate[category_title] contains entry if { some category, title - rules_to_run[category][title] + _rules_to_run[category][title] some entry in data.regal.rules[category][title].aggregate category_title := concat("/", [category, title]) } -# Collect aggregates in custom rules +# METADATA +# description: collects aggregates in custom rules +# scope: rule aggregate[category_title] contains entry if { some category, title @@ -109,7 +131,7 @@ aggregate[category_title] contains entry if { # - input: schema.regal.aggregate aggregate_report contains violation if { some category, title - rules_to_run[category][title] + _rules_to_run[category][title] key := concat("/", [category, title]) input_for_rule := object.remove( @@ -120,9 +142,12 @@ aggregate_report contains violation if { # regal ignore:with-outside-test-context some violation in data.regal.rules[category][title].aggregate_report with input as input_for_rule - ignore_directives := object.get(input.ignore_directives, violation.location.file, {}) + # some aggregate violations won't have a location at all, like no-defined-entrypoint + file := object.get(violation, ["location", "file"], "") + + ignore_directives := object.get(input.ignore_directives, file, {}) - not ignored(violation, util.keys_to_numbers(ignore_directives)) + not _ignored(violation, util.keys_to_numbers(ignore_directives)) } # METADATA @@ -149,15 +174,15 @@ aggregate_report contains violation if { file := object.get(violation, ["location", "file"], "") ignore_directives := object.get(input, ["ignore_directives", file], {}) - not ignored(violation, util.keys_to_numbers(ignore_directives)) + not _ignored(violation, util.keys_to_numbers(ignore_directives)) } -ignored(violation, directives) if { +_ignored(violation, directives) if { ignored_rules := directives[util.to_location_object(violation.location).row] violation.title in ignored_rules } -ignored(violation, directives) if { +_ignored(violation, directives) if { ignored_rules := directives[util.to_location_object(violation.location).row + 1] violation.title in ignored_rules } diff --git a/bundle/regal/main/main_test.rego b/bundle/regal/main/main_test.rego index c90beffa..0d506933 100644 --- a/bundle/regal/main/main_test.rego +++ b/bundle/regal/main/main_test.rego @@ -242,7 +242,7 @@ test_camelcase if { violation := util.single_set_item(result.report) violation.title == "prefer-snake-case" - {notice.title | some notice in result.notices} == {"file-missing-test-suffix", "directory-package-mismatch"} + {notice.title | some notice in result.lint.notices} == {"file-missing-test-suffix", "directory-package-mismatch"} } # regal ignore:rule-length @@ -313,7 +313,8 @@ test_main_lint if { test_rules_to_run_not_excluded if { cfg := {"rules": {"testing": {"test": {"level": "error"}}}} - rules_to_run := main.rules_to_run with config.merged_config as cfg + # regal ignore:leaked-internal-reference + rules_to_run := main._rules_to_run with config.merged_config as cfg with config.for_rule as {"level": "error"} with input.regal.file.name as "p.rego" with config.excluded_file as false @@ -330,7 +331,8 @@ test_notices if { "severity": "none", } - notices := main.notices with main.rules_to_run as {"idiomatic": {"testme"}} + # regal ignore:leaked-internal-reference + notices := main.lint.notices with main._rules_to_run as {"idiomatic": {"testme"}} with data.regal.rules.idiomatic.testme.notices as {notice} notices == {notice} @@ -356,7 +358,8 @@ test_report_custom_rule_failure if { } test_aggregate_bundled_rule if { - agg := main.aggregate with main.rules_to_run as {"foo": {"bar"}} + # regal ignore:leaked-internal-reference + agg := main.aggregate with main._rules_to_run as {"foo": {"bar"}} with data.regal.rules as {"foo": {"bar": {"aggregate": {"baz"}}}} agg == {"foo/bar": {"baz"}} diff --git a/bundle/regal/result/result.rego b/bundle/regal/result/result.rego index 134c5168..c57eb520 100644 --- a/bundle/regal/result/result.rego +++ b/bundle/regal/result/result.rego @@ -1,3 +1,9 @@ +# METADATA +# description: | +# utility functions related to return a result from linter rules +# policy authors are encouraged to use these over manually building +# the expected objects, as using these functions should continure to +# work across upgrades — i.e. if the result format changes package regal.result import rego.v1 diff --git a/bundle/regal/rules/bugs/duplicate-rule/duplicate_rule.rego b/bundle/regal/rules/bugs/duplicate-rule/duplicate_rule.rego index df8d16f5..d21c41c1 100644 --- a/bundle/regal/rules/bugs/duplicate-rule/duplicate_rule.rego +++ b/bundle/regal/rules/bugs/duplicate-rule/duplicate_rule.rego @@ -8,7 +8,7 @@ import data.regal.result import data.regal.util report contains violation if { - some indices in duplicates + some indices in _duplicates first := indices[0] @@ -19,13 +19,13 @@ report contains violation if { violation := result.fail(rego.metadata.chain(), object.union( result.ranged_location_from_text(input.rules[first]), - {"description": message(dup_locations)}, + {"description": _message(dup_locations)}, )) } -message(locations) := sprintf("Duplicate rule found at line %d", [locations[0].row]) if count(locations) == 1 +_message(locations) := sprintf("Duplicate rule found at line %d", [locations[0].row]) if count(locations) == 1 -message(locations) := sprintf( +_message(locations) := sprintf( "Duplicate rules found at lines %s", [concat(", ", [line | some location in locations @@ -35,13 +35,13 @@ message(locations) := sprintf( count(locations) > 1 } -rules_as_text := [base64.decode(util.to_location_object(rule.location).text) | some rule in input.rules] +_rules_as_text := [base64.decode(util.to_location_object(rule.location).text) | some rule in input.rules] -duplicates contains indices if { +_duplicates contains indices if { # Remove whitespace from textual representation of rule and create a hash from the result. # This provides a decent, and importantly *cheap*, approximation of duplicates. We can then # parse the text of these suspected duplicate rules to get a more exact result. - rules_hashed := [crypto.md5(regex.replace(text, `\s+`, "")) | some text in rules_as_text] + rules_hashed := [crypto.md5(regex.replace(text, `\s+`, "")) | some text in _rules_as_text] some possible_duplicates in util.find_duplicates(rules_hashed) @@ -49,7 +49,7 @@ duplicates contains indices if { asts := {index: ast | some index in possible_duplicates - module := sprintf("package p\n\nimport rego.v1\n\n%s", [rules_as_text[index]]) + module := sprintf("package p\n\nimport rego.v1\n\n%s", [_rules_as_text[index]]) # note that we _don't_ use regal.parse_module here, as we do not want location # information — only the structure of the AST must match diff --git a/bundle/regal/rules/bugs/impossible-not/impossible_not.rego b/bundle/regal/rules/bugs/impossible-not/impossible_not.rego index b98bbd30..7a973c0f 100644 --- a/bundle/regal/rules/bugs/impossible-not/impossible_not.rego +++ b/bundle/regal/rules/bugs/impossible-not/impossible_not.rego @@ -11,7 +11,7 @@ import data.regal.util # note: not ast.package_path as we want the "data" component here _package_path := [part.value | some part in input["package"].path] -multivalue_rules contains path if { +_multivalue_rules contains path if { some rule in ast.rules rule.head.key @@ -25,7 +25,7 @@ multivalue_rules contains path if { path := concat(".", array.concat(_package_path, [ref.value | some ref in rule.head.ref])) } -negated_refs contains negated_ref if { +_negated_refs contains negated_ref if { some rule, value ast.negated_expressions[rule][value] @@ -33,7 +33,7 @@ negated_refs contains negated_ref if { is_object(value.terms) value.terms.type in {"ref", "var"} - ref := var_to_ref(value.terms) + ref := _var_to_ref(value.terms) # for now, ignore ref if it has variable components every path in util.rest(ref) { @@ -48,14 +48,16 @@ negated_refs contains negated_ref if { negated_ref := { "ref": ref, - "resolved_path": resolve(ref, _package_path, ast.resolved_imports), + "resolved_path": _resolve(ref, _package_path, ast.resolved_imports), } } +# METADATA +# description: collects imported symbols, multi-value rules and negated refs aggregate contains result.aggregate(rego.metadata.chain(), { "imported_symbols": ast.resolved_imports, - "multivalue_rules": multivalue_rules, - "negated_refs": negated_refs, + "multivalue_rules": _multivalue_rules, + "negated_refs": _negated_refs, }) report contains violation if { @@ -69,7 +71,7 @@ report contains violation if { # 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)]), + "text": sprintf("not %s", [_to_string(negated.ref)]), }}) violation := result.fail(rego.metadata.chain(), loc) @@ -98,22 +100,22 @@ aggregate_report contains violation if { # 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)]), + "text": sprintf("not %s", [_to_string(negated.ref)]), }}) violation := result.fail(rego.metadata.chain(), loc) } -var_to_ref(terms) := [terms] if terms.type == "var" +_var_to_ref(terms) := [terms] if terms.type == "var" -var_to_ref(terms) := terms.value if terms.type == "ref" +_var_to_ref(terms) := terms.value if terms.type == "ref" -to_string(ref) := concat(".", [part.value | some part in ref]) +_to_string(ref) := concat(".", [part.value | some part in ref]) -resolve(ref, _, _) := to_string(ref) if ref[0].value == "data" +_resolve(ref, _, _) := _to_string(ref) if ref[0].value == "data" # imported symbol -resolve(ref, _, imported_symbols) := concat(".", resolved) if { +_resolve(ref, _, imported_symbols) := concat(".", resolved) if { ref[0].value != "data" resolved := array.concat( @@ -123,7 +125,7 @@ resolve(ref, _, imported_symbols) := concat(".", resolved) if { } # not imported — must be local or package -resolve(ref, pkg_path, imported_symbols) := concat(".", resolved) if { +_resolve(ref, pkg_path, imported_symbols) := concat(".", resolved) if { ref[0].value != "data" not imported_symbols[ref[0].value] diff --git a/bundle/regal/rules/bugs/inconsistent-args/inconsistent_args.rego b/bundle/regal/rules/bugs/inconsistent-args/inconsistent_args.rego index 9aa5b81f..c1c9c16b 100644 --- a/bundle/regal/rules/bugs/inconsistent-args/inconsistent_args.rego +++ b/bundle/regal/rules/bugs/inconsistent-args/inconsistent_args.rego @@ -15,11 +15,11 @@ report contains violation if { # to have block level scoped ignore directives... function_args_by_name := {name: args_list | some i - name := ast.ref_to_string(ast.functions[i].head.ref) # regal ignore:prefer-some-in-iteration + name := ast.ref_to_string(ast.functions[i].head.ref) args_list := [args | some j - ast.ref_to_string(ast.functions[j].head.ref) == name # regal ignore:prefer-some-in-iteration - args := ast.functions[j].head.args # regal ignore:prefer-some-in-iteration + ast.ref_to_string(ast.functions[j].head.ref) == name + args := ast.functions[j].head.args ] count(args_list) > 1 } @@ -34,9 +34,9 @@ report contains violation if { some position in by_position - inconsistent_args(position) + _inconsistent_args(position) - violation := result.fail(rego.metadata.chain(), _args_location(find_function_by_name(name))) + violation := result.fail(rego.metadata.chain(), _args_location(_find_function_by_name(name))) } _args_location(fn) := loc if { @@ -57,7 +57,7 @@ _args_location(fn) := loc if { }}) } -inconsistent_args(position) if { +_inconsistent_args(position) if { named_vars := {arg.value | some arg in position arg.type == "var" @@ -68,7 +68,7 @@ inconsistent_args(position) if { # Return the _second_ function found by name, as that # is reasonably the location the inconsistency is found -find_function_by_name(name) := [fn | +_find_function_by_name(name) := [fn | some fn in ast.functions ast.ref_to_string(fn.head.ref) == name ][1] diff --git a/bundle/regal/rules/bugs/top-level-iteration/top_level_iteration.rego b/bundle/regal/rules/bugs/top-level-iteration/top_level_iteration.rego index dcb937cf..0b700a23 100644 --- a/bundle/regal/rules/bugs/top-level-iteration/top_level_iteration.rego +++ b/bundle/regal/rules/bugs/top-level-iteration/top_level_iteration.rego @@ -22,20 +22,20 @@ report contains violation if { last := regal.last(rule.head.value.value) last.type == "var" - illegal_value_ref(last.value, rule, ast.identifiers) + _illegal_value_ref(last.value, rule, ast.identifiers) violation := result.fail(rego.metadata.chain(), result.location(rule.head)) } _path(loc) := concat(".", {l.value | some l in loc}) -illegal_value_ref(value, rule, identifiers) if { +_illegal_value_ref(value, rule, identifiers) if { not value in identifiers - not is_arg_or_input(value, rule) + not _is_arg_or_input(value, rule) } -is_arg_or_input(value, rule) if value in ast.function_arg_names(rule) +_is_arg_or_input(value, rule) if value in ast.function_arg_names(rule) -is_arg_or_input(value, _) if startswith(_path(value), "input.") +_is_arg_or_input(value, _) if startswith(_path(value), "input.") -is_arg_or_input("input", _) +_is_arg_or_input("input", _) diff --git a/bundle/regal/rules/custom/missing-metadata/missing_metadata.rego b/bundle/regal/rules/custom/missing-metadata/missing_metadata.rego new file mode 100644 index 00000000..8594349b --- /dev/null +++ b/bundle/regal/rules/custom/missing-metadata/missing_metadata.rego @@ -0,0 +1,132 @@ +# METADATA +# description: Package or rule missing metadata +package regal.rules.custom["missing-metadata"] + +import rego.v1 + +import data.regal.ast +import data.regal.config +import data.regal.result +import data.regal.util + +# METADATA +# description: aggregates annotations on package declarations and rules +aggregate contains result.aggregate(rego.metadata.chain(), { + "package_annotated": _package_annotated, + "package_location": input["package"].location, + "rule_annotations": _rule_annotations, + "rule_locations": _rule_locations, +}) + +default _package_annotated := false + +_package_annotated if { + some annotation in input.annotations + annotation.scope in {"package", "subpackages"} +} + +_rule_annotations[path] contains annotated if { + some rule in ast.public_rules_and_functions + every part in rule.head.ref { + not startswith(part.value, "_") + } + + path := concat(".", [ast.package_name, ast.ref_static_to_string(rule.head.ref)]) + + annotated := count(object.get(rule, "annotations", [])) > 0 +} + +_rule_locations[path] := location if { + head := ast.public_rules_and_functions[_].head + rref := ast.ref_static_to_string(head.ref) + + location := [h.location | + h := ast.public_rules_and_functions[_].head + ast.ref_static_to_string(h.ref) == rref + ][0] + + path := concat(".", [ast.package_name, rref]) +} + +# METADATA +# description: report packages without metadata +# schemas: +# - input: schema.regal.aggregate +aggregate_report contains violation if { + some pkg_path, aggs in _package_path_aggs + every item in aggs { + item.aggregate_data.package_annotated == false + } + + not _excepted_package_pattern(config.for_rule("custom", "missing-metadata"), pkg_path) + + first_item := [item | some item in aggs][0] + + # get the location from the first package, just to have something... + # but metadata could of course be added to any package definition, so a + # future improvement might be to show the location of all packages 🤔 + loc := util.to_location_object(first_item.aggregate_data.package_location) + + violation := result.fail(rego.metadata.chain(), {"location": object.union( + loc, + { + "file": first_item.aggregate_source.file, + "text": split(base64.decode(loc.text), "\n")[0], + }, + )}) +} + +# METADATA +# description: report rules without metadata annotations +# schemas: +# - input: schema.regal.aggregate +aggregate_report contains violation if { + some rule_path, aggregates in _rule_path_aggs + + every aggregate in aggregates { + aggregate.annotated == false + } + + not _excepted_rule_pattern(config.for_rule("custom", "missing-metadata"), rule_path) + + any_item := util.any_set_item(aggregates) + + loc := util.to_location_object(any_item.location) + + violation := result.fail(rego.metadata.chain(), {"location": object.union( + loc, + { + "file": any_item.file, + "text": split(base64.decode(loc.text), "\n")[0], + }, + )}) +} + +# METADATA +# schemas: +# - input: schema.regal.aggregate +_package_path_aggs[pkg_path] contains item if { + some item in input.aggregate + + pkg_path := concat(".", item.aggregate_source.package_path) +} + +# METADATA +# schemas: +# - input: schema.regal.aggregate +_rule_path_aggs[rule_path] contains agg if { + some item in input.aggregate + + some rule_path, annotations in item.aggregate_data.rule_annotations + annotated := annotations != {false} + + agg := { + "file": item.aggregate_source.file, + "location": item.aggregate_data.rule_locations[rule_path], + "annotated": annotated, + } +} + +_excepted_package_pattern(cfg, value) if regex.match(cfg["except-package-path-pattern"], value) + +_excepted_rule_pattern(cfg, value) if regex.match(cfg["except-rule-path-pattern"], value) diff --git a/bundle/regal/rules/custom/missing-metadata/missing_metadata_test.rego b/bundle/regal/rules/custom/missing-metadata/missing_metadata_test.rego new file mode 100644 index 00000000..6cb83fd5 --- /dev/null +++ b/bundle/regal/rules/custom/missing-metadata/missing_metadata_test.rego @@ -0,0 +1,161 @@ +package regal.rules.custom["missing-metadata_test"] + +import rego.v1 + +import data.regal.config + +import data.regal.rules.custom["missing-metadata"] as rule + +# regal ignore:rule-length +test_success_aggregate_format_as_expected if { + module := regal.parse_module("p.rego", `# METADATA +# title: pkg +package foo.bar + +# METADATA +# title: rule +rule := true + +none := false +`) + aggregated := rule.aggregate with input as module + + aggregated == {{ + "aggregate_data": { + "package_annotated": true, + "package_location": "3:1:cGFja2FnZQ==", + "rule_annotations": { + "foo.bar.none": {false}, + "foo.bar.rule": {true}, + }, + "rule_locations": { + "foo.bar.none": "9:1:bm9uZSA6PSBmYWxzZQ==", + "foo.bar.rule": "7:1:cnVsZSA6PSB0cnVl", + }, + }, + "aggregate_source": { + "file": "p.rego", + "package_path": ["foo", "bar"], + }, + "rule": { + "category": "custom", + "title": "missing-metadata", + }, + }} +} + +test_success_not_missing_package_metadata_report if { + module := regal.parse_module("p.rego", `# METADATA +# title: pkg +package foo.bar +`) + aggregated := rule.aggregate with input as module + r := rule.aggregate_report with input.aggregate as aggregated + + r == set() +} + +# regal ignore:rule-length +test_fail_missing_package_metadata_report if { + module := regal.parse_module("p.rego", "package foo.bar") + aggregated := rule.aggregate with input as module + r := rule.aggregate_report with input.aggregate as aggregated + r == {{ + "category": "custom", + "description": "Package or rule missing metadata", + "level": "error", + "location": { + "col": 1, + "row": 1, + "text": "package", + "file": "p.rego", + }, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/missing-metadata", "custom"), + }], + "title": "missing-metadata", + }} +} + +# regal ignore:rule-length +test_success_one_missing_one_found_package_metadata_report if { + module1 := regal.parse_module("p.rego", "package foo.bar") + module2 := regal.parse_module("p.rego", `# METADATA +# title: pkg +package foo.bar +`) + agg1 := rule.aggregate with input as module1 + agg2 := rule.aggregate with input as module2 + + r := rule.aggregate_report with input.aggregate as {agg1, agg2} + + r == set() +} + +test_success_not_missing_rule_metadata_report if { + module := regal.parse_module("p.rego", `# METADATA +# title: pkg +package foo.bar + +# METADATA +# title: baz +baz := true +`) + + a := rule.aggregate with input as module + r := rule.aggregate_report with input.aggregate as a + + r == set() +} + +test_fail_missing_rule_metadata_report if { + module := regal.parse_module("p.rego", `# METADATA +# title: pkg +package foo.bar + +baz := true +`) + + a := rule.aggregate with input as module + r := rule.aggregate_report with input.aggregate as a with config.for_rule as {"level": "error"} + + r == {{ + "category": "custom", + "description": "Package or rule missing metadata", + "level": "error", + "location": { + "col": 1, + "file": "p.rego", + "row": 5, + "text": "baz := true", + }, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/missing-metadata", "custom"), + }], + "title": "missing-metadata", + }} +} + +test_success_one_missing_one_found_rule_metadata_report if { + module1 := regal.parse_module("p.rego", `# METADATA +# title: pkg +package foo.bar + +rule := true +`) + module2 := regal.parse_module("p.rego", ` +package foo.bar + +# METADATA +# description: hey +rule := false +`) + agg1 := rule.aggregate with input as module1 + agg2 := rule.aggregate with input as module2 + + r := rule.aggregate_report with input.aggregate as {agg1, agg2} + + r == set() +} diff --git a/bundle/regal/rules/custom/naming-convention/naming_convention.rego b/bundle/regal/rules/custom/naming-convention/naming_convention.rego index daba4522..b09e2840 100644 --- a/bundle/regal/rules/custom/naming-convention/naming_convention.rego +++ b/bundle/regal/rules/custom/naming-convention/naming_convention.rego @@ -8,10 +8,9 @@ import data.regal.ast import data.regal.config import data.regal.result -cfg := config.for_rule("custom", "naming-convention") - # target: package report contains violation if { + cfg := config.for_rule("custom", "naming-convention") some convention in cfg.conventions some target in convention.targets @@ -19,7 +18,7 @@ report contains violation if { not regex.match(convention.pattern, ast.package_name) - violation := with_description( + violation := _with_description( result.fail(rego.metadata.chain(), result.location(input["package"])), sprintf( "Naming convention violation: package name %q does not match pattern '%s'", @@ -30,6 +29,7 @@ report contains violation if { # target: rule report contains violation if { + cfg := config.for_rule("custom", "naming-convention") some convention in cfg.conventions some target in convention.targets @@ -43,7 +43,7 @@ report contains violation if { not regex.match(convention.pattern, name) - violation := with_description( + violation := _with_description( result.fail(rego.metadata.chain(), result.location(rule.head)), sprintf( "Naming convention violation: rule name %q does not match pattern '%s'", @@ -54,6 +54,7 @@ report contains violation if { # target: function report contains violation if { + cfg := config.for_rule("custom", "naming-convention") some convention in cfg.conventions some target in convention.targets @@ -65,7 +66,7 @@ report contains violation if { not regex.match(convention.pattern, name) - violation := with_description( + violation := _with_description( result.fail(rego.metadata.chain(), result.location(rule.head)), sprintf( "Naming convention violation: function name %q does not match pattern '%s'", @@ -76,6 +77,7 @@ report contains violation if { # target: var report contains violation if { + cfg := config.for_rule("custom", "naming-convention") some convention in cfg.conventions some target in convention.targets @@ -85,7 +87,7 @@ report contains violation if { not regex.match(convention.pattern, var.value) - violation := with_description( + violation := _with_description( result.fail(rego.metadata.chain(), result.location(var)), sprintf( "Naming convention violation: variable name %q does not match pattern '%s'", @@ -94,7 +96,7 @@ report contains violation if { ) } -with_description(violation, description) := json.patch( +_with_description(violation, description) := json.patch( violation, [{"op": "replace", "path": "/description", "value": description}], ) diff --git a/bundle/regal/rules/custom/one-liner-rule/one_liner_rule.rego b/bundle/regal/rules/custom/one-liner-rule/one_liner_rule.rego index 2b45982c..97e8f899 100644 --- a/bundle/regal/rules/custom/one-liner-rule/one_liner_rule.rego +++ b/bundle/regal/rules/custom/one-liner-rule/one_liner_rule.rego @@ -10,8 +10,6 @@ import data.regal.config import data.regal.result import data.regal.util -cfg := config.for_rule("custom", "one-liner-rule") - # METADATA # description: Missing capability for keyword `if` # custom: @@ -44,28 +42,31 @@ report contains violation if { # Technically, the `if` could be on another line, but who would do that? regex.match(`\s+if`, lines[0]) - rule_body_brackets(lines) + _rule_body_brackets(lines) + + cfg := config.for_rule("custom", "one-liner-rule") + max_line_length := object.get(cfg, "max-line-length", 120) # ideally we'd take style preference into account but for now assume tab == 4 spaces # then just add the sum of the line counts minus the removed '{' character # redundant parens added by `opa fmt` :/ ((4 + count(lines[0])) + count(lines[1])) - 1 < max_line_length - not comment_in_body(rule, object.get(input, "comments", []), lines) + not _comment_in_body(rule, object.get(input, "comments", []), lines) violation := result.fail(rego.metadata.chain(), result.location(rule.head)) } # K&R style -rule_body_brackets(lines) if endswith(lines[0], "{") +_rule_body_brackets(lines) if endswith(lines[0], "{") # Allman style -rule_body_brackets(lines) if { +_rule_body_brackets(lines) if { not endswith(lines[0], "{") startswith(lines[1], "{") } -comment_in_body(rule, comments, lines) if { +_comment_in_body(rule, comments, lines) if { rule_location := util.to_location_object(rule.location) some comment in comments @@ -75,7 +76,3 @@ comment_in_body(rule, comments, lines) if { comment_location.row > rule_location.row comment_location.row < rule_location.row + count(lines) } - -default max_line_length := 120 - -max_line_length := cfg["max-line-length"] diff --git a/bundle/regal/rules/custom/prefer-value-in-head/prefer_value_in_head.rego b/bundle/regal/rules/custom/prefer-value-in-head/prefer_value_in_head.rego index 72ed622a..42a97eef 100644 --- a/bundle/regal/rules/custom/prefer-value-in-head/prefer_value_in_head.rego +++ b/bundle/regal/rules/custom/prefer-value-in-head/prefer_value_in_head.rego @@ -8,12 +8,12 @@ import data.regal.ast import data.regal.config import data.regal.result -cfg := config.for_rule("custom", "prefer-value-in-head") - report contains violation if { + cfg := config.for_rule("custom", "prefer-value-in-head") + some rule in input.rules - var := var_in_head(rule) + var := _var_in_head(rule) last := regal.last(rule.body) last.terms[0].value[0].type == "var" @@ -21,19 +21,19 @@ report contains violation if { last.terms[1].type == "var" last.terms[1].value == var - not scalar_fail(cfg, last.terms[2], ast.scalar_types) + not _scalar_fail(cfg, last.terms[2], ast.scalar_types) violation := result.fail(rego.metadata.chain(), result.location(last)) } -var_in_head(rule) := rule.head.value.value if rule.head.value.type == "var" +_var_in_head(rule) := rule.head.value.value if rule.head.value.type == "var" -var_in_head(rule) := rule.head.key.value if { +_var_in_head(rule) := rule.head.key.value if { not rule.head.value rule.head.key.type == "var" } -scalar_fail(cfg, term, scalar_types) if { +_scalar_fail(cfg, term, scalar_types) if { cfg["only-scalars"] == true not term.type in scalar_types } diff --git a/bundle/regal/rules/idiomatic/no-defined-entrypoint/no_defined_entrypoint.rego b/bundle/regal/rules/idiomatic/no-defined-entrypoint/no_defined_entrypoint.rego index 2e0b300d..5905ad5c 100644 --- a/bundle/regal/rules/idiomatic/no-defined-entrypoint/no_defined_entrypoint.rego +++ b/bundle/regal/rules/idiomatic/no-defined-entrypoint/no_defined_entrypoint.rego @@ -7,6 +7,9 @@ import rego.v1 import data.regal.result import data.regal.util +# METADATA +# description: | +# collects `entrypoint: true` annotations from any given module aggregate contains entry if { some annotation in input.annotations annotation.entrypoint == true diff --git a/bundle/regal/rules/idiomatic/prefer-set-or-object-rule/prefer_set_or_object_rule.rego b/bundle/regal/rules/idiomatic/prefer-set-or-object-rule/prefer_set_or_object_rule.rego index b76347e7..828a2f6d 100644 --- a/bundle/regal/rules/idiomatic/prefer-set-or-object-rule/prefer_set_or_object_rule.rego +++ b/bundle/regal/rules/idiomatic/prefer-set-or-object-rule/prefer_set_or_object_rule.rego @@ -14,13 +14,13 @@ report contains violation if { not rule.body # Ignore simple conversions from array to set - not is_array_conversion(rule.head.value) + not _is_array_conversion(rule.head.value) violation := result.fail(rego.metadata.chain(), result.location(rule.head)) } # {s | some s in arr} -is_array_conversion(value) if { +_is_array_conversion(value) if { value.type == "setcomprehension" value.value.term.type == "var" @@ -47,7 +47,7 @@ is_array_conversion(value) if { # {s | s := arr[_].foo} # or # {s | s := arr[_].foo[_]} -is_array_conversion(value) if { +_is_array_conversion(value) if { value.type == "setcomprehension" value.value.term.type == "var" diff --git a/bundle/regal/rules/idiomatic/use-in-operator/use_in_operator.rego b/bundle/regal/rules/idiomatic/use-in-operator/use_in_operator.rego index fce9e2a4..ba0f6463 100644 --- a/bundle/regal/rules/idiomatic/use-in-operator/use_in_operator.rego +++ b/bundle/regal/rules/idiomatic/use-in-operator/use_in_operator.rego @@ -8,20 +8,20 @@ import data.regal.ast import data.regal.result report contains violation if { - some terms in eq_exprs_terms + some terms in _eq_exprs_terms - nl_terms := non_loop_term(terms) + nl_terms := _non_loop_term(terms) count(nl_terms) == 1 nlt := nl_terms[0] - static_term(nlt.term) + _static_term(nlt.term) # Use the non-loop term position to determine the # location of the loop term (3 is the count of terms) violation := result.fail(rego.metadata.chain(), result.location(terms[3 - nlt.pos])) } -eq_exprs_terms contains terms if { +_eq_exprs_terms contains terms if { terms := input.rules[_].body[_].terms terms[0].type == "ref" @@ -29,12 +29,12 @@ eq_exprs_terms contains terms if { terms[0].value[0].value in {"eq", "equal"} } -non_loop_term(terms) := [{"pos": i + 1, "term": term} | +_non_loop_term(terms) := [{"pos": i + 1, "term": term} | some i, term in array.slice(terms, 1, 3) - not loop_term(term) + not _loop_term(term) ] -loop_term(term) if { +_loop_term(term) if { term.type == "ref" term.value[0].type == "var" last := regal.last(term.value) @@ -42,9 +42,9 @@ loop_term(term) if { startswith(last.value, "$") } -static_term(term) if term.type in {"array", "boolean", "object", "null", "number", "set", "string", "var"} +_static_term(term) if term.type in {"array", "boolean", "object", "null", "number", "set", "string", "var"} -static_term(term) if { +_static_term(term) if { term.type == "ref" ast.static_ref(term) } diff --git a/bundle/regal/rules/idiomatic/use-some-for-output-vars/use_some_for_output_vars.rego b/bundle/regal/rules/idiomatic/use-some-for-output-vars/use_some_for_output_vars.rego index 7c726506..0689a96f 100644 --- a/bundle/regal/rules/idiomatic/use-some-for-output-vars/use_some_for_output_vars.rego +++ b/bundle/regal/rules/idiomatic/use-some-for-output-vars/use_some_for_output_vars.rego @@ -23,14 +23,14 @@ report contains violation if { path := _location_path(rule, elem.location) - not var_in_comprehension_body(elem, rule, path) + not _var_in_comprehension_body(elem, rule, path) violation := result.fail(rego.metadata.chain(), result.ranged_location_from_text(elem)) } _location_path(rule, location) := path if walk(rule, [path, location]) -var_in_comprehension_body(var, rule, path) if { +_var_in_comprehension_body(var, rule, path) if { some v in _comprehension_body_vars(rule, path) v.type == var.type v.value == var.value diff --git a/bundle/regal/rules/imports/circular-import/circular_import.rego b/bundle/regal/rules/imports/circular-import/circular_import.rego index 9bd121b7..6aff1f54 100644 --- a/bundle/regal/rules/imports/circular-import/circular_import.rego +++ b/bundle/regal/rules/imports/circular-import/circular_import.rego @@ -13,7 +13,7 @@ import data.regal.ast import data.regal.result import data.regal.util -refs contains ref if { +_refs contains ref if { r := ast.found.refs[_][_] r.value[0].value == "data" @@ -26,7 +26,7 @@ refs contains ref if { } } -refs contains ref if { +_refs contains ref if { some imported in ast.imports imported.path.value[0].value == "data" @@ -37,10 +37,12 @@ refs contains ref if { } } +# METADATA +# description: collects refs from module aggregate contains entry if { - count(refs) > 0 + count(_refs) > 0 - entry := result.aggregate(rego.metadata.chain(), {"refs": refs}) + entry := result.aggregate(rego.metadata.chain(), {"refs": _refs}) } # METADATA @@ -51,14 +53,14 @@ aggregate_report contains violation if { # for a circular import to be possible count(input.aggregate) > 1 - some g in groups + some g in _groups sorted_group := sort(g) location := [loc | some m1 in sorted_group some m2 in sorted_group - some loc in package_locations[m1][m2] + some loc in _package_locations[m1][m2] ][0] violation := result.fail( @@ -75,14 +77,14 @@ aggregate_report contains violation if { # - input: schema.regal.aggregate aggregate_report contains violation if { # this rule tests for self dependencies - some g in groups + some g in _groups count(g) == 1 some pkg in g # this will the only package location := [e | - some e in package_locations[pkg][pkg] + some e in _package_locations[pkg][pkg] ][0] violation := result.fail( @@ -97,7 +99,7 @@ aggregate_report contains violation if { # METADATA # schemas: # - input: schema.regal.aggregate -package_locations[referenced_pkg][referencing_pkg] contains location if { +_package_locations[referenced_pkg][referencing_pkg] contains location if { some ag_pkg in input.aggregate some ref in ag_pkg.aggregate_data.refs @@ -116,7 +118,7 @@ package_locations[referenced_pkg][referencing_pkg] contains location if { # METADATA # schemas: # - input: schema.regal.aggregate -import_graph[pkg] contains edge if { +_import_graph[pkg] contains edge if { some ag_pkg in input.aggregate pkg := sprintf("data.%s", [concat(".", ag_pkg.aggregate_source.package_path)]) @@ -126,29 +128,29 @@ import_graph[pkg] contains edge if { edge := pkg_ref.package_path } -reachable_index[pkg] := reachable if { - some pkg, _ in import_graph +_reachable_index[pkg] := reachable if { + some pkg, _ in _import_graph - reachable := graph.reachable(import_graph, {pkg}) + reachable := graph.reachable(_import_graph, {pkg}) } -self_reachable contains pkg if { - some pkg, _ in import_graph +_self_reachable contains pkg if { + some pkg, _ in _import_graph - pkg in reachable_index[pkg] + pkg in _reachable_index[pkg] } -groups contains group if { - some pkg in self_reachable +_groups contains group if { + some pkg in _self_reachable # only consider packages that have edges to other packages, # even if only to themselves - import_graph[pkg] != {} + _import_graph[pkg] != {} - reachable := graph.reachable(import_graph, {pkg}) + reachable := graph.reachable(_import_graph, {pkg}) group := {m | some m in reachable - pkg in reachable_index[m] + pkg in _reachable_index[m] } } diff --git a/bundle/regal/rules/imports/circular-import/circular_import_test.rego b/bundle/regal/rules/imports/circular-import/circular_import_test.rego index 7195af56..f5c31034 100644 --- a/bundle/regal/rules/imports/circular-import/circular_import_test.rego +++ b/bundle/regal/rules/imports/circular-import/circular_import_test.rego @@ -79,7 +79,8 @@ test_aggregate_rule_surfaces_refs if { } test_import_graph if { - r := rule.import_graph with input as {"aggregate": { + # regal ignore:leaked-internal-reference + r := rule._import_graph with input as {"aggregate": { { "aggregate_data": {"refs": {{"location": {"col": 12, "row": 3}, "package_path": "data.policy.b"}}}, "aggregate_source": { @@ -110,7 +111,8 @@ test_import_graph if { } test_import_graph_self_import if { - r := rule.import_graph with input as {"aggregate": {{ + # regal ignore:leaked-internal-reference + r := rule._import_graph with input as {"aggregate": {{ "aggregate_data": {"refs": {{"location": {"col": 12, "row": 4}, "package_path": "data.example"}}}, "aggregate_source": {"file": "example.rego", "package_path": ["example"]}, "rule": {"category": "imports", "title": "circular-import"}, @@ -120,7 +122,8 @@ test_import_graph_self_import if { } test_self_reachable if { - r := rule.self_reachable with rule.import_graph as { + # regal ignore:leaked-internal-reference + r := rule._self_reachable with rule._import_graph as { "data.policy.a": {"data.policy.b"}, "data.policy.b": {"data.policy.c"}, "data.policy.c": {"data.policy.a"}, } @@ -129,7 +132,8 @@ test_self_reachable if { } test_groups if { - r := rule.groups with rule.import_graph as { + # regal ignore:leaked-internal-reference + r := rule._groups with rule._import_graph as { "data.policy.a": {"data.policy.b"}, "data.policy.b": {"data.policy.c"}, "data.policy.c": {"data.policy.a"}, @@ -147,13 +151,15 @@ test_groups if { } test_groups_empty_graph if { - r := rule.groups with rule.import_graph as {"data.policy.a": {}} + # regal ignore:leaked-internal-reference + r := rule._groups with rule._import_graph as {"data.policy.a": {}} r == set() } test_package_locations if { - r := rule.package_locations with input as {"aggregate": { + # regal ignore:leaked-internal-reference + r := rule._package_locations with input as {"aggregate": { { "aggregate_data": {"refs": {{"location": {"col": 12, "row": 3}, "package_path": "data.policy.b"}}}, "aggregate_source": {"file": "a.rego", "package_path": ["policy.a"]}, diff --git a/bundle/regal/rules/imports/ignored-import/ignored_import.rego b/bundle/regal/rules/imports/ignored-import/ignored_import.rego index ce086241..b3d81387 100644 --- a/bundle/regal/rules/imports/ignored-import/ignored_import.rego +++ b/bundle/regal/rules/imports/ignored-import/ignored_import.rego @@ -7,7 +7,7 @@ import rego.v1 import data.regal.ast import data.regal.result -import_paths contains path if { +_import_paths contains path if { some imp in input.imports path := [p.value | some p in imp.path.value] @@ -24,7 +24,7 @@ report contains violation if { most_specific_match := regal.last(sort([ip | ref_path := [p.value | some p in ref.value] - some ip in import_paths + some ip in _import_paths array.slice(ref_path, 0, count(ip)) == ip ])) diff --git a/bundle/regal/rules/imports/import-shadows-builtin/import_shadows_builtin.rego b/bundle/regal/rules/imports/import-shadows-builtin/import_shadows_builtin.rego index 52947a8f..4df5c56b 100644 --- a/bundle/regal/rules/imports/import-shadows-builtin/import_shadows_builtin.rego +++ b/bundle/regal/rules/imports/import-shadows-builtin/import_shadows_builtin.rego @@ -12,7 +12,7 @@ report contains violation if { imp.path.value[0].value in {"data", "input"} - name := significant_name(imp) + name := _significant_name(imp) name in ast.builtin_namespaces # AST quirk: while we'd ideally provide the location of the *path component*, @@ -21,6 +21,6 @@ report contains violation if { violation := result.fail(rego.metadata.chain(), result.location(imp)) } -significant_name(imp) := imp.alias +_significant_name(imp) := imp.alias -significant_name(imp) := regal.last(imp.path.value).value if not imp.alias +_significant_name(imp) := regal.last(imp.path.value).value if not imp.alias diff --git a/bundle/regal/rules/imports/prefer-package-imports/prefer_package_imports.rego b/bundle/regal/rules/imports/prefer-package-imports/prefer_package_imports.rego index af25fab6..eddcd31f 100644 --- a/bundle/regal/rules/imports/prefer-package-imports/prefer_package_imports.rego +++ b/bundle/regal/rules/imports/prefer-package-imports/prefer_package_imports.rego @@ -8,8 +8,8 @@ import data.regal.ast import data.regal.config import data.regal.result -cfg := config.for_rule("imports", "prefer-package-imports") - +# METADATA +# description: collects imports and package paths from each module aggregate contains entry if { imports_with_location := [imp | some _import in input.imports @@ -21,7 +21,7 @@ aggregate contains entry if { # Special case for custom rules, where we don't want to flag e.g. `import data.regal.ast` # as unknown, even though it's not a package included in evaluation. - not custom_regal_package_and_import(ast.package_path, path) + not _custom_regal_package_and_import(ast.package_path, path) imp := object.union(result.location(_import), {"path": path}) ] @@ -32,7 +32,7 @@ aggregate contains entry if { }) } -custom_regal_package_and_import(pkg_path, path) if { +_custom_regal_package_and_import(pkg_path, path) if { pkg_path[0] == "custom" pkg_path[1] == "regal" path[0] == "regal" @@ -64,6 +64,8 @@ _resolves(path, pkg_paths) if count([path | ]) > 0 _ignored_import_paths contains path if { + cfg := config.for_rule("imports", "prefer-package-imports") + some item in cfg["ignore-import-paths"] path := [part | some i, p in split(item, ".") diff --git a/bundle/regal/rules/imports/unresolved-import/unresolved_import.rego b/bundle/regal/rules/imports/unresolved-import/unresolved_import.rego index 76cea675..ae8a44f6 100644 --- a/bundle/regal/rules/imports/unresolved-import/unresolved_import.rego +++ b/bundle/regal/rules/imports/unresolved-import/unresolved_import.rego @@ -9,6 +9,8 @@ import data.regal.config import data.regal.result import data.regal.util +# METADATA +# description: collects imports and exported refs from each module aggregate contains entry if { imports_with_location := [imp | some _import in input.imports diff --git a/bundle/regal/rules/style/default-over-else/default_over_else.rego b/bundle/regal/rules/style/default-over-else/default_over_else.rego index dc185dff..712b977e 100644 --- a/bundle/regal/rules/style/default-over-else/default_over_else.rego +++ b/bundle/regal/rules/style/default-over-else/default_over_else.rego @@ -8,10 +8,8 @@ import data.regal.ast import data.regal.config import data.regal.result -cfg := config.for_rule("style", "default-over-else") - report contains violation if { - some rule in considered_rules + some rule in _considered_rules # walking is expensive but necessary here, since there could be # any number of `else` clauses nested below. no need to traverse @@ -27,6 +25,8 @@ report contains violation if { violation := result.fail(rego.metadata.chain(), result.location(else_head)) } -considered_rules := input.rules if cfg["prefer-default-functions"] == true +_cfg := config.for_rule("style", "default-over-else") + +_considered_rules := input.rules if _cfg["prefer-default-functions"] == true -considered_rules := ast.rules if not cfg["prefer-default-functions"] +_considered_rules := ast.rules if not _cfg["prefer-default-functions"] diff --git a/bundle/regal/rules/style/detached-metadata/detached_metadata.rego b/bundle/regal/rules/style/detached-metadata/detached_metadata.rego index 5440645c..e0914fb2 100644 --- a/bundle/regal/rules/style/detached-metadata/detached_metadata.rego +++ b/bundle/regal/rules/style/detached-metadata/detached_metadata.rego @@ -18,13 +18,13 @@ report contains violation if { # no need to +1 the index here as rows start counting from 1 trim_space(input.regal.file.lines[last_row]) == "" - annotation := annotation_at_row(util.to_location_object(block[0].location).row) + annotation := _annotation_at_row(util.to_location_object(block[0].location).row) annotation.scope != "document" violation := result.fail(rego.metadata.chain(), result.location(block[0])) } -annotation_at_row(row) := annotation if { +_annotation_at_row(row) := annotation if { some annotation in input.annotations util.to_location_object(annotation.location).row == row diff --git a/bundle/regal/rules/style/double-negative/double_negative.rego b/bundle/regal/rules/style/double-negative/double_negative.rego index 3b8377b5..224c1c1e 100644 --- a/bundle/regal/rules/style/double-negative/double_negative.rego +++ b/bundle/regal/rules/style/double-negative/double_negative.rego @@ -17,14 +17,12 @@ report contains violation if { ast.negated_expressions[_][node] node.terms.type == "var" - strings.any_prefix_match(node.terms.value, negatives) + strings.any_prefix_match(node.terms.value, { + "cannot_", + "no_", + "non_", + "not_", + }) violation := result.fail(rego.metadata.chain(), result.location(node)) } - -negatives := { - "cannot_", - "no_", - "non_", - "not_", -} diff --git a/bundle/regal/rules/style/external-reference/external_reference.rego b/bundle/regal/rules/style/external-reference/external_reference.rego index 14aa22ad..b149ea3f 100644 --- a/bundle/regal/rules/style/external-reference/external_reference.rego +++ b/bundle/regal/rules/style/external-reference/external_reference.rego @@ -6,6 +6,7 @@ import rego.v1 import data.regal.ast import data.regal.result +import data.regal.util report contains violation if { fn_namespaces := {split(name, ".")[0] | some name in object.keys(ast.all_functions)} @@ -27,13 +28,11 @@ report contains violation if { value.type == "var" not value.value in allowed_refs not startswith(value.value, "$") - not function_call_ctx(fn, path) + not _function_call_ctx(fn, path) violation := result.fail(rego.metadata.chain(), result.location(value)) } -last_indexof(arr, item) := regal.last([i | some i, x in arr; x == item]) - # METADATA # scope: document # description: | @@ -42,8 +41,8 @@ last_indexof(arr, item) := regal.last([i | some i, x in arr; x == item]) # note: this doesn't check for built-in calls or calls to function # defined in the same package, as those are already covered by # "fn_namespaces" in the report rule -function_call_ctx(fn, path) if { - terms_path := array.slice(path, 0, last_indexof(path, "terms") + 2) +_function_call_ctx(fn, path) if { + terms_path := array.slice(path, 0, util.last_indexof(path, "terms") + 2) next_term_path := array.concat( array.slice(terms_path, 0, count(terms_path) - 1), # ["body", 0, "terms", 0] -> ["body", 0, "terms"] [regal.last(terms_path) + 1], # 0 -> 1 @@ -54,4 +53,4 @@ function_call_ctx(fn, path) if { object.get(fn, next_term_path, null) != null } -function_call_ctx(fn, path) if object.get(fn, array.slice(path, 0, count(path) - 4), {}).type == "call" +_function_call_ctx(fn, path) if object.get(fn, array.slice(path, 0, count(path) - 4), {}).type == "call" diff --git a/bundle/regal/rules/style/file-length/file_length.rego b/bundle/regal/rules/style/file-length/file_length.rego index 52d7bc8b..c0cfc8ab 100644 --- a/bundle/regal/rules/style/file-length/file_length.rego +++ b/bundle/regal/rules/style/file-length/file_length.rego @@ -7,9 +7,9 @@ import rego.v1 import data.regal.config import data.regal.result -cfg := config.for_rule("style", "file-length") - report contains violation if { + cfg := config.for_rule("style", "file-length") + count(input.regal.file.lines) > cfg["max-file-length"] violation := result.fail(rego.metadata.chain(), result.location(input["package"])) diff --git a/bundle/regal/rules/style/line-length/line_length.rego b/bundle/regal/rules/style/line-length/line_length.rego index df803243..4155ac72 100644 --- a/bundle/regal/rules/style/line-length/line_length.rego +++ b/bundle/regal/rules/style/line-length/line_length.rego @@ -7,13 +7,11 @@ import rego.v1 import data.regal.config import data.regal.result -cfg := config.for_rule("style", "line-length") - -default max_line_length := 120 +report contains violation if { + cfg := config.for_rule("style", "line-length") -max_line_length := cfg["max-line-length"] + max_line_length := object.get(cfg, "max-line-length", 120) -report contains violation if { some i, line in input.regal.file.lines line != "" @@ -21,7 +19,7 @@ report contains violation if { line_length := count(line) line_length > max_line_length - not has_word_above_threshold(line, cfg) + not _has_word_above_threshold(line, cfg) violation := result.fail( rego.metadata.chain(), @@ -38,7 +36,7 @@ report contains violation if { ) } -has_word_above_threshold(line, conf) if { +_has_word_above_threshold(line, conf) if { threshold := conf["non-breakable-word-threshold"] some word in split(line, " ") diff --git a/bundle/regal/rules/style/prefer-some-in-iteration/prefer_some_in_iteration.rego b/bundle/regal/rules/style/prefer-some-in-iteration/prefer_some_in_iteration.rego index 5a1ab019..98b48b79 100644 --- a/bundle/regal/rules/style/prefer-some-in-iteration/prefer_some_in_iteration.rego +++ b/bundle/regal/rules/style/prefer-some-in-iteration/prefer_some_in_iteration.rego @@ -9,12 +9,12 @@ import data.regal.config import data.regal.result import data.regal.util -cfg := config.for_rule("style", "prefer-some-in-iteration") - report contains violation if { + cfg := config.for_rule("style", "prefer-some-in-iteration") + some i, rule in input.rules - not possible_top_level_iteration(rule) + not _possible_top_level_iteration(rule) walk(rule, [path, value]) @@ -35,18 +35,18 @@ report contains violation if { num_output_vars != 0 num_output_vars < cfg["ignore-nesting-level"] - not except_sub_attribute(value.value) - not invalid_some_context(input.rules[i], path) + not _except_sub_attribute(cfg, value.value) + not _invalid_some_context(input.rules[i], path) violation := result.fail(rego.metadata.chain(), result.location(value)) } -except_sub_attribute(ref) if { +_except_sub_attribute(cfg, ref) if { cfg["ignore-if-sub-attribute"] == true - has_sub_attribute(ref) + _has_sub_attribute(ref) } -has_sub_attribute(ref) if { +_has_sub_attribute(ref) if { last_var_pos := regal.last([i | some i, part in ref part.type == "var" @@ -56,18 +56,18 @@ has_sub_attribute(ref) if { # don't walk top level iteration refs: # https://docs.styra.com/regal/rules/bugs/top-level-iteration -possible_top_level_iteration(rule) if { +_possible_top_level_iteration(rule) if { not rule.body rule.head.value.type == "ref" } # don't recommend `some .. in` if iteration occurs inside of arrays, objects, or sets -invalid_some_context(rule, path) if { +_invalid_some_context(rule, path) if { some p in util.all_paths(path) node := object.get(rule, p, []) - impossible_some(node) + _impossible_some(node) } # don't recommend `some .. in` if iteration occurs inside of a @@ -75,7 +75,7 @@ invalid_some_context(rule, path) if { # this should honestly be a rule of its own, I think, but it's # not _directly_ replaceable by `some .. in`, so we'll leave it # be here -invalid_some_context(rule, path) if { +_invalid_some_context(rule, path) if { some p in util.all_paths(path) node := object.get(rule, p, []) @@ -87,16 +87,16 @@ invalid_some_context(rule, path) if { } # if previous node is of type call, also don't recommend `some .. in` -invalid_some_context(rule, path) if object.get(rule, array.slice(path, 0, count(path) - 2), {}).type == "call" +_invalid_some_context(rule, path) if object.get(rule, array.slice(path, 0, count(path) - 2), {}).type == "call" -impossible_some(node) if node.type in {"array", "object", "set"} +_impossible_some(node) if node.type in {"array", "object", "set"} -impossible_some(node) if node.key +_impossible_some(node) if node.key # technically this is not an _impossible_ some, as we could replace e.g. `"x" == input[_]` # with `some "x" in input`, but that'd be an `unnecessary-some` violation as `"x" in input` # would be the correct way to express that -impossible_some(node) if { +_impossible_some(node) if { node.terms[0].value[0].type == "var" node.terms[0].value[0].value in {"eq", "equal"} diff --git a/bundle/regal/rules/style/rule-length/rule_length.rego b/bundle/regal/rules/style/rule-length/rule_length.rego index a316da3b..13b0c9fe 100644 --- a/bundle/regal/rules/style/rule-length/rule_length.rego +++ b/bundle/regal/rules/style/rule-length/rule_length.rego @@ -8,27 +8,27 @@ import data.regal.config import data.regal.result import data.regal.util -cfg := config.for_rule("style", "rule-length") - report contains violation if { + cfg := config.for_rule("style", "rule-length") + some rule in input.rules lines := split(base64.decode(util.to_location_object(rule.location).text), "\n") - line_count(cfg, rule, lines) > cfg["max-rule-length"] + _line_count(cfg, rule, lines) > cfg["max-rule-length"] - not generated_body_exception(cfg, rule) + not _generated_body_exception(cfg, rule) violation := result.fail(rego.metadata.chain(), result.location(rule.head)) } -generated_body_exception(conf, rule) if { +_generated_body_exception(conf, rule) if { conf["except-empty-body"] == true not rule.body } -line_count(cfg, _, lines) := count(lines) if cfg["count-comments"] == true +_line_count(cfg, _, lines) := count(lines) if cfg["count-comments"] == true -line_count(cfg, rule, lines) := n if { +_line_count(cfg, rule, lines) := n if { not cfg["count-comments"] # Note that this assumes } on its own line diff --git a/bundle/regal/rules/style/todo-comment/todo_comment.rego b/bundle/regal/rules/style/todo-comment/todo_comment.rego index e0fc6495..45bfc111 100644 --- a/bundle/regal/rules/style/todo-comment/todo_comment.rego +++ b/bundle/regal/rules/style/todo-comment/todo_comment.rego @@ -7,15 +7,10 @@ import rego.v1 import data.regal.ast import data.regal.result -# For comments, OPA uses capital-cased Text and Location rather -# than text and location. As fixing this would potentially break -# things, we need to take it into consideration here. - -todo_identifiers := ["todo", "TODO", "fixme", "FIXME"] - -todo_pattern := sprintf(`^\s*(%s)`, [concat("|", todo_identifiers)]) - report contains violation if { + todo_identifiers := ["todo", "TODO", "fixme", "FIXME"] + todo_pattern := sprintf(`^\s*(%s)`, [concat("|", todo_identifiers)]) + some comment in ast.comments_decoded regex.match(todo_pattern, comment.text) diff --git a/bundle/regal/rules/style/trailing-default-rule/trailing_default_rule.rego b/bundle/regal/rules/style/trailing-default-rule/trailing_default_rule.rego index b8fb1da8..1e9af6e5 100644 --- a/bundle/regal/rules/style/trailing-default-rule/trailing_default_rule.rego +++ b/bundle/regal/rules/style/trailing-default-rule/trailing_default_rule.rego @@ -13,12 +13,12 @@ report contains violation if { rule["default"] == true name := ast.ref_to_string(rule.head.ref) - name in all_names(array.slice(input.rules, 0, i)) + name in _all_names(array.slice(input.rules, 0, i)) violation := result.fail(rego.metadata.chain(), result.location(rule)) } -all_names(rules) := {name | +_all_names(rules) := {name | some rule in rules name := ast.ref_to_string(rule.head.ref) } diff --git a/bundle/regal/rules/style/unconditional-assignment/unconditional_assignment.rego b/bundle/regal/rules/style/unconditional-assignment/unconditional_assignment.rego index d041f554..1d2e2613 100644 --- a/bundle/regal/rules/style/unconditional-assignment/unconditional_assignment.rego +++ b/bundle/regal/rules/style/unconditional-assignment/unconditional_assignment.rego @@ -30,7 +30,7 @@ report contains violation if { # `with` statements can't be moved to the rule head not rule.body[0]["with"] - assignment_expr(rule.body[0].terms) + _assignment_expr(rule.body[0].terms) # Of var declared in rule head rule.body[0].terms[1].type == "var" @@ -51,7 +51,7 @@ report contains violation if { not rule.body[0]["with"] - assignment_expr(rule.body[0].terms) + _assignment_expr(rule.body[0].terms) rule.body[0].terms[1].type == "var" rule.body[0].terms[1].value == rule.head.key.value @@ -60,7 +60,7 @@ report contains violation if { } # Assignment using either = or := -assignment_expr(terms) if { +_assignment_expr(terms) if { terms[0].type == "ref" terms[0].value[0].type == "var" terms[0].value[0].value in {"eq", "assign"} diff --git a/bundle/regal/rules/style/unnecessary-some/unnecessary_some.rego b/bundle/regal/rules/style/unnecessary-some/unnecessary_some.rego index fbe8a40e..9b39adfb 100644 --- a/bundle/regal/rules/style/unnecessary-some/unnecessary_some.rego +++ b/bundle/regal/rules/style/unnecessary-some/unnecessary_some.rego @@ -16,12 +16,12 @@ report contains violation if { symbols[0].type == "call" symbols[0].value[0].type == "ref" - some_is_unnecessary(symbols, ast.scalar_types) + _some_is_unnecessary(symbols, ast.scalar_types) violation := result.fail(rego.metadata.chain(), result.location(symbols)) } -some_is_unnecessary(value, scalar_types) if { +_some_is_unnecessary(value, scalar_types) if { ref := value[0].value[0].value [ref[0].value, ref[1].value] == ["internal", "member_2"] @@ -29,7 +29,7 @@ some_is_unnecessary(value, scalar_types) if { value[0].value[1].type in scalar_types } -some_is_unnecessary(value, scalar_types) if { +_some_is_unnecessary(value, scalar_types) if { ref := value[0].value[0].value [ref[0].value, ref[1].value] == ["internal", "member_3"] diff --git a/bundle/regal/rules/style/use-assignment-operator/use_assignment_operator.rego b/bundle/regal/rules/style/use-assignment-operator/use_assignment_operator.rego index 0af171d3..eabfa358 100644 --- a/bundle/regal/rules/style/use-assignment-operator/use_assignment_operator.rego +++ b/bundle/regal/rules/style/use-assignment-operator/use_assignment_operator.rego @@ -29,6 +29,7 @@ report contains violation if { rule.head.key rule.head.value not rule.head.assign + not ast.implicit_boolean_assignment(rule) loc := result.location(result.location(rule.head.ref[0])) diff --git a/bundle/regal/rules/style/use-assignment-operator/use_assignment_operator_test.rego b/bundle/regal/rules/style/use-assignment-operator/use_assignment_operator_test.rego index ad24baf1..747b1e6f 100644 --- a/bundle/regal/rules/style/use-assignment-operator/use_assignment_operator_test.rego +++ b/bundle/regal/rules/style/use-assignment-operator/use_assignment_operator_test.rego @@ -156,6 +156,11 @@ test_success_ref_head_rule_if if { r == set() } +test_success_ref_head_rule_with_var_if if { + r := rule.report with input as ast.with_rego_v1(`works[x] if x := 5`) + r == set() +} + # regal ignore:rule-length test_fail_unification_in_else if { r := rule.report with input as ast.with_rego_v1(` diff --git a/bundle/regal/rules/style/yoda-condition/yoda_condition.rego b/bundle/regal/rules/style/yoda-condition/yoda_condition.rego index edf18cd4..3e768015 100644 --- a/bundle/regal/rules/style/yoda-condition/yoda_condition.rego +++ b/bundle/regal/rules/style/yoda-condition/yoda_condition.rego @@ -14,12 +14,12 @@ report contains violation if { value[0].value[0].value in {"equal", "neq"} # perhaps add more operators here? value[1].type in ast.scalar_types not value[2].type in ast.scalar_types - not ref_with_vars(value[2].value) + not _ref_with_vars(value[2].value) violation := result.fail(rego.metadata.chain(), result.location(value)) } -ref_with_vars(ref) if { +_ref_with_vars(ref) if { count(ref) > 2 some i, part in ref i > 0 diff --git a/bundle/regal/rules/testing/identically-named-tests/identically_named_tests.rego b/bundle/regal/rules/testing/identically-named-tests/identically_named_tests.rego index 240487fb..4018ed27 100644 --- a/bundle/regal/rules/testing/identically-named-tests/identically_named_tests.rego +++ b/bundle/regal/rules/testing/identically-named-tests/identically_named_tests.rego @@ -14,10 +14,10 @@ report contains violation if { name in array.slice(test_names, 0, i) - violation := result.fail(rego.metadata.chain(), result.location(rule_by_name(name, ast.tests))) + violation := result.fail(rego.metadata.chain(), result.location(_rule_by_name(name, ast.tests))) } -rule_by_name(name, rules) := regal.last([rule | +_rule_by_name(name, rules) := regal.last([rule | some rule in rules rule.head.ref[0].value == name ]) diff --git a/bundle/regal/rules/testing/metasyntactic-variable/metasyntactic_variable.rego b/bundle/regal/rules/testing/metasyntactic-variable/metasyntactic_variable.rego index c821e596..2eb6ec2c 100644 --- a/bundle/regal/rules/testing/metasyntactic-variable/metasyntactic_variable.rego +++ b/bundle/regal/rules/testing/metasyntactic-variable/metasyntactic_variable.rego @@ -7,7 +7,7 @@ import rego.v1 import data.regal.ast import data.regal.result -metasyntactic := { +_metasyntactic := { "foobar", "foo", "bar", @@ -28,7 +28,7 @@ report contains violation if { some rule in input.rules some part in ast.named_refs(rule.head.ref) - lower(part.value) in metasyntactic + lower(part.value) in _metasyntactic # In case we have chained rule bodies — only flag the location where we have an actual name: # foo { @@ -45,7 +45,7 @@ report contains violation if { some i var := ast.found.vars[i][_][_] - lower(var.value) in metasyntactic + lower(var.value) in _metasyntactic ast.is_output_var(input.rules[to_number(i)], var) diff --git a/bundle/regal/util/util.rego b/bundle/regal/util/util.rego index 857ee4ce..2f3f0872 100644 --- a/bundle/regal/util/util.rego +++ b/bundle/regal/util/util.rego @@ -43,6 +43,9 @@ keys_to_numbers(obj) := {num: v | num := to_number(k) } +# METADATA +# description: convert location string to location object +# scope: document to_location_object(loc) := {"row": to_number(row), "col": to_number(col), "text": text} if { is_string(loc) [row, col, text] := split(loc, ":") @@ -57,12 +60,19 @@ json_pretty(value) := json.marshal_with_options(value, { "pretty": true, }) +# METADATA +# description: returns all elements of arr after the first rest(arr) := array.slice(arr, 1, count(arr)) +# METADATA +# description: converts x to set if array, returns x if set +# scope: document to_set(x) := x if is_set(x) to_set(x) := {y | some y in x} if not is_set(x) +# METADATA +# description: true if s1 and s2 has any intersecting items intersects(s1, s2) if count(intersection({s1, s2})) > 0 # METADATA @@ -72,3 +82,11 @@ single_set_item(s) := item if { some item in s } + +# METADATA +# description: returns any item of a set +any_set_item(s) := [x | some x in s][0] # this is convoluted.. but can't think of a better way + +# METADATA +# description: returns last index of item, or undefined (*not* -1) if missing +last_indexof(arr, item) := regal.last([i | some i, x in arr; x == item]) diff --git a/cmd/languageserver.go b/cmd/languageserver.go index 2da96263..52d8d536 100644 --- a/cmd/languageserver.go +++ b/cmd/languageserver.go @@ -64,5 +64,7 @@ func init() { languageServerCommand.Flags().BoolVarP(&verboseLogging, "verbose", "v", verboseLogging, "Enable verbose logging") + addPprofFlag(languageServerCommand.Flags()) + RootCommand.AddCommand(languageServerCommand) } diff --git a/docs/rules/custom/missing-metadata.md b/docs/rules/custom/missing-metadata.md new file mode 100644 index 00000000..7ac7192b --- /dev/null +++ b/docs/rules/custom/missing-metadata.md @@ -0,0 +1,86 @@ +# missing-metadata + +**Summary**: Package or rule missing metadata + +**Category**: Custom + +**Avoid** +```rego +package acmecorp.authz + +import rego.v1 + +authorized_users contains user if { + # logic to determine authorized users +} +``` + +**Prefer** +```rego +# METADATA +# description: The `acmecorp.authz` module provides authorization logic for the AcmeCorp application. +package acmecorp.authz + +import rego.v1 + +# METADATA +# description: Provides a set of all authorized users given the conditions in `input`. +# scope: document +authorized_users contains user if { + # logic to determine authorized users +} +``` + +## Rationale + +Using metadata annotations is a great way to document your policies, for both yourself and others. While using metadata +annotations _everywhere_ might be overkill for many projects, it should absolutely be considered for libraries, or +policies that target a larger audience. + +## Exceptions + +Rules and functions with an underscore prefix in their name are commonly used to denote that they are intended +to be used internally (i.e. within the same file) only, and while metadata occasionally help document these, +they are not part of the "public API". The `missing-metadata` thus excludes these from the metadata requirement. + +It is also possible to configure your own exceptions for both package and rule paths. See the configuration options +below. + +## Configuration Options + +This linter rule provides the following configuration options: + +```yaml +rules: + custom: + missing-metadata: + # note that all rules in the "custom" category are disabled by default + # (i.e. level "ignore"), so make sure to set the level to "error" if you + # want this enabled! + # + # one of "error", "warning", "ignore" + level: error + # package path pattern(s) to exclude from the requirement + # defaults to no exclusions + except-package-path-pattern: ^internal\.* + # rule path pattern(s) to exclude from the requirement + # defaults to no exclusions + except-rule-path-pattern: \.report$ + # you might also want to exclude files based on their name, + # like e.g. tests: + ignore: + files: + - "*_test.rego" +``` + +## Related Resources + +- OPA Docs: [Metadata](https://www.openpolicyagent.org/docs/latest/policy-language/#metadata) +- OPA Docs: [Annotations](https://www.openpolicyagent.org/docs/latest/policy-language/#annotations) +- Rego Style Guide: [Use Metadata Annotations](https://docs.styra.com/opa/rego-style-guide#use-metadata-annotations) + +## 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)! diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 784aeaa2..54fcaf4d 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -482,18 +482,21 @@ func TestLintAggregateIgnoreDirective(t *testing.T) { t.Fatalf("expected JSON response, got %v", stdout.String()) } - if rep.Summary.NumViolations != 1 { - t.Errorf("expected 1 violation, got %d", rep.Summary.NumViolations) + if rep.Summary.NumViolations != 2 { + t.Errorf("expected 2 violations, got %d", rep.Summary.NumViolations) + } + + if rep.Violations[0].Title != "no-defined-entrypoint" { + t.Errorf("expected violation 'no-defined-entrypoint', got %q", rep.Violations[0].Title) } - violation := rep.Violations[0] - if violation.Title != "unresolved-import" { - t.Errorf("expected violation 'unresolved-import', got %q", violation.Title) + if rep.Violations[1].Title != "unresolved-import" { + t.Errorf("expected violation 'unresolved-import', got %q", rep.Violations[1].Title) } // ensure that it's the file without the ignore directive that has the violation - if !strings.HasSuffix(violation.Location.File, "second.rego") { - t.Errorf("expected violation in second.rego, got %q", violation.Location.File) + if !strings.HasSuffix(rep.Violations[1].Location.File, "second.rego") { + t.Errorf("expected violation in second.rego, got %q", rep.Violations[1].Location.File) } } diff --git a/e2e/testdata/aggregates/ignore_directive/first.rego b/e2e/testdata/aggregates/ignore_directive/first.rego index 75d9c424..5c43276a 100644 --- a/e2e/testdata/aggregates/ignore_directive/first.rego +++ b/e2e/testdata/aggregates/ignore_directive/first.rego @@ -1,3 +1,5 @@ +# METADATA +# title: ignore_directive package ignore_directive import rego.v1 diff --git a/e2e/testdata/aggregates/three_policies/policy_1.rego b/e2e/testdata/aggregates/three_policies/policy_1.rego index 9c7188ba..26716ca6 100644 --- a/e2e/testdata/aggregates/three_policies/policy_1.rego +++ b/e2e/testdata/aggregates/three_policies/policy_1.rego @@ -1,3 +1,5 @@ +# METADATA +# title: three policies package three_policies import rego.v1 diff --git a/e2e/testdata/aggregates/three_policies/policy_2.rego b/e2e/testdata/aggregates/three_policies/policy_2.rego index 30447032..d9f74ac7 100644 --- a/e2e/testdata/aggregates/three_policies/policy_2.rego +++ b/e2e/testdata/aggregates/three_policies/policy_2.rego @@ -2,4 +2,6 @@ package three_policies import rego.v1 +# METADATA +# title: export export := [] diff --git a/e2e/testdata/aggregates/three_policies/policy_3.rego b/e2e/testdata/aggregates/three_policies/policy_3.rego index adb99f8a..337e9d60 100644 --- a/e2e/testdata/aggregates/three_policies/policy_3.rego +++ b/e2e/testdata/aggregates/three_policies/policy_3.rego @@ -2,4 +2,6 @@ package three_policies import rego.v1 +# METADATA +# title: my_policy_3 my_policy_3 := true diff --git a/e2e/testdata/aggregates/two_policies/policy_1.rego b/e2e/testdata/aggregates/two_policies/policy_1.rego index 1f103fdd..99c61efb 100644 --- a/e2e/testdata/aggregates/two_policies/policy_1.rego +++ b/e2e/testdata/aggregates/two_policies/policy_1.rego @@ -1,3 +1,5 @@ +# METADATA +# title: two policies package two_policies import rego.v1 diff --git a/e2e/testdata/aggregates/two_policies/policy_2.rego b/e2e/testdata/aggregates/two_policies/policy_2.rego index 9b95fe42..77adf40d 100644 --- a/e2e/testdata/aggregates/two_policies/policy_2.rego +++ b/e2e/testdata/aggregates/two_policies/policy_2.rego @@ -2,4 +2,6 @@ package two_policies import rego.v1 +# METADATA +# title: export export := [] diff --git a/e2e/testdata/violations/most_violations.rego b/e2e/testdata/violations/most_violations.rego index c86598d2..977d430a 100644 --- a/e2e/testdata/violations/most_violations.rego +++ b/e2e/testdata/violations/most_violations.rego @@ -160,7 +160,7 @@ function_arg_return if { i == 1 } -line_length_should_be_no_longer_than_120_characters_but_this_line_is_really_long_and_will_exceed_that_limit_which_is := "bad" +line_length := "should be no longer than 120 characters but this line is really long and will exceed that limit which is kinda bad" #no-whitespace-comment diff --git a/internal/lsp/server.go b/internal/lsp/server.go index da54d7b7..b29b5974 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -648,7 +648,14 @@ func (l *LanguageServer) StartCommandWorker(ctx context.Context) { // nolint:mai // used as the input rather than the contents of input.json. This is a development feature for // working on rules (built-in or custom), allowing querying the AST of the module directly. if len(currentModule.Comments) > 0 && regalEvalUseAsInputComment.Match(currentModule.Comments[0].Text) { - bs, err := encoding.JSON().Marshal(currentModule) + inputMap, err := rparse.PrepareAST(file, currentContents, currentModule) + if err != nil { + l.logError(fmt.Errorf("failed to prepare module: %w", err)) + + break + } + + bs, err := encoding.JSON().Marshal(inputMap) if err != nil { l.logError(fmt.Errorf("failed to marshal module: %w", err)) diff --git a/internal/update/update.rego b/internal/update/update.rego index 76e2230b..5fd8e6b0 100644 --- a/internal/update/update.rego +++ b/internal/update/update.rego @@ -1,14 +1,18 @@ +# METADATA +# description: utility module to help determine if an update of Regal should be recommended package update import rego.v1 -current_version := trim(input.current_version, "v") - -latest_version := trim(input.latest_version, "v") - default needs_update := false +# METADATA +# description: true if current version is behind latest version +# scope: document needs_update if { + current_version := trim(input.current_version, "v") + latest_version := trim(input.latest_version, "v") + semver.is_valid(current_version) semver.compare(current_version, latest_version) == -1 } diff --git a/internal/web/server.go b/internal/web/server.go index 3c41240a..44ad8e89 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -13,6 +13,8 @@ import ( "github.com/styrainc/regal/internal/explorer" "github.com/styrainc/regal/internal/lsp/cache" "github.com/styrainc/regal/internal/lsp/clients" + + _ "net/http/pprof" //nolint:gosec ) const mainTemplate = "main.tpl" @@ -125,6 +127,11 @@ func (s *Server) Start(_ context.Context) { mux.Handle("/assets/", http.FileServer(http.FS(assets))) + // pprof handlers + mux.HandleFunc("/debug/pprof/", http.DefaultServeMux.ServeHTTP) + mux.HandleFunc("/debug/pprof/profile", http.DefaultServeMux.ServeHTTP) + mux.HandleFunc("/debug/pprof/heap", http.DefaultServeMux.ServeHTTP) + listener, err := net.Listen("tcp", "localhost:0") if err != nil { panic(err) diff --git a/pkg/linter/linter.go b/pkg/linter/linter.go index 75773da1..fbd55b88 100644 --- a/pkg/linter/linter.go +++ b/pkg/linter/linter.go @@ -849,15 +849,13 @@ func resultSetToReport(resultSet rego.ResultSet) (report.Report, error) { if binding, ok := resultSet[0].Bindings["lint"]; ok { if err := rio.JSONRoundTrip(binding, &r); err != nil { - return report.Report{}, - fmt.Errorf("JSON rountrip failed for bindings: %v %w", binding, err) + return report.Report{}, fmt.Errorf("JSON rountrip failed for bindings: %v %w", binding, err) } } if binding, ok := resultSet[0].Bindings["lint_aggregate"]; ok { if err := rio.JSONRoundTrip(binding, &r); err != nil { - return report.Report{}, - fmt.Errorf("JSON rountrip failed for bindings: %v %w", binding, err) + return report.Report{}, fmt.Errorf("JSON rountrip failed for bindings: %v %w", binding, err) } }