github.com/dvyukov/gometalinter@v2.0.12-0.20181028185006-9777a28a8438+incompatible/issue.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  	"text/template"
    12  )
    13  
    14  // DefaultIssueFormat used to print an issue
    15  const DefaultIssueFormat = "{{.Path}}:{{.Line}}:{{if .Col}}{{.Col}}{{end}}:{{.Severity}}: {{.Message}} ({{.Linter}})"
    16  
    17  // Severity of linter message
    18  type Severity string
    19  
    20  // Linter message severity levels.
    21  const (
    22  	Error   Severity = "error"
    23  	Warning Severity = "warning"
    24  )
    25  
    26  type IssuePath struct {
    27  	root string
    28  	path string
    29  }
    30  
    31  func (i IssuePath) String() string {
    32  	return i.Relative()
    33  }
    34  
    35  func (i IssuePath) Relative() string {
    36  	return i.path
    37  }
    38  
    39  func (i IssuePath) Abs() string {
    40  	return filepath.Join(i.root, i.path)
    41  }
    42  
    43  func (i IssuePath) MarshalJSON() ([]byte, error) {
    44  	return json.Marshal(i.String())
    45  }
    46  
    47  func newIssuePath(root, path string) IssuePath {
    48  	return IssuePath{root: root, path: path}
    49  }
    50  
    51  // newIssuePathFromAbsPath returns a new issuePath from a path that may be
    52  // an absolute path. root must be an absolute path.
    53  func newIssuePathFromAbsPath(root, path string) (IssuePath, error) {
    54  	resolvedRoot, err := filepath.EvalSymlinks(root)
    55  	if err != nil {
    56  		return newIssuePath(root, path), err
    57  	}
    58  
    59  	resolvedPath, err := filepath.EvalSymlinks(path)
    60  	if err != nil {
    61  		return newIssuePath(root, path), err
    62  	}
    63  
    64  	if !filepath.IsAbs(path) {
    65  		return newIssuePath(resolvedRoot, resolvedPath), nil
    66  	}
    67  
    68  	relPath, err := filepath.Rel(resolvedRoot, resolvedPath)
    69  	return newIssuePath(resolvedRoot, relPath), err
    70  }
    71  
    72  type Issue struct {
    73  	Linter     string    `json:"linter"`
    74  	Severity   Severity  `json:"severity"`
    75  	Path       IssuePath `json:"path"`
    76  	Line       int       `json:"line"`
    77  	Col        int       `json:"col"`
    78  	Message    string    `json:"message"`
    79  	formatTmpl *template.Template
    80  }
    81  
    82  // NewIssue returns a new issue. Returns an error if formatTmpl is not a valid
    83  // template for an Issue.
    84  func NewIssue(linter string, formatTmpl *template.Template) (*Issue, error) {
    85  	issue := &Issue{
    86  		Line:       1,
    87  		Severity:   Warning,
    88  		Linter:     linter,
    89  		formatTmpl: formatTmpl,
    90  	}
    91  	err := formatTmpl.Execute(ioutil.Discard, issue)
    92  	return issue, err
    93  }
    94  
    95  func (i *Issue) String() string {
    96  	if i.formatTmpl == nil {
    97  		col := ""
    98  		if i.Col != 0 {
    99  			col = fmt.Sprintf("%d", i.Col)
   100  		}
   101  		return fmt.Sprintf("%s:%d:%s:%s: %s (%s)",
   102  			strings.TrimSpace(i.Path.Relative()),
   103  			i.Line, col, i.Severity,
   104  			strings.TrimSpace(i.Message),
   105  			i.Linter)
   106  	}
   107  	buf := new(bytes.Buffer)
   108  	_ = i.formatTmpl.Execute(buf, i)
   109  	return buf.String()
   110  }
   111  
   112  type sortedIssues struct {
   113  	issues []*Issue
   114  	order  []string
   115  }
   116  
   117  func (s *sortedIssues) Len() int      { return len(s.issues) }
   118  func (s *sortedIssues) Swap(i, j int) { s.issues[i], s.issues[j] = s.issues[j], s.issues[i] }
   119  
   120  func (s *sortedIssues) Less(i, j int) bool {
   121  	l, r := s.issues[i], s.issues[j]
   122  	return CompareIssue(*l, *r, s.order)
   123  }
   124  
   125  // CompareIssue two Issues and return true if left should sort before right
   126  // nolint: gocyclo
   127  func CompareIssue(l, r Issue, order []string) bool {
   128  	for _, key := range order {
   129  		switch {
   130  		case key == "path" && l.Path != r.Path:
   131  			return l.Path.String() < r.Path.String()
   132  		case key == "line" && l.Line != r.Line:
   133  			return l.Line < r.Line
   134  		case key == "column" && l.Col != r.Col:
   135  			return l.Col < r.Col
   136  		case key == "severity" && l.Severity != r.Severity:
   137  			return l.Severity < r.Severity
   138  		case key == "message" && l.Message != r.Message:
   139  			return l.Message < r.Message
   140  		case key == "linter" && l.Linter != r.Linter:
   141  			return l.Linter < r.Linter
   142  		}
   143  	}
   144  	return true
   145  }
   146  
   147  // SortIssueChan reads issues from one channel, sorts them, and returns them to another
   148  // channel
   149  func SortIssueChan(issues chan *Issue, order []string) chan *Issue {
   150  	out := make(chan *Issue, 1000000)
   151  	sorted := &sortedIssues{
   152  		issues: []*Issue{},
   153  		order:  order,
   154  	}
   155  	go func() {
   156  		for issue := range issues {
   157  			sorted.issues = append(sorted.issues, issue)
   158  		}
   159  		sort.Sort(sorted)
   160  		for _, issue := range sorted.issues {
   161  			out <- issue
   162  		}
   163  		close(out)
   164  	}()
   165  	return out
   166  }