Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Allow regal lint - to lint from stdin #1122

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bundle/regal/config/config.rego
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ docs["resolve_url"](url, category) := replace(

merged_config := data.internal.combined_config

capabilities := merged_config.capabilities
capabilities := object.union(merged_config.capabilities, {"special": special})

special contains "no_filename" if input.regal.file.name == "stdin"

default for_rule(_, _) := {"level": "error"}

Expand Down
29 changes: 29 additions & 0 deletions bundle/regal/main/main_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import rego.v1

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

test_basic_input_success if {
report := main.report with input as regal.parse_module("p.rego", `package p`)
Expand Down Expand Up @@ -216,6 +217,34 @@ test_force_exclude_file_config if {
count(report) == 0
}

test_lint_from_stdin_disables_rules_depending_on_filename_creates_notices if {
policy := `package p

import rego.v1

camelCase := "yes"

test_camelcase if {
camelCase == "yes"
}
`
result := main with input as regal.parse_module("p.rego", policy)
with input.regal.file.name as "stdin"
with config.merged_config as {
"capabilities": {},
"rules": {
"style": {"prefer-snake-case": {"level": "error"}},
"testing": {"file-missing-test-suffix": {"level": "error"}},
"idiomatic": {"directory-package-mismatch": {"level": "error"}},
},
}

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"}
}

# regal ignore:rule-length
test_main_lint if {
ast := {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import data.regal.ast
import data.regal.config
import data.regal.result

# METADATA
# description: disabled when filename is unknown
# custom:
# severity: warn
notices contains result.notice(rego.metadata.chain()) if "no_filename" in config.capabilities.special

report contains violation if {
# get the last n components from file path, where n == count(_pkg_path_values)
file_path_length_matched := array.slice(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ package regal.rules.testing["file-missing-test-suffix"]
import rego.v1

import data.regal.ast
import data.regal.config
import data.regal.result

# METADATA
# description: disabled when filename is unknown
# custom:
# severity: warn
notices contains result.notice(rego.metadata.chain()) if "no_filename" in config.capabilities.special

report contains violation if {
count(ast.tests) > 0

Expand Down
8 changes: 8 additions & 0 deletions bundle/regal/util/util.rego
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,11 @@ to_set(x) := x if is_set(x)
to_set(x) := {y | some y in x} if not is_set(x)

intersects(s1, s2) if count(intersection({s1, s2})) > 0

# METADATA
# description: returns the item contained in a single-item set
single_set_item(s) := item if {
count(s) == 1

some item in s
}
5 changes: 5 additions & 0 deletions pkg/config/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import (
)

func FilterIgnoredPaths(paths, ignore []string, checkFileExists bool, rootDir string) ([]string, error) {
// - special case for stdin, return as is
if len(paths) == 1 && paths[0] == "-" {
return paths, nil
}

// if set, rootDir is normalized to end with a platform appropriate separator
if rootDir != "" && !strings.HasSuffix(rootDir, string(filepath.Separator)) {
rootDir += string(filepath.Separator)
Expand Down
24 changes: 24 additions & 0 deletions pkg/rules/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package rules
import (
"context"
"fmt"
"io"
"os"
"sort"
"sync"

Expand Down Expand Up @@ -58,6 +60,10 @@ func NewInput(fileContent map[string]string, modules map[string]*ast.Module) Inp
// paths point to valid Rego files. Use config.FilterIgnoredPaths to filter out unwanted content *before* calling this
// function.
func InputFromPaths(paths []string) (Input, error) {
if len(paths) == 1 && paths[0] == "-" {
return inputFromStdin()
}

fileContent := make(map[string]string, len(paths))
modules := make(map[string]*ast.Module, len(paths))

Expand Down Expand Up @@ -98,6 +104,24 @@ func InputFromPaths(paths []string) (Input, error) {
return NewInput(fileContent, modules), nil
}

func inputFromStdin() (Input, error) {
// Ideally, we'd just pass the reader to OPA, but as the parser materializes
// the input immediately anyway, there's currently no benefit to doing so.
bs, err := io.ReadAll(os.Stdin)
if err != nil {
return Input{}, fmt.Errorf("failed to read from reader: %w", err)
}

policy := string(bs)

module, err := parse.Module("stdin", policy)
if err != nil {
return Input{}, fmt.Errorf("failed to parse module from stdin: %w", err)
}

return NewInput(map[string]string{"stdin": policy}, map[string]*ast.Module{"stdin": module}), nil
}

// InputFromText creates a new Input from raw Rego text.
func InputFromText(fileName, text string) (Input, error) {
mod, err := parse.Module(fileName, text)
Expand Down