-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add TeamCity output format (#3606)
Co-authored-by: Fernandez Ludovic <[email protected]> Co-authored-by: Oleksandr Redko <[email protected]>
- Loading branch information
1 parent
998329d
commit 075691c
Showing
5 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package printers | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"strings" | ||
"unicode/utf8" | ||
|
||
"github.com/golangci/golangci-lint/pkg/result" | ||
) | ||
|
||
// Field limits. | ||
const ( | ||
smallLimit = 255 | ||
largeLimit = 4000 | ||
) | ||
|
||
// TeamCity printer for TeamCity format. | ||
type TeamCity struct { | ||
w io.Writer | ||
escaper *strings.Replacer | ||
} | ||
|
||
// NewTeamCity output format outputs issues according to TeamCity service message format. | ||
func NewTeamCity(w io.Writer) *TeamCity { | ||
return &TeamCity{ | ||
w: w, | ||
// https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+Values | ||
escaper: strings.NewReplacer( | ||
"'", "|'", | ||
"\n", "|n", | ||
"\r", "|r", | ||
"|", "||", | ||
"[", "|[", | ||
"]", "|]", | ||
), | ||
} | ||
} | ||
|
||
func (p *TeamCity) Print(_ context.Context, issues []result.Issue) error { | ||
uniqLinters := map[string]struct{}{} | ||
|
||
for i := range issues { | ||
issue := issues[i] | ||
|
||
_, ok := uniqLinters[issue.FromLinter] | ||
if !ok { | ||
inspectionType := InspectionType{ | ||
id: issue.FromLinter, | ||
name: issue.FromLinter, | ||
description: issue.FromLinter, | ||
category: "Golangci-lint reports", | ||
} | ||
|
||
_, err := inspectionType.Print(p.w, p.escaper) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
uniqLinters[issue.FromLinter] = struct{}{} | ||
} | ||
|
||
instance := InspectionInstance{ | ||
typeID: issue.FromLinter, | ||
message: issue.Text, | ||
file: issue.FilePath(), | ||
line: issue.Line(), | ||
severity: issue.Severity, | ||
} | ||
|
||
_, err := instance.Print(p.w, p.escaper) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// InspectionType is the unique description of the conducted inspection. Each specific warning or | ||
// an error in code (inspection instance) has an inspection type. | ||
// https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Type | ||
type InspectionType struct { | ||
id string // (mandatory) limited by 255 characters. | ||
name string // (mandatory) limited by 255 characters. | ||
description string // (mandatory) limited by 255 characters. | ||
category string // (mandatory) limited by 4000 characters. | ||
} | ||
|
||
func (i InspectionType) Print(w io.Writer, escaper *strings.Replacer) (int, error) { | ||
return fmt.Fprintf(w, "##teamcity[InspectionType id='%s' name='%s' description='%s' category='%s']\n", | ||
limit(i.id, smallLimit), limit(i.name, smallLimit), limit(escaper.Replace(i.description), largeLimit), limit(i.category, smallLimit)) | ||
} | ||
|
||
// InspectionInstance reports a specific defect, warning, error message. | ||
// Includes location, description, and various optional and custom attributes. | ||
// https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance | ||
type InspectionInstance struct { | ||
typeID string // (mandatory) limited by 255 characters. | ||
message string // (optional) limited by 4000 characters. | ||
file string // (mandatory) file path limited by 4000 characters. | ||
line int // (optional) line of the file. | ||
severity string // (optional) any linter severity. | ||
} | ||
|
||
func (i InspectionInstance) Print(w io.Writer, replacer *strings.Replacer) (int, error) { | ||
return fmt.Fprintf(w, "##teamcity[inspection typeId='%s' message='%s' file='%s' line='%d' SEVERITY='%s']\n", | ||
limit(i.typeID, smallLimit), | ||
limit(replacer.Replace(i.message), largeLimit), | ||
limit(i.file, largeLimit), | ||
i.line, strings.ToUpper(i.severity)) | ||
} | ||
|
||
func limit(s string, max int) string { | ||
var size, count int | ||
for i := 0; i < max && count < len(s); i++ { | ||
_, size = utf8.DecodeRuneInString(s[count:]) | ||
count += size | ||
} | ||
|
||
return s[:count] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package printers | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"go/token" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/golangci/golangci-lint/pkg/result" | ||
) | ||
|
||
func TestTeamCity_Print(t *testing.T) { | ||
issues := []result.Issue{ | ||
{ | ||
FromLinter: "linter-a", | ||
Text: "warning issue", | ||
Pos: token.Position{ | ||
Filename: "path/to/filea.go", | ||
Offset: 2, | ||
Line: 10, | ||
Column: 4, | ||
}, | ||
}, | ||
{ | ||
FromLinter: "linter-a", | ||
Severity: "error", | ||
Text: "error issue", | ||
Pos: token.Position{ | ||
Filename: "path/to/filea.go", | ||
Offset: 2, | ||
Line: 10, | ||
}, | ||
}, | ||
{ | ||
FromLinter: "linter-b", | ||
Text: "info issue", | ||
SourceLines: []string{ | ||
"func foo() {", | ||
"\tfmt.Println(\"bar\")", | ||
"}", | ||
}, | ||
Pos: token.Position{ | ||
Filename: "path/to/fileb.go", | ||
Offset: 5, | ||
Line: 300, | ||
Column: 9, | ||
}, | ||
}, | ||
} | ||
|
||
buf := new(bytes.Buffer) | ||
printer := NewTeamCity(buf) | ||
|
||
err := printer.Print(context.Background(), issues) | ||
require.NoError(t, err) | ||
|
||
expected := `##teamcity[InspectionType id='linter-a' name='linter-a' description='linter-a' category='Golangci-lint reports'] | ||
##teamcity[inspection typeId='linter-a' message='warning issue' file='path/to/filea.go' line='10' SEVERITY=''] | ||
##teamcity[inspection typeId='linter-a' message='error issue' file='path/to/filea.go' line='10' SEVERITY='ERROR'] | ||
##teamcity[InspectionType id='linter-b' name='linter-b' description='linter-b' category='Golangci-lint reports'] | ||
##teamcity[inspection typeId='linter-b' message='info issue' file='path/to/fileb.go' line='300' SEVERITY=''] | ||
` | ||
|
||
assert.Equal(t, expected, buf.String()) | ||
} | ||
|
||
func TestTeamCity_limit(t *testing.T) { | ||
tests := []struct { | ||
input string | ||
max int | ||
expected string | ||
}{ | ||
{ | ||
input: "golangci-lint", | ||
max: 0, | ||
expected: "", | ||
}, | ||
{ | ||
input: "golangci-lint", | ||
max: 8, | ||
expected: "golangci", | ||
}, | ||
{ | ||
input: "golangci-lint", | ||
max: 13, | ||
expected: "golangci-lint", | ||
}, | ||
{ | ||
input: "golangci-lint", | ||
max: 15, | ||
expected: "golangci-lint", | ||
}, | ||
{ | ||
input: "こんにちは", | ||
max: 3, | ||
expected: "こんに", | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
require.Equal(t, tc.expected, limit(tc.input, tc.max)) | ||
} | ||
} |