github.com/rliebz/gometalinter@v2.0.2-0.20171206234108-d5a071029e07+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 }