|
1 | 1 | package printers
|
2 | 2 |
|
3 | 3 | import (
|
4 |
| - "bytes" |
5 | 4 | "context"
|
6 | 5 | "fmt"
|
7 | 6 | "io"
|
8 |
| - "sort" |
9 | 7 | "strings"
|
10 |
| - "time" |
| 8 | + "unicode/utf8" |
11 | 9 |
|
12 |
| - "github.com/golangci/golangci-lint/pkg/report" |
13 | 10 | "github.com/golangci/golangci-lint/pkg/result"
|
14 | 11 | )
|
15 | 12 |
|
| 13 | +// Field limits. |
16 | 14 | const (
|
17 |
| - timestampFormat = "2006-01-02T15:04:05.000" |
18 |
| - testStarted = "##teamcity[testStarted timestamp='%s' name='%s']\n" |
19 |
| - testStdErr = "##teamcity[testStdErr timestamp='%s' name='%s' out='%s']\n" |
20 |
| - testFailed = "##teamcity[testFailed timestamp='%s' name='%s']\n" |
21 |
| - testIgnored = "##teamcity[testIgnored timestamp='%s' name='%s']\n" |
22 |
| - testFinished = "##teamcity[testFinished timestamp='%s' name='%s']\n" |
| 15 | + smallLimit = 255 |
| 16 | + largeLimit = 4000 |
23 | 17 | )
|
24 | 18 |
|
25 |
| -type teamcityLinter struct { |
26 |
| - data *report.LinterData |
27 |
| - issues []string |
| 19 | +// TeamCity printer for TeamCity format. |
| 20 | +type TeamCity struct { |
| 21 | + w io.Writer |
| 22 | + escaper *strings.Replacer |
28 | 23 | }
|
29 | 24 |
|
30 |
| -func (l *teamcityLinter) getName() string { |
31 |
| - return fmt.Sprintf("linter: %s", l.data.Name) |
| 25 | +// NewTeamCity output format outputs issues according to TeamCity service message format |
| 26 | +func NewTeamCity(w io.Writer) *TeamCity { |
| 27 | + return &TeamCity{ |
| 28 | + w: w, |
| 29 | + // https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+Values |
| 30 | + escaper: strings.NewReplacer( |
| 31 | + "'", "|'", |
| 32 | + "\n", "|n", |
| 33 | + "\r", "|r", |
| 34 | + "|", "||", |
| 35 | + "[", "|[", |
| 36 | + "]", "|]", |
| 37 | + ), |
| 38 | + } |
32 | 39 | }
|
33 | 40 |
|
34 |
| -func (l *teamcityLinter) failed() bool { |
35 |
| - return len(l.issues) > 0 |
36 |
| -} |
| 41 | +func (p *TeamCity) Print(_ context.Context, issues []result.Issue) error { |
| 42 | + uniqLinters := map[string]struct{}{} |
37 | 43 |
|
38 |
| -type teamcity struct { |
39 |
| - linters map[string]*teamcityLinter |
40 |
| - w io.Writer |
41 |
| - err error |
42 |
| - now now |
43 |
| -} |
| 44 | + for i := range issues { |
| 45 | + issue := issues[i] |
| 46 | + |
| 47 | + _, ok := uniqLinters[issue.FromLinter] |
| 48 | + if !ok { |
| 49 | + inspectionType := InspectionType{ |
| 50 | + id: issue.FromLinter, |
| 51 | + name: issue.FromLinter, |
| 52 | + description: issue.FromLinter, |
| 53 | + category: "Golangci-lint reports", |
| 54 | + } |
44 | 55 |
|
45 |
| -type now func() string |
| 56 | + _, err := inspectionType.Print(p.w, p.escaper) |
| 57 | + if err != nil { |
| 58 | + return err |
| 59 | + } |
46 | 60 |
|
47 |
| -// NewTeamCity output format outputs issues according to TeamCity service message format |
48 |
| -func NewTeamCity(rd *report.Data, w io.Writer, nower now) Printer { |
49 |
| - t := &teamcity{ |
50 |
| - linters: map[string]*teamcityLinter{}, |
51 |
| - w: w, |
52 |
| - now: nower, |
53 |
| - } |
54 |
| - if t.now == nil { |
55 |
| - t.now = func() string { |
56 |
| - return time.Now().Format(timestampFormat) |
| 61 | + uniqLinters[issue.FromLinter] = struct{}{} |
57 | 62 | }
|
58 |
| - } |
59 |
| - for i, l := range rd.Linters { |
60 |
| - t.linters[l.Name] = &teamcityLinter{ |
61 |
| - data: &rd.Linters[i], |
| 63 | + |
| 64 | + instance := InspectionInstance{ |
| 65 | + typeID: issue.FromLinter, |
| 66 | + message: issue.Text, |
| 67 | + file: issue.FilePath(), |
| 68 | + line: issue.Line(), |
| 69 | + additionalAttribute: issue.Severity, |
62 | 70 | }
|
63 |
| - } |
64 |
| - return t |
65 |
| -} |
66 | 71 |
|
67 |
| -func (p *teamcity) getSortedLinterNames() []string { |
68 |
| - names := make([]string, 0, len(p.linters)) |
69 |
| - for name := range p.linters { |
70 |
| - names = append(names, name) |
| 72 | + _, err := instance.Print(p.w, p.escaper) |
| 73 | + if err != nil { |
| 74 | + return err |
| 75 | + } |
71 | 76 | }
|
72 |
| - sort.Strings(names) |
73 |
| - return names |
| 77 | + |
| 78 | + return nil |
74 | 79 | }
|
75 | 80 |
|
76 |
| -// escape transforms strings for TeamCity service messages |
77 |
| -// https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+values |
78 |
| -func (p *teamcity) escape(s string) string { |
79 |
| - var buf bytes.Buffer |
80 |
| - for { |
81 |
| - nextSpecial := strings.IndexAny(s, "'\n\r|[]") |
82 |
| - switch nextSpecial { |
83 |
| - case -1: |
84 |
| - if buf.Len() == 0 { |
85 |
| - return s |
86 |
| - } |
87 |
| - return buf.String() + s |
88 |
| - case 0: |
89 |
| - default: |
90 |
| - buf.WriteString(s[:nextSpecial]) |
91 |
| - } |
92 |
| - switch s[nextSpecial] { |
93 |
| - case '\'': |
94 |
| - buf.WriteString("|'") |
95 |
| - case '\n': |
96 |
| - buf.WriteString("|n") |
97 |
| - case '\r': |
98 |
| - buf.WriteString("|r") |
99 |
| - case '|': |
100 |
| - buf.WriteString("||") |
101 |
| - case '[': |
102 |
| - buf.WriteString("|[") |
103 |
| - case ']': |
104 |
| - buf.WriteString("|]") |
105 |
| - } |
106 |
| - s = s[nextSpecial+1:] |
107 |
| - } |
| 81 | +// InspectionType Each specific warning or an error in code (inspection instance) has an inspection type. |
| 82 | +// https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Type |
| 83 | +type InspectionType struct { |
| 84 | + id string // (mandatory) limited by 255 characters. |
| 85 | + name string // (mandatory) limited by 255 characters. |
| 86 | + description string // (mandatory) limited by 255 characters. |
| 87 | + category string // (mandatory) limited by 4000 characters. |
108 | 88 | }
|
109 | 89 |
|
110 |
| -func (p *teamcity) print(format string, args ...any) { |
111 |
| - if p.err != nil { |
112 |
| - return |
113 |
| - } |
114 |
| - args = append([]any{p.now()}, args...) |
115 |
| - _, p.err = fmt.Fprintf(p.w, format, args...) |
| 90 | +func (i InspectionType) Print(w io.Writer, escaper *strings.Replacer) (int, error) { |
| 91 | + return fmt.Fprintf(w, "##teamcity[inspectionType id='%s' name='%s' description='%s' category='%s']\n", |
| 92 | + limit(i.id, smallLimit), limit(i.name, smallLimit), limit(escaper.Replace(i.description), largeLimit), limit(i.category, smallLimit)) |
116 | 93 | }
|
117 | 94 |
|
118 |
| -func (p *teamcity) Print(_ context.Context, issues []result.Issue) error { |
119 |
| - for i := range issues { |
120 |
| - issue := &issues[i] |
| 95 | +// InspectionInstance Reports a specific defect, warning, error message. |
| 96 | +// Includes location, description, and various optional and custom attributes. |
| 97 | +// https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance |
| 98 | +type InspectionInstance struct { |
| 99 | + typeID string // (mandatory) limited by 255 characters. |
| 100 | + message string // (optional) limited by 4000 characters. |
| 101 | + file string // (mandatory) file path limited by 4000 characters. |
| 102 | + line int // (optional) line of the file, integer. |
| 103 | + additionalAttribute string |
| 104 | +} |
121 | 105 |
|
122 |
| - var col string |
123 |
| - if issue.Pos.Column != 0 { |
124 |
| - col = fmt.Sprintf(":%d", issue.Pos.Column) |
125 |
| - } |
| 106 | +func (i InspectionInstance) Print(w io.Writer, replacer *strings.Replacer) (int, error) { |
| 107 | + return fmt.Fprintf(w, "##teamcity[inspection typeId='%s' message='%s' file='%s' line='%d' additional attribute='%s']\n", |
| 108 | + limit(i.typeID, smallLimit), limit(replacer.Replace(i.message), largeLimit), limit(i.file, largeLimit), i.line, i.additionalAttribute) |
| 109 | +} |
126 | 110 |
|
127 |
| - formatted := fmt.Sprintf("%s:%v%s - %s", issue.FilePath(), issue.Line(), col, issue.Text) |
128 |
| - p.linters[issue.FromLinter].issues = append(p.linters[issue.FromLinter].issues, formatted) |
| 111 | +func limit(s string, max int) string { |
| 112 | + var size, count int |
| 113 | + for i := 0; i < max && count < len(s); i++ { |
| 114 | + _, size = utf8.DecodeRuneInString(s[count:]) |
| 115 | + count += size |
129 | 116 | }
|
130 | 117 |
|
131 |
| - for _, linterName := range p.getSortedLinterNames() { |
132 |
| - linter := p.linters[linterName] |
133 |
| - |
134 |
| - name := p.escape(linter.getName()) |
135 |
| - p.print(testStarted, name) |
136 |
| - if !linter.data.Enabled && !linter.data.EnabledByDefault { |
137 |
| - p.print(testIgnored, name) |
138 |
| - continue |
139 |
| - } |
140 |
| - |
141 |
| - if linter.failed() { |
142 |
| - for _, issue := range linter.issues { |
143 |
| - p.print(testStdErr, name, p.escape(issue)) |
144 |
| - } |
145 |
| - p.print(testFailed, name) |
146 |
| - } else { |
147 |
| - p.print(testFinished, name) |
148 |
| - } |
149 |
| - } |
150 |
| - return p.err |
| 118 | + return s[:count] |
151 | 119 | }
|
0 commit comments