diff --git a/.github/workflows/update-example-index.yaml b/.github/workflows/update-example-index.yaml index a45f2f4a2..e21e0c711 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 5c0c8f2d0..40b5b84b4 100644 --- a/.regal/config.yaml +++ b/.regal/config.yaml @@ -2,3 +2,18 @@ ignore: files: - e2e/* - pkg/* + +rules: + custom: + missing-metadata: + level: ignore + 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 d2f36d524..10c49287a 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 df16ba445..91942cf6b 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 73% rename from build/workflows/update-example-index/process.rego rename to build/workflows/update_example_index.rego index 275e5e1ca..7f8dfac43 100644 --- a/build/workflows/update-example-index/process.rego +++ b/build/workflows/update_example_index.rego @@ -1,5 +1,11 @@ -# 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 diff --git a/bundle/regal/ast/ast.rego b/bundle/regal/ast/ast.rego index 9d8d8431c..176eedde5 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)) @@ -280,6 +330,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 +357,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 c5e0c89c6..20d6996fa 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 92d14abe6..52435e094 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 4058ad551..724cc5a7f 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 17cd582b0..000000000 --- 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 5bb8f4841..000000000 --- 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 175bd3474..5f80f29e8 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 856d9545e..a8338144c 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 3a447f185..1e73c8a45 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 ad5eeab08..aa44aac70 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 2726fc631..67fd7fe8b 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 b78137579..245e91ee7 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 8587b943e..7fcfc0ace 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 b4f020e71..6ede5b699 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 1651f86df..745f210db 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 8e72743be..5a4b5bf4d 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 88d776c2a..995152666 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 897dd00a8..529000941 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 79374eabd..49e24df06 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 c5aa78d72..e59cbcd37 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 418269ffe..553aa108f 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 a13007cea..78441af4f 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 d72c124e5..bd722ee28 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 76b3bb2f8..81619191e 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 780a68e62..207cedb54 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 95d9d11e3..e74d761d9 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 7cb6027ff..d296d92f8 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 5fe3b480c..81869f0a5 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 827aad30f..959982a04 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 461f9e21d..9e59124e4 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 527cbb57e..5bbf471d0 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 c3c0a2ad1..edcae9dac 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 af2625d81..164e7f022 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( @@ -122,7 +144,7 @@ aggregate_report contains violation if { ignore_directives := object.get(input.ignore_directives, violation.location.file, {}) - not ignored(violation, util.keys_to_numbers(ignore_directives)) + not _ignored(violation, util.keys_to_numbers(ignore_directives)) } # METADATA @@ -149,15 +171,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 c90beffa8..0d5069334 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 134c51686..c57eb5201 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 df8d16f51..d21c41c15 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 b98bbd305..7a973c0f5 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 9aa5b81f0..c1c9c16bd 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 dcb937cfe..0b700a236 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 000000000..dd5d19d53 --- /dev/null +++ b/bundle/regal/rules/custom/missing-metadata/missing_metadata.rego @@ -0,0 +1,148 @@ +# 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_annotations": _package_annotations, + "package_location": input["package"].location, + "rule_annotations": object.union(_annotated_rules, _not_annotated_rules), + "rule_locations": _rule_locations, +}) + +_package_annotations contains annotation if { + some annotation in input.annotations + annotation.scope in {"package", "subpackages"} +} + +_annotated_rules[path] contains annotation if { + some rule in ast.public_rules_and_functions + every part in rule.head.ref { + not startswith(part.value, "_") + } + + rref := ast.ref_static_to_string(rule.head.ref) + path := concat(".", [ast.package_name, rref]) + + some annotation in rule.annotations +} + +_not_annotated_rules[path] := set() if { + ref := ast.ref_static_to_string(ast.public_rules_and_functions[_].head.ref) + annotations := [annotation | + some rule in ast.public_rules_and_functions + ast.ref_static_to_string(rule.head.ref) == ref + some annotation in rule.annotations + ] + + count(annotations) == 0 + + path := concat(".", [ast.package_name, ref]) +} + +_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 + + sum([count(item.aggregate_data.package_annotations) | some item in aggs]) == 0 + + 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 { + count(aggregate.annotations) == 0 + } + + 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], + }, + )}) +} + +# note(anderseknert): below is an interesting case, as so far I had thought +# of metadata annotations as necessary only to document the public "API".. +# and using them on "_private" rules a "bug" we should flag +# however, the schema annotation is an outlier in that regard, as this would +# not compile without a pointer to the schema + +# 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 + + agg := { + "file": item.aggregate_source.file, + "location": item.aggregate_data.rule_locations[rule_path], + "annotations": annotations, + } +} + +_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 000000000..75b36895d --- /dev/null +++ b/bundle/regal/rules/custom/missing-metadata/missing_metadata_test.rego @@ -0,0 +1,169 @@ +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_annotations": {{ + "location": "1:1:IyBNRVRBREFUQQojIHRpdGxlOiBwa2c=", + "scope": "package", + "title": "pkg", + }}, + "package_location": "3:1:cGFja2FnZQ==", + "rule_annotations": { + "foo.bar.none": set(), + "foo.bar.rule": {{ + "location": "5:1:IyBNRVRBREFUQQojIHRpdGxlOiBydWxl", + "scope": "rule", + "title": "rule", + }}, + }, + "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 daba45228..b09e28406 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 2b45982ce..97e8f899d 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 72ed622a2..42a97eef2 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 2e0b300de..5905ad5cb 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 b76347e75..828a2f6dd 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 fce9e2a4e..ba0f64635 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 7c7265067..0689a96ff 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 9bd121b7f..6aff1f549 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 7195af568..f5c310348 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 ce086241f..b3d813878 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 52947a8f3..4df5c56ba 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 af25fab6d..eddcd31fb 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 76cea6759..ae8a44f69 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 dc185dff1..712b977e5 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 5440645c5..e0914fb23 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 3b8377b5c..224c1c1e1 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 14aa22adb..b149ea3f8 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 52d7bc8b5..c0cfc8abd 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 df8032434..4155ac726 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 5a1ab019b..98b48b79b 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 a316da3bd..13b0c9fe1 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 e0fc64952..45bfc1116 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 b8fb1da84..1e9af6e57 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 d041f5541..1d2e26132 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 fbe8a40e8..9b39adfb6 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/yoda-condition/yoda_condition.rego b/bundle/regal/rules/style/yoda-condition/yoda_condition.rego index edf18cd44..3e7680157 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 240487fb4..4018ed27f 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 c821e596a..2eb6ec2c2 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 857ee4ce7..2f3f0872c 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/docs/rules/custom/missing-metadata.md b/docs/rules/custom/missing-metadata.md new file mode 100644 index 000000000..e36ab4a92 --- /dev/null +++ b/docs/rules/custom/missing-metadata.md @@ -0,0 +1,43 @@ +# missing-metadata + +**Summary**: ADD DESCRIPTION HERE + +**Category**: CUSTOM + +**Automatically fixable**: Yes/No + +**Avoid** +```rego +package policy + +# ... ADD CODE TO AVOID HERE +``` + +**Prefer** +```rego +package policy + +# ... ADD CODE TO PREFER HERE +``` + +## Rationale + +ADD RATIONALE HERE + +## Configuration Options + +This linter rule provides the following configuration options: + +```yaml +rules: + custom: + missing-metadata: + # one of "error", "warning", "ignore" + level: error +``` + +## 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/testdata/violations/most_violations.rego b/e2e/testdata/violations/most_violations.rego index c86598d23..977d430ae 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/go.mod b/go.mod index 65c6dfc70..e291273df 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/jstemmer/go-junit-report/v2 v2.1.0 github.com/mitchellh/mapstructure v1.5.0 github.com/olekukonko/tablewriter v0.0.5 - github.com/open-policy-agent/opa v0.68.0 + github.com/open-policy-agent/opa v0.68.1-0.20240923113314-e959bce1410d github.com/owenrumney/go-sarif/v2 v2.3.3 github.com/pdevine/go-asciisprite v0.1.6 github.com/pkg/profile v1.7.0 @@ -26,7 +26,7 @@ require ( ) require ( - github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect @@ -44,7 +44,6 @@ require ( github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/glog v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect @@ -63,7 +62,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/prometheus/client_golang v1.20.2 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -81,12 +80,10 @@ require ( go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240820151423-278611b39280 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280 // indirect diff --git a/go.sum b/go.sum index aded13ca6..16c906d56 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= @@ -172,8 +172,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2eCJYSqJQ= -github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w= +github.com/open-policy-agent/opa v0.68.1-0.20240923113314-e959bce1410d h1:5h+lfY3dT8d/pc/f+EwGrz77g1fuVuzdF2E9WIlxmRE= +github.com/open-policy-agent/opa v0.68.1-0.20240923113314-e959bce1410d/go.mod h1:V9vbXtXRhQ8G4/sjL/g6txe8MaLOfkmmklufwZNaVK4= github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= github.com/owenrumney/go-sarif/v2 v2.3.3 h1:ubWDJcF5i3L/EIOER+ZyQ03IfplbSU1BLOE26uKQIIU= github.com/owenrumney/go-sarif/v2 v2.3.3/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= @@ -188,8 +188,8 @@ github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDj github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= -github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= @@ -265,8 +265,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= @@ -280,8 +280,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -303,15 +303,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -321,8 +321,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -335,8 +335,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240820151423-278611b39280 h1: google.golang.org/genproto/googleapis/api v0.0.0-20240820151423-278611b39280/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280 h1:XQMA2e105XNlEZ8NRF0HqnUOZzP14sUSsgL09kpdNnU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/lsp/server.go b/internal/lsp/server.go index da54d7b74..b29b59747 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))