Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bundle/regal/config/config.rego
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ docs["resolve_url"](url, category) := replace(
# description: the default configuration with user config merged on top (if provided)
merged_config := data.internal.combined_config

# METADATA
# description: the config the user manually configured without defaults
user_config := data.internal.user_config

# ensure that config.rules can be referenced in tests even without mocking
default rules := {}

Expand Down
20 changes: 11 additions & 9 deletions bundle/regal/main/main.rego
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ package regal.main

import data.regal.ast
import data.regal.config
import data.regal.notices
import data.regal.util

# METADATA
# description: set of all notices returned from linter rules
lint.notices contains _grouped_notices[_][_][_] if "lint" in input.regal.operations
lint.notices contains notice if {
"lint" in input.regal.operations

some category, title
_rules_to_run[category][title]

rule_notices := notices.promoted_notices[category][title]
some notice in rule_notices
}

# METADATA
# description: map of all ignore directives encountered when linting
Expand Down Expand Up @@ -57,13 +66,6 @@ _rules_to_run[category] contains title if {
not config.excluded_file(category, title, relative_filename)
}

_grouped_notices[category][title] contains notice if {
some category, title
_rules_to_run[category][title]

some notice in data.regal.rules[category][title].notices
}

# METADATA
# title: report
# description: |
Expand Down Expand Up @@ -94,7 +96,7 @@ report contains violation if {
some category, title
_rules_to_run[category][title]

count(object.get(_grouped_notices, [category, title], [])) == 0
count(object.get(notices.promoted_notices, [category, title], [])) == 0

some violation in data.regal.rules[category][title].report

Expand Down
16 changes: 0 additions & 16 deletions bundle/regal/main/main_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -358,22 +358,6 @@ test_rules_to_run_not_excluded if {
rules_to_run == {"testing": {"test"}}
}

test_notices if {
notice := {
"category": "idiomatic",
"description": "here's a notice",
"level": "notice",
"title": "testme notice",
"severity": "none",
}

notices := main.lint.notices with main._rules_to_run as {"idiomatic": {"testme"}}
with data.regal.rules.idiomatic.testme.notices as {notice} # regal ignore:unresolved-reference
with input.regal.operations as ["lint"]

notices == {notice}
}

test_main_fail_when_input_not_object if {
violation := {
"category": "error",
Expand Down
46 changes: 46 additions & 0 deletions bundle/regal/notices/notices.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# METADATA
# description: |
# package with functionality for post-processing notices
# to ensure they are presented correctly as errors when relevant.
package regal.notices

import data.regal.config

# METADATA
# scope: rule
# description: |
# promoted_notices maps notices from rules, potentially changing their severity
# based on user configuration
promoted_notices[category][title] contains original_notice if {
some category, title
notices := data.regal.rules[category][title].notices

some original_notice in notices

not config.user_config.rules[category][title]
}

promoted_notices[category][title] contains notice if {
some category, title
notices := data.regal.rules[category][title].notices

some notice in notices

rule_config := config.user_config.rules[category][title]
object.get(rule_config, "level", "") == "ignore"
}

promoted_notices[category][title] contains notice if {
some category, title
notices := data.regal.rules[category][title].notices

some original_notice in notices

rule_config := config.user_config.rules[category][title]
object.get(rule_config, "level", "") != "ignore"

# Use configured level as severity, or default to "error"
new_severity := object.get(rule_config, "level", "error")

notice := object.union(original_notice, {"severity": new_severity})
}
72 changes: 72 additions & 0 deletions bundle/regal/notices/notices_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package regal.notices_test

import data.regal.notices

test_promoted_notices_with_level_error if {
result := notices.promoted_notices with data.regal.rules.imports["use-rego-v1"].notices as {_example_notice}
with data.internal.user_config as {"rules": {"imports": {"use-rego-v1": {"level": "error"}}}}

# With user config level set to error, the severity should be promoted to error
result.imports["use-rego-v1"] == {object.union(_example_notice, {"severity": "error"})}
}

test_promoted_notices_with_level_ignore if {
result := notices.promoted_notices with data.regal.rules.imports["use-rego-v1"].notices as {_example_notice}
with data.internal.user_config as {"rules": {"imports": {"use-rego-v1": {"level": "ignore"}}}}

# With user config level set to ignore, the severity should stay none
result.imports["use-rego-v1"] == {_example_notice}
}

test_promoted_notices_with_level_warning if {
result := notices.promoted_notices with data.regal.rules.imports["use-rego-v1"].notices as {_example_notice}
with data.internal.user_config as {"rules": {"imports": {"use-rego-v1": {"level": "warning"}}}}

# With user config level set to warning, the severity should be promoted to warning
result.imports["use-rego-v1"] == {object.union(_example_notice, {"severity": "warning"})}
}

test_promoted_notices_configured_without_level if {
# Rule is configured but level field is not present
result := notices.promoted_notices with data.regal.rules.imports["use-rego-v1"].notices as {_example_notice}
with data.internal.user_config as {"rules": {"imports": {"use-rego-v1": {}}}}

# When configured without level, should default to error
result.imports["use-rego-v1"] == {object.union(_example_notice, {"severity": "error"})}
}

test_promoted_notices_mixed_configured_and_unconfigured if {
notice_configured_rule := {
"category": "imports",
"description": "Configured rule",
"level": "notice",
"title": "use-rego-v1",
"severity": "none",
}

notice_unconfigured_rule := {
"category": "bugs",
"description": "Unconfigured rule",
"level": "notice",
"title": "deprecated-builtin",
"severity": "none",
}

result := notices.promoted_notices with data.regal.rules.imports["use-rego-v1"].notices as {notice_configured_rule}
with data.regal.rules.bugs["deprecated-builtin"].notices as {notice_unconfigured_rule}
with data.internal.user_config as {"rules": {"imports": {"use-rego-v1": {"level": "error"}}}}

# Configured rule should have severity promoted to error
result.imports["use-rego-v1"] == {object.union(notice_configured_rule, {"severity": "error"})}

# Unconfigured rule should keep original severity: none
result.bugs["deprecated-builtin"] == {notice_unconfigured_rule}
}

_example_notice := {
"category": "imports",
"description": "Test rule description",
"level": "notice",
"title": "use-rego-v1",
"severity": "none",
}
6 changes: 6 additions & 0 deletions internal/lsp/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ func prepareRegoArgs(
evalConfig = *cfg
}

userConfigMap := map[string]any{}
if cfg != nil {
userConfigMap = config.ToMap(*cfg)
}

internalBundle := &bundle.Bundle{
Manifest: bundle.Manifest{
Roots: &[]string{"internal"},
Expand All @@ -127,6 +132,7 @@ func prepareRegoArgs(
Data: map[string]any{
"internal": map[string]any{
"combined_config": config.ToMap(evalConfig),
"user_config": userConfigMap,
"capabilities": caps,
},
},
Expand Down
2 changes: 1 addition & 1 deletion internal/lsp/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestEvalWorkspacePathInternalData(t *testing.T) {
val := testutil.MustBe[[]any](t, res.Value)
act := util.Sorted(testutil.Must(util.AnySliceTo[string](val))(t))

if exp := []string{"capabilities", "combined_config"}; !slices.Equal(exp, act) {
if exp := []string{"capabilities", "combined_config", "user_config"}; !slices.Equal(exp, act) {
t.Fatalf("expected %v, got %v", exp, act)
}
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/linter/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,11 @@ func (l Linter) createDataBundle(conf config.Config) *bundle.Bundle {
"ignore_files": util.NilSliceToEmpty(l.ignoreFiles),
}

userConfigMap := map[string]any{}
if l.userConfig != nil {
userConfigMap = config.ToMap(*l.userConfig)
}

return &bundle.Bundle{
Manifest: bundle.Manifest{
Roots: &[]string{"internal", "eval"},
Expand All @@ -659,6 +664,7 @@ func (l Linter) createDataBundle(conf config.Config) *bundle.Bundle {
},
"internal": map[string]any{
"combined_config": config.ToMap(conf),
"user_config": userConfigMap,
"capabilities": rio.ToMap(config.CapabilitiesForThisVersion()),
"path_prefix": l.pathPrefix,
},
Expand Down
Loading