Skip to content

Commit 4a185da

Browse files
ferhatelmasldezalexandear
authored andcommitted
feat: add TeamCity output format (golangci#3606)
Co-authored-by: Fernandez Ludovic <[email protected]> Co-authored-by: Oleksandr Redko <[email protected]>
1 parent 485dc40 commit 4a185da

File tree

5 files changed

+234
-0
lines changed

5 files changed

+234
-0
lines changed

.golangci.reference.yml

+1
Original file line numberDiff line numberDiff line change
@@ -2364,6 +2364,7 @@ severity:
23642364
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
23652365
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel
23662366
# - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
2367+
# - TeamCity: https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance
23672368
#
23682369
# Default value is an empty string.
23692370
default-severity: error

pkg/commands/run.go

+2
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer,
494494
p = printers.NewJunitXML(w)
495495
case config.OutFormatGithubActions:
496496
p = printers.NewGithub(w)
497+
case config.OutFormatTeamCity:
498+
p = printers.NewTeamCity(w)
497499
default:
498500
return nil, fmt.Errorf("unknown output format %s", format)
499501
}

pkg/config/output.go

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const (
1010
OutFormatHTML = "html"
1111
OutFormatJunitXML = "junit-xml"
1212
OutFormatGithubActions = "github-actions"
13+
OutFormatTeamCity = "teamcity"
1314
)
1415

1516
var OutFormats = []string{
@@ -22,6 +23,7 @@ var OutFormats = []string{
2223
OutFormatHTML,
2324
OutFormatJunitXML,
2425
OutFormatGithubActions,
26+
OutFormatTeamCity,
2527
}
2628

2729
type Output struct {

pkg/printers/teamcity.go

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package printers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"strings"
8+
"unicode/utf8"
9+
10+
"github.com/golangci/golangci-lint/pkg/result"
11+
)
12+
13+
// Field limits.
14+
const (
15+
smallLimit = 255
16+
largeLimit = 4000
17+
)
18+
19+
// TeamCity printer for TeamCity format.
20+
type TeamCity struct {
21+
w io.Writer
22+
escaper *strings.Replacer
23+
}
24+
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+
}
39+
}
40+
41+
func (p *TeamCity) Print(_ context.Context, issues []result.Issue) error {
42+
uniqLinters := map[string]struct{}{}
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+
}
55+
56+
_, err := inspectionType.Print(p.w, p.escaper)
57+
if err != nil {
58+
return err
59+
}
60+
61+
uniqLinters[issue.FromLinter] = struct{}{}
62+
}
63+
64+
instance := InspectionInstance{
65+
typeID: issue.FromLinter,
66+
message: issue.Text,
67+
file: issue.FilePath(),
68+
line: issue.Line(),
69+
severity: issue.Severity,
70+
}
71+
72+
_, err := instance.Print(p.w, p.escaper)
73+
if err != nil {
74+
return err
75+
}
76+
}
77+
78+
return nil
79+
}
80+
81+
// InspectionType is the unique description of the conducted inspection. Each specific warning or
82+
// an error in code (inspection instance) has an inspection type.
83+
// https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Type
84+
type InspectionType struct {
85+
id string // (mandatory) limited by 255 characters.
86+
name string // (mandatory) limited by 255 characters.
87+
description string // (mandatory) limited by 255 characters.
88+
category string // (mandatory) limited by 4000 characters.
89+
}
90+
91+
func (i InspectionType) Print(w io.Writer, escaper *strings.Replacer) (int, error) {
92+
return fmt.Fprintf(w, "##teamcity[InspectionType id='%s' name='%s' description='%s' category='%s']\n",
93+
limit(i.id, smallLimit), limit(i.name, smallLimit), limit(escaper.Replace(i.description), largeLimit), limit(i.category, smallLimit))
94+
}
95+
96+
// InspectionInstance reports a specific defect, warning, error message.
97+
// Includes location, description, and various optional and custom attributes.
98+
// https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance
99+
type InspectionInstance struct {
100+
typeID string // (mandatory) limited by 255 characters.
101+
message string // (optional) limited by 4000 characters.
102+
file string // (mandatory) file path limited by 4000 characters.
103+
line int // (optional) line of the file.
104+
severity string // (optional) any linter severity.
105+
}
106+
107+
func (i InspectionInstance) Print(w io.Writer, replacer *strings.Replacer) (int, error) {
108+
return fmt.Fprintf(w, "##teamcity[inspection typeId='%s' message='%s' file='%s' line='%d' SEVERITY='%s']\n",
109+
limit(i.typeID, smallLimit),
110+
limit(replacer.Replace(i.message), largeLimit),
111+
limit(i.file, largeLimit),
112+
i.line, strings.ToUpper(i.severity))
113+
}
114+
115+
func limit(s string, max int) string {
116+
var size, count int
117+
for i := 0; i < max && count < len(s); i++ {
118+
_, size = utf8.DecodeRuneInString(s[count:])
119+
count += size
120+
}
121+
122+
return s[:count]
123+
}

