Add support for multiple outputs (#2386)

This commit is contained in:
Lauris BH 2022-01-04 22:36:27 +02:00 committed by GitHub
parent 669852edbb
commit d209389625
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 191 additions and 61 deletions

View File

@ -62,6 +62,10 @@ run:
output: output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
# default is "colored-line-number" # default is "colored-line-number"
# multiple can be specified by separating them by comma, output can be provided
# for each of them by separating format name and path by colon symbol.
# Output path can be either `stdout`, `stderr` or path to the file to write to.
# Example "checkstyle:report.json,colored-line-number"
format: colored-line-number format: colored-line-number
# print lines of code with issue, default is true # print lines of code with issue, default is true

View File

@ -26,6 +26,8 @@ import (
"github.com/golangci/golangci-lint/pkg/result/processors" "github.com/golangci/golangci-lint/pkg/result/processors"
) )
const defaultFileMode = 0644
func getDefaultIssueExcludeHelp() string { func getDefaultIssueExcludeHelp() string {
parts := []string{"Use or not use default excludes:"} parts := []string{"Use or not use default excludes:"}
for _, ep := range config.DefaultExcludePatterns { for _, ep := range config.DefaultExcludePatterns {
@ -400,44 +402,89 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
return err // XXX: don't loose type return err // XXX: don't loose type
} }
p, err := e.createPrinter() formats := strings.Split(e.cfg.Output.Format, ",")
if err != nil { for _, format := range formats {
return err out := strings.SplitN(format, ":", 2)
if len(out) < 2 {
out = append(out, "")
}
err := e.printReports(ctx, issues, out[1], out[0])
if err != nil {
return err
}
} }
e.setExitCodeIfIssuesFound(issues) e.setExitCodeIfIssuesFound(issues)
if err = p.Print(ctx, issues); err != nil {
return fmt.Errorf("can't print %d issues: %s", len(issues), err)
}
e.fileCache.PrintStats(e.log) e.fileCache.PrintStats(e.log)
return nil return nil
} }
func (e *Executor) createPrinter() (printers.Printer, error) { func (e *Executor) printReports(ctx context.Context, issues []result.Issue, path, format string) error {
w, shouldClose, err := e.createWriter(path)
if err != nil {
return fmt.Errorf("can't create output for %s: %w", path, err)
}
p, err := e.createPrinter(format, w)
if err != nil {
if file, ok := w.(io.Closer); shouldClose && ok {
_ = file.Close()
}
return err
}
if err = p.Print(ctx, issues); err != nil {
if file, ok := w.(io.Closer); shouldClose && ok {
_ = file.Close()
}
return fmt.Errorf("can't print %d issues: %s", len(issues), err)
}
if file, ok := w.(io.Closer); shouldClose && ok {
_ = file.Close()
}
return nil
}
func (e *Executor) createWriter(path string) (io.Writer, bool, error) {
if path == "" || path == "stdout" {
return logutils.StdOut, false, nil
}
if path == "stderr" {
return logutils.StdErr, false, nil
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultFileMode)
if err != nil {
return nil, false, err
}
return f, true, nil
}
func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer, error) {
var p printers.Printer var p printers.Printer
format := e.cfg.Output.Format
switch format { switch format {
case config.OutFormatJSON: case config.OutFormatJSON:
p = printers.NewJSON(&e.reportData) p = printers.NewJSON(&e.reportData, w)
case config.OutFormatColoredLineNumber, config.OutFormatLineNumber: case config.OutFormatColoredLineNumber, config.OutFormatLineNumber:
p = printers.NewText(e.cfg.Output.PrintIssuedLine, p = printers.NewText(e.cfg.Output.PrintIssuedLine,
format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName, format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName,
e.log.Child("text_printer")) e.log.Child("text_printer"), w)
case config.OutFormatTab: case config.OutFormatTab:
p = printers.NewTab(e.cfg.Output.PrintLinterName, e.log.Child("tab_printer")) p = printers.NewTab(e.cfg.Output.PrintLinterName, e.log.Child("tab_printer"), w)
case config.OutFormatCheckstyle: case config.OutFormatCheckstyle:
p = printers.NewCheckstyle() p = printers.NewCheckstyle(w)
case config.OutFormatCodeClimate: case config.OutFormatCodeClimate:
p = printers.NewCodeClimate() p = printers.NewCodeClimate(w)
case config.OutFormatHTML: case config.OutFormatHTML:
p = printers.NewHTML() p = printers.NewHTML(w)
case config.OutFormatJunitXML: case config.OutFormatJunitXML:
p = printers.NewJunitXML() p = printers.NewJunitXML(w)
case config.OutFormatGithubActions: case config.OutFormatGithubActions:
p = printers.NewGithub() p = printers.NewGithub(w)
default: default:
return nil, fmt.Errorf("unknown output format %s", format) return nil, fmt.Errorf("unknown output format %s", format)
} }

View File

@ -4,10 +4,10 @@ import (
"context" "context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io"
"github.com/go-xmlfmt/xmlfmt" "github.com/go-xmlfmt/xmlfmt"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
@ -32,13 +32,15 @@ type checkstyleError struct {
const defaultCheckstyleSeverity = "error" const defaultCheckstyleSeverity = "error"
type Checkstyle struct{} type Checkstyle struct {
w io.Writer
func NewCheckstyle() *Checkstyle {
return &Checkstyle{}
} }
func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error { func NewCheckstyle(w io.Writer) *Checkstyle {
return &Checkstyle{w: w}
}
func (p Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
out := checkstyleOutput{ out := checkstyleOutput{
Version: "5.0", Version: "5.0",
} }
@ -82,6 +84,10 @@ func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
return err return err
} }
fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, xmlfmt.FormatXML(string(data), "", " ")) _, err = fmt.Fprintf(p.w, "%s%s\n", xml.Header, xmlfmt.FormatXML(string(data), "", " "))
if err != nil {
return err
}
return nil return nil
} }

