Skip to content

Commit d404923

Browse files
committed
review
1 parent 6d8d023 commit d404923

File tree

3 files changed

+90
-133
lines changed

3 files changed

+90
-133
lines changed

pkg/commands/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer,
495495
case config.OutFormatGithubActions:
496496
p = printers.NewGithub(w)
497497
case config.OutFormatTeamCity:
498-
p = printers.NewTeamCity(&e.reportData, w, nil)
498+
p = printers.NewTeamCity(w)
499499
default:
500500
return nil, fmt.Errorf("unknown output format %s", format)
501501
}

pkg/printers/teamcity.go

Lines changed: 83 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,119 @@
11
package printers
22

33
import (
4-
"bytes"
54
"context"
65
"fmt"
76
"io"
8-
"sort"
97
"strings"
10-
"time"
8+
"unicode/utf8"
119

12-
"github.com/golangci/golangci-lint/pkg/report"
1310
"github.com/golangci/golangci-lint/pkg/result"
1411
)
1512

13+
// Field limits.
1614
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
2317
)
2418

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
2823
}
2924

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+
}
3239
}
3340

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{}{}
3743

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+
}
4455

45-
type now func() string
56+
_, err := inspectionType.Print(p.w, p.escaper)
57+
if err != nil {
58+
return err
59+
}
4660

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{}{}
5762
}
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,
6270
}
63-
}
64-
return t
65-
}
6671

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+
}
7176
}
72-
sort.Strings(names)
73-
return names
77+
78+
return nil
7479
}
7580

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.
10888
}
10989

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))
11693
}
11794

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+
}
121105

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+
}
126110

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
129116
}
130117

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]
151119
}

pkg/printers/teamcity_test.go

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/stretchr/testify/assert"
1010
"github.com/stretchr/testify/require"
1111

12-
"github.com/golangci/golangci-lint/pkg/report"
1312
"github.com/golangci/golangci-lint/pkg/result"
1413
)
1514

@@ -55,26 +54,16 @@ func TestTeamCity_Print(t *testing.T) {
5554
}
5655

5756
buf := new(bytes.Buffer)
58-
rd := &report.Data{
59-
Linters: []report.LinterData{
60-
{Name: "linter-a", Enabled: true},
61-
{Name: "linter-b", Enabled: false},
62-
},
63-
}
64-
nower := func() string {
65-
return "2023-02-17T15:42:23.630"
66-
}
67-
printer := NewTeamCity(rd, buf, nower)
57+
printer := NewTeamCity(buf)
6858

6959
err := printer.Print(context.Background(), issues)
7060
require.NoError(t, err)
7161

72-
expected := `##teamcity[testStarted timestamp='2023-02-17T15:42:23.630' name='linter: linter-a']
73-
##teamcity[testStdErr timestamp='2023-02-17T15:42:23.630' name='linter: linter-a' out='path/to/filea.go:10:4 - some issue']
74-
##teamcity[testStdErr timestamp='2023-02-17T15:42:23.630' name='linter: linter-a' out='path/to/filea.go:10 - some issue 2']
75-
##teamcity[testFailed timestamp='2023-02-17T15:42:23.630' name='linter: linter-a']
76-
##teamcity[testStarted timestamp='2023-02-17T15:42:23.630' name='linter: linter-b']
77-
##teamcity[testIgnored timestamp='2023-02-17T15:42:23.630' name='linter: linter-b']
62+
expected := `##teamcity[inspectionType id='linter-a' name='linter-a' description='linter-a' category='Golangci-lint reports']
63+
##teamcity[inspection typeId='linter-a' message='some issue' file='path/to/filea.go' line='10' additional attribute='error']
64+
##teamcity[inspection typeId='linter-a' message='some issue 2' file='path/to/filea.go' line='10' additional attribute='error']
65+
##teamcity[inspectionType id='linter-b' name='linter-b' description='linter-b' category='Golangci-lint reports']
66+
##teamcity[inspection typeId='linter-b' message='another issue' file='path/to/fileb.go' line='300' additional attribute='error']
7867
`
7968

8069
assert.Equal(t, expected, buf.String())

0 commit comments

Comments
 (0)