feat: add TeamCity output format (#3606)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com> Co-authored-by: Oleksandr Redko <oleksandr.red+github@gmail.com>
This commit is contained in:
parent
998329d9be
commit
075691c4e9
@ -2339,6 +2339,7 @@ severity:
|
|||||||
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
|
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
|
||||||
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel
|
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel
|
||||||
# - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
|
# - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
|
||||||
|
# - TeamCity: https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance
|
||||||
#
|
#
|
||||||
# Default value is an empty string.
|
# Default value is an empty string.
|
||||||
default-severity: error
|
default-severity: error
|
||||||
|
@ -494,6 +494,8 @@ func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer,
|
|||||||
p = printers.NewJunitXML(w)
|
p = printers.NewJunitXML(w)
|
||||||
case config.OutFormatGithubActions:
|
case config.OutFormatGithubActions:
|
||||||
p = printers.NewGithub(w)
|
p = printers.NewGithub(w)
|
||||||
|
case config.OutFormatTeamCity:
|
||||||
|
p = printers.NewTeamCity(w)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown output format %s", format)
|
return nil, fmt.Errorf("unknown output format %s", format)
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ const (
|
|||||||
OutFormatHTML = "html"
|
OutFormatHTML = "html"
|
||||||
OutFormatJunitXML = "junit-xml"
|
OutFormatJunitXML = "junit-xml"
|
||||||
OutFormatGithubActions = "github-actions"
|
OutFormatGithubActions = "github-actions"
|
||||||
|
OutFormatTeamCity = "teamcity"
|
||||||
)
|
)
|
||||||
|
|
||||||
var OutFormats = []string{
|
var OutFormats = []string{
|
||||||
@ -22,6 +23,7 @@ var OutFormats = []string{
|
|||||||
OutFormatHTML,
|
OutFormatHTML,
|
||||||
OutFormatJunitXML,
|
OutFormatJunitXML,
|
||||||
OutFormatGithubActions,
|
OutFormatGithubActions,
|
||||||
|
OutFormatTeamCity,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Output struct {
|
type Output struct {
|
||||||
|
123
pkg/printers/teamcity.go
Normal file
123
pkg/printers/teamcity.go
Normal file
@ -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]
|
||||||
|
}
|
106
pkg/printers/teamcity_test.go
Normal file
106
pkg/printers/teamcity_test.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user