259 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package processors
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"slices"
 | |
| 	"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're achieving this
 | |
| // by sorting results.Issues using processor step, and chain based
 | |
| // rules that can compare different properties of the Issues struct.
 | |
| 
 | |
| const (
 | |
| 	orderNameFile     = "file"
 | |
| 	orderNameLinter   = "linter"
 | |
| 	orderNameSeverity = "severity"
 | |
| )
 | |
| 
 | |
| var _ Processor = (*SortResults)(nil)
 | |
| 
 | |
| type SortResults struct {
 | |
| 	cmps map[string]*comparator
 | |
| 
 | |
| 	cfg *config.Output
 | |
| }
 | |
| 
 | |
| func NewSortResults(cfg *config.Config) *SortResults {
 | |
| 	return &SortResults{
 | |
| 		cmps: map[string]*comparator{
 | |
| 			// For sorting we are comparing (in next order):
 | |
| 			// file names, line numbers, position, and finally - giving up.
 | |
| 			orderNameFile: byFileName().SetNext(byLine().SetNext(byColumn())),
 | |
| 			// For sorting we are comparing: linter name
 | |
| 			orderNameLinter: byLinter(),
 | |
| 			// For sorting we are comparing: severity
 | |
| 			orderNameSeverity: bySeverity(),
 | |
| 		},
 | |
| 		cfg: &cfg.Output,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Process is performing sorting of the result issues.
 | |
| func (sr SortResults) Process(issues []result.Issue) ([]result.Issue, error) {
 | |
| 	if !sr.cfg.SortResults {
 | |
| 		return issues, nil
 | |
| 	}
 | |
| 
 | |
| 	if len(sr.cfg.SortOrder) == 0 {
 | |
| 		sr.cfg.SortOrder = []string{orderNameFile}
 | |
| 	}
 | |
| 
 | |
| 	var cmps []*comparator
 | |
| 	for _, name := range sr.cfg.SortOrder {
 | |
| 		if c, ok := sr.cmps[name]; ok {
 | |
| 			cmps = append(cmps, c)
 | |
| 		} else {
 | |
| 			return nil, fmt.Errorf("unsupported sort-order name %q", name)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	cmp, err := mergeComparators(cmps)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	sort.Slice(issues, func(i, j int) bool {
 | |
| 		return 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
 | |
| }
 | |
| 
 | |
| func (c compareResult) String() string {
 | |
| 	switch c {
 | |
| 	case less:
 | |
| 		return "less"
 | |
| 	case equal:
 | |
| 		return "equal"
 | |
| 	case greater:
 | |
| 		return "greater"
 | |
| 	default:
 | |
| 		return "none"
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // comparator describes how to implement compare for two "issues".
 | |
| type comparator struct {
 | |
| 	name    string
 | |
| 	compare func(a, b *result.Issue) compareResult
 | |
| 	next    *comparator
 | |
| }
 | |
| 
 | |
| func (cmp *comparator) Next() *comparator { return cmp.next }
 | |
| 
 | |
| func (cmp *comparator) SetNext(c *comparator) *comparator {
 | |
| 	cmp.next = c
 | |
| 	return cmp
 | |
| }
 | |
| 
 | |
| func (cmp *comparator) String() string {
 | |
| 	s := cmp.name
 | |
| 	if cmp.Next() != nil {
 | |
| 		s += " > " + cmp.Next().String()
 | |
| 	}
 | |
| 
 | |
| 	return s
 | |
| }
 | |
| 
 | |
| func (cmp *comparator) Compare(a, b *result.Issue) compareResult {
 | |
| 	res := cmp.compare(a, b)
 | |
| 	if !res.isNeutral() {
 | |
| 		return res
 | |
| 	}
 | |
| 
 | |
| 	if next := cmp.Next(); next != nil {
 | |
| 		return next.Compare(a, b)
 | |
| 	}
 | |
| 
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| func byFileName() *comparator {
 | |
| 	return &comparator{
 | |
| 		name: "byFileName",
 | |
| 		compare: func(a, b *result.Issue) compareResult {
 | |
| 			return compareResult(strings.Compare(a.FilePath(), b.FilePath()))
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func byLine() *comparator {
 | |
| 	return &comparator{
 | |
| 		name: "byLine",
 | |
| 		compare: func(a, b *result.Issue) compareResult {
 | |
| 			return numericCompare(a.Line(), b.Line())
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func byColumn() *comparator {
 | |
| 	return &comparator{
 | |
| 		name: "byColumn",
 | |
| 		compare: func(a, b *result.Issue) compareResult {
 | |
| 			return numericCompare(a.Column(), b.Column())
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func byLinter() *comparator {
 | |
| 	return &comparator{
 | |
| 		name: "byLinter",
 | |
| 		compare: func(a, b *result.Issue) compareResult {
 | |
| 			return compareResult(strings.Compare(a.FromLinter, b.FromLinter))
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func bySeverity() *comparator {
 | |
| 	return &comparator{
 | |
| 		name: "bySeverity",
 | |
| 		compare: func(a, b *result.Issue) compareResult {
 | |
| 			return severityCompare(a.Severity, b.Severity)
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func mergeComparators(cmps []*comparator) (*comparator, error) {
 | |
| 	if len(cmps) == 0 {
 | |
| 		return nil, errors.New("no comparator")
 | |
| 	}
 | |
| 
 | |
| 	for i := 0; i < len(cmps)-1; i++ {
 | |
| 		findComparatorTip(cmps[i]).SetNext(cmps[i+1])
 | |
| 	}
 | |
| 
 | |
| 	return cmps[0], nil
 | |
| }
 | |
| 
 | |
| func findComparatorTip(cmp *comparator) *comparator {
 | |
| 	if cmp.Next() != nil {
 | |
| 		return findComparatorTip(cmp.Next())
 | |
| 	}
 | |
| 
 | |
| 	return cmp
 | |
| }
 | |
| 
 | |
| func severityCompare(a, b string) compareResult {
 | |
| 	// The position inside the slice define the importance (lower to higher).
 | |
| 	classic := []string{"low", "medium", "high", "warning", "error"}
 | |
| 
 | |
| 	if slices.Contains(classic, a) && slices.Contains(classic, b) {
 | |
| 		switch {
 | |
| 		case slices.Index(classic, a) > slices.Index(classic, b):
 | |
| 			return greater
 | |
| 		case slices.Index(classic, a) < slices.Index(classic, b):
 | |
| 			return less
 | |
| 		default:
 | |
| 			return equal
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if slices.Contains(classic, a) {
 | |
| 		return greater
 | |
| 	}
 | |
| 
 | |
| 	if slices.Contains(classic, b) {
 | |
| 		return less
 | |
| 	}
 | |
| 
 | |
| 	return compareResult(strings.Compare(a, b))
 | |
| }
 | |
| 
 | |
| 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
 | |
| }
 | 