pkg/printers/teamcity_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package printers
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"go/token"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/golangci/golangci-lint/pkg/result"
13+
)
14+
15+
func TestTeamCity_Print(t *testing.T) {
16+
issues := []result.Issue{
17+
{
18+
FromLinter: "linter-a",
19+
Text: "warning issue",
20+
Pos: token.Position{
21+
Filename: "path/to/filea.go",
22+
Offset: 2,
23+
Line: 10,
24+
Column: 4,
25+
},
26+
},
27+
{
28+
FromLinter: "linter-a",
29+
Severity: "error",
30+
Text: "error issue",
31+
Pos: token.Position{
32+
Filename: "path/to/filea.go",
33+
Offset: 2,
34+
Line: 10,
35+
},
36+
},
37+
{
38+
FromLinter: "linter-b",
39+
Text: "info issue",
40+
SourceLines: []string{
41+
"func foo() {",
42+
"\tfmt.Println(\"bar\")",
43+
"}",
44+
},
45+
Pos: token.Position{
46+
Filename: "path/to/fileb.go",
47+
Offset: 5,
48+
Line: 300,
49+
Column: 9,
50+
},
51+
},
52+
}
53+
54+
buf := new(bytes.Buffer)
55+
printer := NewTeamCity(buf)
56+
57+
err := printer.Print(context.Background(), issues)
58+
require.NoError(t, err)
59+
60+
expected := `##teamcity[InspectionType id='linter-a' name='linter-a' description='linter-a' category='Golangci-lint reports']
61+
##teamcity[inspection typeId='linter-a' message='warning issue' file='path/to/filea.go' line='10' SEVERITY='']
62+
##teamcity[inspection typeId='linter-a' message='error issue' file='path/to/filea.go' line='10' SEVERITY='ERROR']
63+
##teamcity[InspectionType id='linter-b' name='linter-b' description='linter-b' category='Golangci-lint reports']
64+
##teamcity[inspection typeId='linter-b' message='info issue' file='path/to/fileb.go' line='300' SEVERITY='']
65+
`
66+
67+
assert.Equal(t, expected, buf.String())
68+
}
69+
70+
func TestTeamCity_limit(t *testing.T) {
71+
tests := []struct {
72+
input string
73+
max int
74+
expected string
75+
}{
76+
{
77+
input: "golangci-lint",
78+
max: 0,
79+
expected: "",
80+
},
81+
{
82+
input: "golangci-lint",
83+
max: 8,
84+
expected: "golangci",
85+
},
86+
{
87+
input: "golangci-lint",
88+
max: 13,
89+
expected: "golangci-lint",
90+
},
91+
{
92+
input: "golangci-lint",
93+
max: 15,
94+
expected: "golangci-lint",
95+
},
96+
{
97+
input: "こんにちは",
98+
max: 3,
99+
expected: "こんに",
100+
},
101+
}
102+
103+
for _, tc := range tests {
104+
require.Equal(t, tc.expected, limit(tc.input, tc.max))
105+
}
106+
}

0 commit comments

Comments
 (0)