View File

@ -4,8 +4,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
@ -24,10 +24,11 @@ type CodeClimateIssue struct {
} }
type CodeClimate struct { type CodeClimate struct {
w io.Writer
} }
func NewCodeClimate() *CodeClimate { func NewCodeClimate(w io.Writer) *CodeClimate {
return &CodeClimate{} return &CodeClimate{w: w}
} }
func (p CodeClimate) Print(ctx context.Context, issues []result.Issue) error { func (p CodeClimate) Print(ctx context.Context, issues []result.Issue) error {
@ -52,6 +53,9 @@ func (p CodeClimate) Print(ctx context.Context, issues []result.Issue) error {
return err return err
} }
fmt.Fprint(logutils.StdOut, string(outputJSON)) _, err = fmt.Fprint(p.w, string(outputJSON))
if err != nil {
return err
}
return nil return nil
} }

View File

@ -3,20 +3,21 @@ package printers
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
type github struct { type github struct {
w io.Writer
} }
const defaultGithubSeverity = "error" const defaultGithubSeverity = "error"
// NewGithub output format outputs issues according to GitHub actions format: // NewGithub output format outputs issues according to GitHub actions format:
// https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message // https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
func NewGithub() Printer { func NewGithub(w io.Writer) Printer {
return &github{} return &github{w: w}
} }
// print each line as: ::error file=app.js,line=10,col=15::Something went wrong // print each line as: ::error file=app.js,line=10,col=15::Something went wrong
@ -35,9 +36,9 @@ func formatIssueAsGithub(issue *result.Issue) string {
return ret return ret
} }
func (g *github) Print(_ context.Context, issues []result.Issue) error { func (p *github) Print(_ context.Context, issues []result.Issue) error {
for ind := range issues { for ind := range issues {
_, err := fmt.Fprintln(logutils.StdOut, formatIssueAsGithub(&issues[ind])) _, err := fmt.Fprintln(p.w, formatIssueAsGithub(&issues[ind]))
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,9 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"html/template" "html/template"
"io"
"strings" "strings"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
@ -123,13 +123,15 @@ type htmlIssue struct {
Code string Code string
} }
type HTML struct{} type HTML struct {
w io.Writer
func NewHTML() *HTML {
return &HTML{}
} }
func (h HTML) Print(_ context.Context, issues []result.Issue) error { func NewHTML(w io.Writer) *HTML {
return &HTML{w: w}
}
func (p HTML) Print(_ context.Context, issues []result.Issue) error {
var htmlIssues []htmlIssue var htmlIssues []htmlIssue
for i := range issues { for i := range issues {
@ -151,5 +153,5 @@ func (h HTML) Print(_ context.Context, issues []result.Issue) error {
return err return err
} }
return t.Execute(logutils.StdOut, struct{ Issues []htmlIssue }{Issues: htmlIssues}) return t.Execute(p.w, struct{ Issues []htmlIssue }{Issues: htmlIssues})
} }

View File

@ -3,20 +3,21 @@ package printers
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "io"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report" "github.com/golangci/golangci-lint/pkg/report"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
type JSON struct { type JSON struct {
rd *report.Data rd *report.Data
w io.Writer
} }
func NewJSON(rd *report.Data) *JSON { func NewJSON(rd *report.Data, w io.Writer) *JSON {
return &JSON{ return &JSON{
rd: rd, rd: rd,
w: w,
} }
} }
@ -34,11 +35,5 @@ func (p JSON) Print(ctx context.Context, issues []result.Issue) error {
res.Issues = []result.Issue{} res.Issues = []result.Issue{}
} }
outputJSON, err := json.Marshal(res) return json.NewEncoder(p.w).Encode(res)
if err != nil {
return err
}
fmt.Fprint(logutils.StdOut, string(outputJSON))
return nil
} }

View File

@ -3,9 +3,9 @@ package printers
import ( import (
"context" "context"
"encoding/xml" "encoding/xml"
"io"
"strings" "strings"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
@ -35,13 +35,14 @@ type failureXML struct {
} }
type JunitXML struct { type JunitXML struct {
w io.Writer
} }
func NewJunitXML() *JunitXML { func NewJunitXML(w io.Writer) *JunitXML {
return &JunitXML{} return &JunitXML{w: w}
} }
func (JunitXML) Print(ctx context.Context, issues []result.Issue) error { func (p JunitXML) Print(ctx context.Context, issues []result.Issue) error {
suites := make(map[string]testSuiteXML) // use a map to group by file suites := make(map[string]testSuiteXML) // use a map to group by file
for ind := range issues { for ind := range issues {
@ -70,7 +71,7 @@ func (JunitXML) Print(ctx context.Context, issues []result.Issue) error {
res.TestSuites = append(res.TestSuites, val) res.TestSuites = append(res.TestSuites, val)
} }
enc := xml.NewEncoder(logutils.StdOut) enc := xml.NewEncoder(p.w)
enc.Indent("", " ") enc.Indent("", " ")
if err := enc.Encode(res); err != nil { if err := enc.Encode(res); err != nil {
return err return err

View File

@ -15,12 +15,14 @@ import (
type Tab struct { type Tab struct {
printLinterName bool printLinterName bool
log logutils.Log log logutils.Log
w io.Writer
} }
func NewTab(printLinterName bool, log logutils.Log) *Tab { func NewTab(printLinterName bool, log logutils.Log, w io.Writer) *Tab {
return &Tab{ return &Tab{
printLinterName: printLinterName, printLinterName: printLinterName,
log: log, log: log,
w: w,
} }
} }
@ -30,7 +32,7 @@ func (p Tab) SprintfColored(ca color.Attribute, format string, args ...interface
} }
func (p *Tab) Print(ctx context.Context, issues []result.Issue) error { func (p *Tab) Print(ctx context.Context, issues []result.Issue) error {
w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(p.w, 0, 0, 2, ' ', 0)
for i := range issues { for i := range issues {
p.printIssue(&issues[i], w) p.printIssue(&issues[i], w)

View File

@ -3,6 +3,7 @@ package printers
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
@ -17,14 +18,16 @@ type Text struct {
printLinterName bool printLinterName bool
log logutils.Log log logutils.Log
w io.Writer
} }
func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log) *Text { func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log, w io.Writer) *Text {
return &Text{ return &Text{
printIssuedLine: printIssuedLine, printIssuedLine: printIssuedLine,
useColors: useColors, useColors: useColors,
printLinterName: printLinterName, printLinterName: printLinterName,
log: log, log: log,
w: w,
} }
} }
@ -61,12 +64,12 @@ func (p Text) printIssue(i *result.Issue) {
if i.Pos.Column != 0 { if i.Pos.Column != 0 {
pos += fmt.Sprintf(":%d", i.Pos.Column) pos += fmt.Sprintf(":%d", i.Pos.Column)
} }
fmt.Fprintf(logutils.StdOut, "%s: %s\n", pos, text) fmt.Fprintf(p.w, "%s: %s\n", pos, text)
} }
func (p Text) printSourceCode(i *result.Issue) { func (p Text) printSourceCode(i *result.Issue) {
for _, line := range i.SourceLines { for _, line := range i.SourceLines {
fmt.Fprintln(logutils.StdOut, line) fmt.Fprintln(p.w, line)
} }
} }
@ -87,5 +90,5 @@ func (p Text) printUnderLinePointer(i *result.Issue) {
} }
} }
fmt.Fprintf(logutils.StdOut, "%s%s\n", string(prefixRunes), p.SprintfColored(color.FgYellow, "^")) fmt.Fprintf(p.w, "%s%s\n", string(prefixRunes), p.SprintfColored(color.FgYellow, "^"))
} }

View File

@ -2,8 +2,10 @@ package test
import ( import (
"bufio" "bufio"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@ -97,6 +99,64 @@ func TestGciLocal(t *testing.T) {
ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed") ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed")
} }
func TestMultipleOutputs(t *testing.T) {
sourcePath := filepath.Join(testdataDir, "gci", "gci.go")
args := []string{
"--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number,json:stdout",
sourcePath,
}
rc := extractRunContextFromComments(t, sourcePath)
args = append(args, rc.args...)
cfg, err := yaml.Marshal(rc.config)
require.NoError(t, err)
testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...).
ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed").
ExpectOutputContains(`"Issues":[`)
}
func TestStderrOutput(t *testing.T) {
sourcePath := filepath.Join(testdataDir, "gci", "gci.go")
args := []string{
"--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number,json:stderr",
sourcePath,
}
rc := extractRunContextFromComments(t, sourcePath)
args = append(args, rc.args...)
cfg, err := yaml.Marshal(rc.config)
require.NoError(t, err)
testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...).
ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed").
ExpectOutputContains(`"Issues":[`)
}
func TestFileOutput(t *testing.T) {
resultPath := path.Join(t.TempDir(), "golangci_lint_test_result")
sourcePath := filepath.Join(testdataDir, "gci", "gci.go")
args := []string{
"--disable-all", "--print-issued-lines=false", "--print-linter-name=false",
fmt.Sprintf("--out-format=json:%s,line-number", resultPath),
sourcePath,
}
rc := extractRunContextFromComments(t, sourcePath)
args = append(args, rc.args...)
cfg, err := yaml.Marshal(rc.config)
require.NoError(t, err)
testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...).
ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed").
ExpectOutputNotContains(`"Issues":[`)
b, err := os.ReadFile(resultPath)
require.NoError(t, err)
require.Contains(t, string(b), `"Issues":[`)
}
func saveConfig(t *testing.T, cfg map[string]interface{}) (cfgPath string, finishFunc func()) { func saveConfig(t *testing.T, cfg map[string]interface{}) (cfgPath string, finishFunc func()) {
f, err := os.CreateTemp("", "golangci_lint_test") f, err := os.CreateTemp("", "golangci_lint_test")
require.NoError(t, err) require.NoError(t, err)

View File

@ -76,6 +76,11 @@ func (r *RunResult) ExpectOutputContains(s string) *RunResult {
return r return r
} }
func (r *RunResult) ExpectOutputNotContains(s string) *RunResult {
assert.NotContains(r.t, r.output, s, "exit code is %d", r.exitCode)
return r
}
func (r *RunResult) ExpectOutputEq(s string) *RunResult { func (r *RunResult) ExpectOutputEq(s string) *RunResult {
assert.Equal(r.t, s, r.output, "exit code is %d", r.exitCode) assert.Equal(r.t, s, r.output, "exit code is %d", r.exitCode)
return r return r