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 | ||||
|   # - 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 | ||||
|   # - TeamCity: https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance | ||||
|   # | ||||
|   # Default value is an empty string. | ||||
|   default-severity: error | ||||
|  | ||||
| @ -494,6 +494,8 @@ func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer, | ||||
| 		p = printers.NewJunitXML(w) | ||||
| 	case config.OutFormatGithubActions: | ||||
| 		p = printers.NewGithub(w) | ||||
| 	case config.OutFormatTeamCity: | ||||
| 		p = printers.NewTeamCity(w) | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("unknown output format %s", format) | ||||
| 	} | ||||
|  | ||||
| @ -10,6 +10,7 @@ const ( | ||||
| 	OutFormatHTML              = "html" | ||||
| 	OutFormatJunitXML          = "junit-xml" | ||||
| 	OutFormatGithubActions     = "github-actions" | ||||
| 	OutFormatTeamCity          = "teamcity" | ||||
| ) | ||||
| 
 | ||||
| var OutFormats = []string{ | ||||
| @ -22,6 +23,7 @@ var OutFormats = []string{ | ||||
| 	OutFormatHTML, | ||||
| 	OutFormatJunitXML, | ||||
| 	OutFormatGithubActions, | ||||
| 	OutFormatTeamCity, | ||||
| } | ||||
| 
 | ||||
| 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
	 ferhat elmas
						ferhat elmas