@@ -32,6 +32,7 @@ import (
3232 "github.com/open-policy-agent/opa/v1/format"
3333 "github.com/styrainc/regal/pkg/config"
3434 "github.com/styrainc/regal/pkg/linter"
35+ "github.com/styrainc/regal/pkg/report"
3536 "github.com/styrainc/regal/pkg/rules"
3637 "gopkg.in/yaml.v3"
3738)
@@ -289,13 +290,39 @@ func (p *PolicyToLint) runRegalLinter(filePath, content string) {
289290 return
290291 }
291292
292- // Add any violations to the policy errors
293+ // Parse the Rego AST to map line numbers to rule names
294+ regoRuleMap := p .buildRegoRuleMap (content )
295+
296+ // Add violations to the policy errors
293297 for _ , v := range report .Violations {
294- errorStr := strings . ReplaceAll ( v . Description , "`opa fmt`" , "`--format`" )
298+ errorStr := p . formatViolationError ( v , regoRuleMap )
295299 p .AddError (filePath , errorStr , v .Location .Row )
296300 }
297301}
298302
303+ // Creates a formatted error message from a Regal violation
304+ // Follows format <file>:<line>: [<ruleName>] <errorMsg> - <docLinks>
305+ func (p * PolicyToLint ) formatViolationError (v report.Violation , regoRuleMap map [int ]string ) string {
306+ // Extract resources
307+ resources := make ([]string , 0 , len (v .RelatedResources ))
308+ for _ , r := range v .RelatedResources {
309+ resources = append (resources , r .Reference )
310+ }
311+ resourceStr := strings .Join (resources , ", " )
312+
313+ // Try to identify which Rego rule contains this violation
314+ regoRuleName , exists := regoRuleMap [v .Location .Row ]
315+ if ! exists {
316+ regoRuleName = ""
317+ } else {
318+ regoRuleName = fmt .Sprintf ("[%s]" , regoRuleName )
319+ }
320+
321+ // Format the error message
322+ lintError := fmt .Sprintf ("%s: %s - %s" , regoRuleName , v .Description , resourceStr )
323+ return strings .ReplaceAll (lintError , "`opa fmt`" , "`--format`" )
324+ }
325+
299326// Attempts to load configuration in this order:
300327// 1. User-specified config
301328// 2. Default config
@@ -340,3 +367,39 @@ func (p *PolicyToLint) loadDefaultConfig() (*config.Config, error) {
340367
341368 return & cfg , nil
342369}
370+
371+ // Creates a mapping from line numbers to Rego rule names
372+ func (p * PolicyToLint ) buildRegoRuleMap (regoSrc string ) map [int ]string {
373+ ruleMap := make (map [int ]string )
374+
375+ // Parse the Rego source into AST
376+ module , err := opaAst .ParseModule ("" , regoSrc )
377+ if err != nil {
378+ // Return empty map if parsing fails
379+ return ruleMap
380+ }
381+
382+ // Walk through the AST to find rule definitions
383+ for _ , rule := range module .Rules {
384+ if rule .Location != nil {
385+ ruleName := string (rule .Head .Name )
386+ startLine := rule .Location .Row
387+ endLine := startLine
388+
389+ // Try to find the end line of the rule
390+ if len (rule .Body ) > 0 {
391+ lastExpr := rule .Body [len (rule .Body )- 1 ]
392+ if lastExpr .Location != nil {
393+ endLine = lastExpr .Location .Row
394+ }
395+ }
396+
397+ // Map all lines within this rule to the rule name
398+ for line := startLine ; line <= endLine ; line ++ {
399+ ruleMap [line ] = ruleName
400+ }
401+ }
402+ }
403+
404+ return ruleMap
405+ }
0 commit comments