From 60b6c9209dfbf6b7eca3fe6324c970402ecfb195 Mon Sep 17 00:00:00 2001 From: LarytheLord Date: Wed, 25 Feb 2026 21:36:29 +0530 Subject: [PATCH] cli: include first finding location in exit error --- cmd/root.go | 11 ++++++++- cmd/root_test.go | 1 + pkg/parser/parser.go | 49 +++++++++++++++++++++++++++++++++++---- pkg/parser/parser_test.go | 17 ++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index e4822d03..bdfea9fa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -121,12 +121,21 @@ func rootRunE(cmd *cobra.Command, args []string) error { return err } - findings := p.ParsePaths(print, parseArgs(args)...) + parseReport := p.ParsePathsWithReport(print, parseArgs(args)...) + findings := parseReport.FilesWithFindings if exitOneOnFailure && findings > 0 { // We intentionally return an error if exitOneOnFailure is true, but don't want to show usage cmd.SilenceUsage = true err = fmt.Errorf("files with findings: %d", findings) + if parseReport.FirstFinding != nil { + err = fmt.Errorf("%w (first finding: %s:%d:%d)", + err, + parseReport.FirstFinding.Filename, + parseReport.FirstFinding.Line, + parseReport.FirstFinding.Column, + ) + } } if findings == 0 { diff --git a/cmd/root_test.go b/cmd/root_test.go index 87071e7e..8b2ac537 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -120,6 +120,7 @@ func TestRunE(t *testing.T) { err := rootRunE(new(cobra.Command), []string{"../testdata"}) assert.Error(t, err) assert.Regexp(t, regexp.MustCompile(`^files with findings: \d`), err.Error()) + assert.Regexp(t, regexp.MustCompile(`first finding: .+:\d+:\d+`), err.Error()) }) t.Run("no rules enabled", func(t *testing.T) { diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index a542b7f3..5a192d85 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -30,6 +30,19 @@ type Parser struct { rchan chan result.FileResults } +// FindingLocation captures where a finding starts in source. +type FindingLocation struct { + Filename string + Line int + Column int +} + +// ParseReport summarizes ParsePaths results. +type ParseReport struct { + FilesWithFindings int + FirstFinding *FindingLocation +} + // NewParser returns a pointer to a Parser that is used to check for findings // based on the rules provided, ignoring files based on the ignorer provided func NewParser(rules []*rule.Rule, ignorer *ignore.Ignore) *Parser { @@ -42,6 +55,13 @@ func NewParser(rules []*rule.Rule, ignorer *ignore.Ignore) *Parser { // ParsePaths parses all files provided and returns the number of files with findings func (p *Parser) ParsePaths(print printer.Printer, paths ...string) int { + return p.ParsePathsWithReport(print, paths...).FilesWithFindings +} + +// ParsePathsWithReport parses all files provided and returns a report including +// the number of files with findings and the first finding location. +func (p *Parser) ParsePathsWithReport(print printer.Printer, paths ...string) ParseReport { + report := ParseReport{} print.Start() defer print.End() @@ -49,9 +69,11 @@ func (p *Parser) ParsePaths(print printer.Printer, paths ...string) int { if util.InSlice(os.Stdin.Name(), paths) { r, _ := p.generateFileFindings(os.Stdin) if r.Len() > 0 { + report.FirstFinding = firstFindingLocation(r) print.Print(r) } - return r.Len() + report.FilesWithFindings = r.Len() + return report } if len(paths) == 0 { @@ -75,13 +97,32 @@ func (p *Parser) ParsePaths(print printer.Printer, paths ...string) int { close(p.rchan) }() - findings := 0 for r := range p.rchan { sort.Sort(r) print.Print(&r) - findings++ + report.FilesWithFindings++ + if report.FirstFinding == nil { + report.FirstFinding = firstFindingLocation(&r) + } + } + return report +} + +func firstFindingLocation(r *result.FileResults) *FindingLocation { + if r == nil || len(r.Results) == 0 { + return nil + } + + start := r.Results[0].GetStartPosition() + if start == nil { + return nil + } + + return &FindingLocation{ + Filename: start.Filename, + Line: start.Line, + Column: start.Column, } - return findings } func (p *Parser) processFiles(files <-chan string, done chan bool, wg *sync.WaitGroup) { diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 639395e7..b1858d71 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -89,6 +89,23 @@ func parsePathTests(t *testing.T) { assert.EqualValues(t, &expected, pr.results[0]) }) + t.Run("finding report includes first location", func(t *testing.T) { + f, err := newFile(t, "i have a whitelist") + assert.NoError(t, err) + + pr := new(testPrinter) + p, err := testParser() + assert.NoError(t, err) + report := p.ParsePathsWithReport(pr, f.Name()) + + assert.Equal(t, 1, report.FilesWithFindings) + if assert.NotNil(t, report.FirstFinding) { + assert.Equal(t, filepath.ToSlash(f.Name()), report.FirstFinding.Filename) + assert.Equal(t, 1, report.FirstFinding.Line) + assert.Equal(t, 9, report.FirstFinding.Column) + } + }) + t.Run("no findings", func(t *testing.T) { f, err := newFile(t, "i have a no findings\n") assert.NoError(t, err)