Sorting result.Issues implementation (golangci/golangci-lint#1217) (#1218)
This commit is contained in:
parent
65e1b30ebd
commit
6e7c317610
@ -80,6 +80,7 @@ func initFlagSet(fs *pflag.FlagSet, cfg *config.Config, m *lintersdb.Manager, is
|
|||||||
fs.BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, wh("Print lines of code with issue"))
|
fs.BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, wh("Print lines of code with issue"))
|
||||||
fs.BoolVar(&oc.PrintLinterName, "print-linter-name", true, wh("Print linter name in issue line"))
|
fs.BoolVar(&oc.PrintLinterName, "print-linter-name", true, wh("Print linter name in issue line"))
|
||||||
fs.BoolVar(&oc.UniqByLine, "uniq-by-line", true, wh("Make issues output unique by line"))
|
fs.BoolVar(&oc.UniqByLine, "uniq-by-line", true, wh("Make issues output unique by line"))
|
||||||
|
fs.BoolVar(&oc.SortResults, "sort-results", false, wh("Sort linter results"))
|
||||||
fs.BoolVar(&oc.PrintWelcomeMessage, "print-welcome", false, wh("Print welcome message"))
|
fs.BoolVar(&oc.PrintWelcomeMessage, "print-welcome", false, wh("Print welcome message"))
|
||||||
fs.StringVar(&oc.PathPrefix, "path-prefix", "", wh("Path prefix to add to output"))
|
fs.StringVar(&oc.PathPrefix, "path-prefix", "", wh("Path prefix to add to output"))
|
||||||
hideFlag("print-welcome") // no longer used
|
hideFlag("print-welcome") // no longer used
|
||||||
|
@ -527,6 +527,7 @@ type Config struct {
|
|||||||
PrintIssuedLine bool `mapstructure:"print-issued-lines"`
|
PrintIssuedLine bool `mapstructure:"print-issued-lines"`
|
||||||
PrintLinterName bool `mapstructure:"print-linter-name"`
|
PrintLinterName bool `mapstructure:"print-linter-name"`
|
||||||
UniqByLine bool `mapstructure:"uniq-by-line"`
|
UniqByLine bool `mapstructure:"uniq-by-line"`
|
||||||
|
SortResults bool `mapstructure:"sort-results"`
|
||||||
PrintWelcomeMessage bool `mapstructure:"print-welcome"`
|
PrintWelcomeMessage bool `mapstructure:"print-welcome"`
|
||||||
PathPrefix string `mapstructure:"path-prefix"`
|
PathPrefix string `mapstructure:"path-prefix"`
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,7 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint
|
|||||||
processors.NewPathShortener(),
|
processors.NewPathShortener(),
|
||||||
getSeverityRulesProcessor(&cfg.Severity, log, lineCache),
|
getSeverityRulesProcessor(&cfg.Severity, log, lineCache),
|
||||||
processors.NewPathPrefixer(cfg.Output.PathPrefix),
|
processors.NewPathPrefixer(cfg.Output.PathPrefix),
|
||||||
|
processors.NewSortResults(cfg),
|
||||||
},
|
},
|
||||||
Log: log,
|
Log: log,
|
||||||
}, nil
|
}, nil
|
||||||
|
173
pkg/result/processors/sort_results.go
Normal file
173
pkg/result/processors/sort_results.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package processors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golangci/golangci-lint/pkg/config"
|
||||||
|
"github.com/golangci/golangci-lint/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Base propose of this functionality to sort results (issues)
|
||||||
|
// produced by various linters by analyzing code. We achieving this
|
||||||
|
// by sorting results.Issues using processor step, and chain based
|
||||||
|
// rules that can compare different properties of the Issues struct.
|
||||||
|
|
||||||
|
var _ Processor = (*SortResults)(nil)
|
||||||
|
|
||||||
|
type SortResults struct {
|
||||||
|
cmp comparator
|
||||||
|
cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSortResults(cfg *config.Config) *SortResults {
|
||||||
|
// For sorting we are comparing (in next order): file names, line numbers,
|
||||||
|
// position, and finally - giving up.
|
||||||
|
return &SortResults{
|
||||||
|
cmp: ByName{
|
||||||
|
next: ByLine{
|
||||||
|
next: ByColumn{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process is performing sorting of the result issues.
|
||||||
|
func (sr SortResults) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||||
|
if !sr.cfg.Output.SortResults {
|
||||||
|
return issues, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(issues, func(i, j int) bool {
|
||||||
|
return sr.cmp.Compare(&issues[i], &issues[j]) == Less
|
||||||
|
})
|
||||||
|
|
||||||
|
return issues, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr SortResults) Name() string { return "sort_results" }
|
||||||
|
func (sr SortResults) Finish() {}
|
||||||
|
|
||||||
|
type compareResult int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Less compareResult = iota - 1
|
||||||
|
Equal
|
||||||
|
Greater
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c compareResult) isNeutral() bool {
|
||||||
|
// return true if compare result is incomparable or equal.
|
||||||
|
return c == None || c == Equal
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:exhaustive
|
||||||
|
func (c compareResult) String() string {
|
||||||
|
switch c {
|
||||||
|
case Less:
|
||||||
|
return "Less"
|
||||||
|
case Equal:
|
||||||
|
return "Equal"
|
||||||
|
case Greater:
|
||||||
|
return "Greater"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "None"
|
||||||
|
}
|
||||||
|
|
||||||
|
// comparator describe how to implement compare for two "issues" lexicographically
|
||||||
|
type comparator interface {
|
||||||
|
Compare(a, b *result.Issue) compareResult
|
||||||
|
Next() comparator
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ comparator = (*ByName)(nil)
|
||||||
|
_ comparator = (*ByLine)(nil)
|
||||||
|
_ comparator = (*ByColumn)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type ByName struct{ next comparator }
|
||||||
|
|
||||||
|
//nolint:golint
|
||||||
|
func (cmp ByName) Next() comparator { return cmp.next }
|
||||||
|
|
||||||
|
//nolint:golint
|
||||||
|
func (cmp ByName) Compare(a, b *result.Issue) compareResult {
|
||||||
|
var res compareResult
|
||||||
|
|
||||||
|
if res = compareResult(strings.Compare(a.FilePath(), b.FilePath())); !res.isNeutral() {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
if next := cmp.Next(); next != nil {
|
||||||
|
return next.Compare(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByLine struct{ next comparator }
|
||||||
|
|
||||||
|
//nolint:golint
|
||||||
|
func (cmp ByLine) Next() comparator { return cmp.next }
|
||||||
|
|
||||||
|
//nolint:golint
|
||||||
|
func (cmp ByLine) Compare(a, b *result.Issue) compareResult {
|
||||||
|
var res compareResult
|
||||||
|
|
||||||
|
if res = numericCompare(a.Line(), b.Line()); !res.isNeutral() {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
if next := cmp.Next(); next != nil {
|
||||||
|
return next.Compare(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByColumn struct{ next comparator }
|
||||||
|
|
||||||
|
//nolint:golint
|
||||||
|
func (cmp ByColumn) Next() comparator { return cmp.next }
|
||||||
|
|
||||||
|
//nolint:golint
|
||||||
|
func (cmp ByColumn) Compare(a, b *result.Issue) compareResult {
|
||||||
|
var res compareResult
|
||||||
|
|
||||||
|
if res = numericCompare(a.Column(), b.Column()); !res.isNeutral() {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
if next := cmp.Next(); next != nil {
|
||||||
|
return next.Compare(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func numericCompare(a, b int) compareResult {
|
||||||
|
var (
|
||||||
|
isValuesInvalid = a < 0 || b < 0
|
||||||
|
isZeroValuesBoth = a == 0 && b == 0
|
||||||
|
isEqual = a == b
|
||||||
|
isZeroValueInA = b > 0 && a == 0
|
||||||
|
isZeroValueInB = a > 0 && b == 0
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case isZeroValuesBoth || isEqual:
|
||||||
|
return Equal
|
||||||
|
case isValuesInvalid || isZeroValueInA || isZeroValueInB:
|
||||||
|
return None
|
||||||
|
case a > b:
|
||||||
|
return Greater
|
||||||
|
case a < b:
|
||||||
|
return Less
|
||||||
|
}
|
||||||
|
|
||||||
|
return Equal
|
||||||
|
}
|
182
pkg/result/processors/sort_results_test.go
Normal file
182
pkg/result/processors/sort_results_test.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package processors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/token"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/golangci/golangci-lint/pkg/config"
|
||||||
|
"github.com/golangci/golangci-lint/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
var issues = []result.Issue{
|
||||||
|
{
|
||||||
|
Pos: token.Position{
|
||||||
|
Filename: "file_windows.go",
|
||||||
|
Column: 80,
|
||||||
|
Line: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pos: token.Position{
|
||||||
|
Filename: "file_linux.go",
|
||||||
|
Column: 70,
|
||||||
|
Line: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pos: token.Position{
|
||||||
|
Filename: "file_darwin.go",
|
||||||
|
Line: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pos: token.Position{
|
||||||
|
Filename: "file_darwin.go",
|
||||||
|
Column: 60,
|
||||||
|
Line: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type compareTestCase struct {
|
||||||
|
a, b result.Issue
|
||||||
|
expected compareResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCompareValues(t *testing.T, cmp comparator, name string, tests []compareTestCase) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for i := 0; i < len(tests); i++ {
|
||||||
|
test := tests[i]
|
||||||
|
t.Run(fmt.Sprintf("%s(%d)", name, i), func(t *testing.T) {
|
||||||
|
res := cmp.Compare(&test.a, &test.b)
|
||||||
|
assert.Equal(t, test.expected.String(), res.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareByLine(t *testing.T) {
|
||||||
|
testCompareValues(t, ByLine{}, "Compare By Line", []compareTestCase{
|
||||||
|
{issues[0], issues[1], Less}, // 10 vs 11
|
||||||
|
{issues[0], issues[0], Equal}, // 10 vs 10
|
||||||
|
{issues[3], issues[3], Equal}, // 10 vs 10
|
||||||
|
{issues[0], issues[3], Equal}, // 10 vs 10
|
||||||
|
{issues[3], issues[2], Less}, // 10 vs 12
|
||||||
|
{issues[1], issues[1], Equal}, // 11 vs 11
|
||||||
|
{issues[1], issues[0], Greater}, // 11 vs 10
|
||||||
|
{issues[1], issues[2], Less}, // 11 vs 12
|
||||||
|
{issues[2], issues[3], Greater}, // 12 vs 10
|
||||||
|
{issues[2], issues[1], Greater}, // 12 vs 11
|
||||||
|
{issues[2], issues[2], Equal}, // 12 vs 12
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareByName(t *testing.T) { //nolint:dupl
|
||||||
|
testCompareValues(t, ByName{}, "Compare By Name", []compareTestCase{
|
||||||
|
{issues[0], issues[1], Greater}, // file_windows.go vs file_linux.go
|
||||||
|
{issues[1], issues[2], Greater}, // file_linux.go vs file_darwin.go
|
||||||
|
{issues[2], issues[3], Equal}, // file_darwin.go vs file_darwin.go
|
||||||
|
{issues[1], issues[1], Equal}, // file_linux.go vs file_linux.go
|
||||||
|
{issues[1], issues[0], Less}, // file_linux.go vs file_windows.go
|
||||||
|
{issues[3], issues[2], Equal}, // file_darwin.go vs file_darwin.go
|
||||||
|
{issues[2], issues[1], Less}, // file_darwin.go vs file_linux.go
|
||||||
|
{issues[0], issues[0], Equal}, // file_windows.go vs file_windows.go
|
||||||
|
{issues[2], issues[2], Equal}, // file_darwin.go vs file_darwin.go
|
||||||
|
{issues[3], issues[3], Equal}, // file_darwin.go vs file_darwin.go
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareByColumn(t *testing.T) { //nolint:dupl
|
||||||
|
testCompareValues(t, ByColumn{}, "Compare By Column", []compareTestCase{
|
||||||
|
{issues[0], issues[1], Greater}, // 80 vs 70
|
||||||
|
{issues[1], issues[2], None}, // 70 vs zero value
|
||||||
|
{issues[3], issues[3], Equal}, // 60 vs 60
|
||||||
|
{issues[2], issues[3], None}, // zero value vs 60
|
||||||
|
{issues[2], issues[1], None}, // zero value vs 70
|
||||||
|
{issues[1], issues[0], Less}, // 70 vs 80
|
||||||
|
{issues[1], issues[1], Equal}, // 70 vs 70
|
||||||
|
{issues[3], issues[2], None}, // vs zero value
|
||||||
|
{issues[2], issues[2], Equal}, // zero value vs zero value
|
||||||
|
{issues[1], issues[1], Equal}, // 70 vs 70
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareNested(t *testing.T) {
|
||||||
|
var cmp = ByName{
|
||||||
|
next: ByLine{
|
||||||
|
next: ByColumn{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCompareValues(t, cmp, "Nested Comparing", []compareTestCase{
|
||||||
|
{issues[1], issues[0], Less}, // file_linux.go vs file_windows.go
|
||||||
|
{issues[2], issues[1], Less}, // file_darwin.go vs file_linux.go
|
||||||
|
{issues[0], issues[1], Greater}, // file_windows.go vs file_linux.go
|
||||||
|
{issues[1], issues[2], Greater}, // file_linux.go vs file_darwin.go
|
||||||
|
{issues[3], issues[2], Less}, // file_darwin.go vs file_darwin.go, 10 vs 12
|
||||||
|
{issues[0], issues[0], Equal}, // file_windows.go vs file_windows.go
|
||||||
|
{issues[2], issues[3], Greater}, // file_darwin.go vs file_darwin.go, 12 vs 10
|
||||||
|
{issues[1], issues[1], Equal}, // file_linux.go vs file_linux.go
|
||||||
|
{issues[2], issues[2], Equal}, // file_darwin.go vs file_darwin.go
|
||||||
|
{issues[3], issues[3], Equal}, // file_darwin.go vs file_darwin.go
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumericCompare(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
a, b int
|
||||||
|
expected compareResult
|
||||||
|
}{
|
||||||
|
{0, 0, Equal},
|
||||||
|
{0, 1, None},
|
||||||
|
{1, 0, None},
|
||||||
|
{1, -1, None},
|
||||||
|
{-1, 1, None},
|
||||||
|
{1, 1, Equal},
|
||||||
|
{1, 2, Less},
|
||||||
|
{2, 1, Greater},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for i := 0; i < len(tests); i++ {
|
||||||
|
test := tests[i]
|
||||||
|
t.Run(fmt.Sprintf("%s(%d)", "Numeric Compare", i), func(t *testing.T) {
|
||||||
|
res := numericCompare(test.a, test.b)
|
||||||
|
assert.Equal(t, test.expected.String(), res.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoSorting(t *testing.T) {
|
||||||
|
var tests = make([]result.Issue, len(issues))
|
||||||
|
copy(tests, issues)
|
||||||
|
|
||||||
|
var sr = NewSortResults(&config.Config{})
|
||||||
|
|
||||||
|
results, err := sr.Process(tests)
|
||||||
|
assert.Equal(t, tests, results)
|
||||||
|
assert.Nil(t, err, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSorting(t *testing.T) {
|
||||||
|
var tests = make([]result.Issue, len(issues))
|
||||||
|
copy(tests, issues)
|
||||||
|
|
||||||
|
var expected = make([]result.Issue, len(issues))
|
||||||
|
expected[0] = issues[3]
|
||||||
|
expected[1] = issues[2]
|
||||||
|
expected[2] = issues[1]
|
||||||
|
expected[3] = issues[0]
|
||||||
|
|
||||||
|
var cfg = config.Config{}
|
||||||
|
cfg.Output.SortResults = true
|
||||||
|
var sr = NewSortResults(&cfg)
|
||||||
|
|
||||||
|
results, err := sr.Process(tests)
|
||||||
|
assert.Equal(t, results, expected)
|
||||||
|
assert.Nil(t, err, nil)
|
||||||
|
}
|
@ -126,6 +126,41 @@ func TestLineDirectiveProcessedFilesLiteLoading(t *testing.T) {
|
|||||||
r.ExpectExitCode(exitcodes.IssuesFound).ExpectOutputEq(output + "\n")
|
r.ExpectExitCode(exitcodes.IssuesFound).ExpectOutputEq(output + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSortedResults(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
opt string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"--sort-results=false",
|
||||||
|
strings.Join([]string{
|
||||||
|
"testdata/sort_results/main.go:12:5: `db` is unused (deadcode)",
|
||||||
|
"testdata/sort_results/main.go:15:13: Error return value of `returnError` is not checked (errcheck)",
|
||||||
|
"testdata/sort_results/main.go:8:6: func `returnError` is unused (unused)",
|
||||||
|
}, "\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"--sort-results=true",
|
||||||
|
strings.Join([]string{
|
||||||
|
"testdata/sort_results/main.go:8:6: func `returnError` is unused (unused)",
|
||||||
|
"testdata/sort_results/main.go:12:5: `db` is unused (deadcode)",
|
||||||
|
"testdata/sort_results/main.go:15:13: Error return value of `returnError` is not checked (errcheck)",
|
||||||
|
}, "\n"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := getTestDataDir("sort_results")
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
for i := range testCases {
|
||||||
|
test := testCases[i]
|
||||||
|
t.Run(test.opt, func(t *testing.T) {
|
||||||
|
r := testshared.NewLintRunner(t).Run("--print-issued-lines=false", test.opt, dir)
|
||||||
|
r.ExpectExitCode(exitcodes.IssuesFound).ExpectOutputEq(test.want + "\n")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLineDirectiveProcessedFilesFullLoading(t *testing.T) {
|
func TestLineDirectiveProcessedFilesFullLoading(t *testing.T) {
|
||||||
r := testshared.NewLintRunner(t).Run("--print-issued-lines=false", "--no-config",
|
r := testshared.NewLintRunner(t).Run("--print-issued-lines=false", "--no-config",
|
||||||
"--exclude-use-default=false", "-Egolint,govet", getTestDataDir("quicktemplate"))
|
"--exclude-use-default=false", "-Egolint,govet", getTestDataDir("quicktemplate"))
|
||||||
|
16
test/testdata/sort_results/main.go
vendored
Normal file
16
test/testdata/sort_results/main.go
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package sortresults
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func returnError() error {
|
||||||
|
return errors.New("sss")
|
||||||
|
}
|
||||||
|
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
returnError()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user