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:
# colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
# 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
# print lines of code with issue, default is true

View File

@ -26,6 +26,8 @@ import (
"github.com/golangci/golangci-lint/pkg/result/processors"
)
const defaultFileMode = 0644
func getDefaultIssueExcludeHelp() string {
parts := []string{"Use or not use default excludes:"}
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
}
p, err := e.createPrinter()
if err != nil {
return err
formats := strings.Split(e.cfg.Output.Format, ",")
for _, format := range formats {
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)
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)
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
format := e.cfg.Output.Format
switch format {
case config.OutFormatJSON:
p = printers.NewJSON(&e.reportData)
p = printers.NewJSON(&e.reportData, w)
case config.OutFormatColoredLineNumber, config.OutFormatLineNumber:
p = printers.NewText(e.cfg.Output.PrintIssuedLine,
format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName,
e.log.Child("text_printer"))
e.log.Child("text_printer"), w)
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:
p = printers.NewCheckstyle()
p = printers.NewCheckstyle(w)
case config.OutFormatCodeClimate:
p = printers.NewCodeClimate()
p = printers.NewCodeClimate(w)
case config.OutFormatHTML:
p = printers.NewHTML()
p = printers.NewHTML(w)
case config.OutFormatJunitXML:
p = printers.NewJunitXML()
p = printers.NewJunitXML(w)
case config.OutFormatGithubActions:
p = printers.NewGithub()
p = printers.NewGithub(w)
default:
return nil, fmt.Errorf("unknown output format %s", format)
}

View File

@ -4,10 +4,10 @@ import (
"context"
"encoding/xml"
"fmt"
"io"
"github.com/go-xmlfmt/xmlfmt"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -32,13 +32,15 @@ type checkstyleError struct {
const defaultCheckstyleSeverity = "error"
type Checkstyle struct{}
func NewCheckstyle() *Checkstyle {
return &Checkstyle{}
type Checkstyle struct {
w io.Writer
}
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{
Version: "5.0",
}
@ -82,6 +84,10 @@ func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
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
}

View File

@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -24,10 +24,11 @@ type CodeClimateIssue struct {
}
type CodeClimate struct {
w io.Writer
}
func NewCodeClimate() *CodeClimate {
return &CodeClimate{}
func NewCodeClimate(w io.Writer) *CodeClimate {
return &CodeClimate{w: w}
}
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
}
fmt.Fprint(logutils.StdOut, string(outputJSON))
_, err = fmt.Fprint(p.w, string(outputJSON))
if err != nil {
return err
}
return nil
}

View File

@ -3,20 +3,21 @@ package printers
import (
"context"
"fmt"
"io"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
type github struct {
w io.Writer
}
const defaultGithubSeverity = "error"
// 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
func NewGithub() Printer {
return &github{}
func NewGithub(w io.Writer) Printer {
return &github{w: w}
}
// 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
}
func (g *github) Print(_ context.Context, issues []result.Issue) error {
func (p *github) Print(_ context.Context, issues []result.Issue) error {
for ind := range issues {
_, err := fmt.Fprintln(logutils.StdOut, formatIssueAsGithub(&issues[ind]))
_, err := fmt.Fprintln(p.w, formatIssueAsGithub(&issues[ind]))
if err != nil {
return err
}

View File

@ -4,9 +4,9 @@ import (
"context"
"fmt"
"html/template"
"io"
"strings"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -123,13 +123,15 @@ type htmlIssue struct {
Code string
}
type HTML struct{}
func NewHTML() *HTML {
return &HTML{}
type HTML struct {
w io.Writer
}
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
for i := range issues {
@ -151,5 +153,5 @@ func (h HTML) Print(_ context.Context, issues []result.Issue) error {
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 (
"context"
"encoding/json"
"fmt"
"io"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report"
"github.com/golangci/golangci-lint/pkg/result"
)
type JSON struct {
rd *report.Data
w io.Writer
}
func NewJSON(rd *report.Data) *JSON {
func NewJSON(rd *report.Data, w io.Writer) *JSON {
return &JSON{
rd: rd,
w: w,
}
}
@ -34,11 +35,5 @@ func (p JSON) Print(ctx context.Context, issues []result.Issue) error {
res.Issues = []result.Issue{}
}
outputJSON, err := json.Marshal(res)
if err != nil {
return err
}
fmt.Fprint(logutils.StdOut, string(outputJSON))
return nil
return json.NewEncoder(p.w).Encode(res)
}

View File

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

View File

@ -15,12 +15,14 @@ import (
type Tab struct {
printLinterName bool
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{
printLinterName: printLinterName,
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 {
w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0)
w := tabwriter.NewWriter(p.w, 0, 0, 2, ' ', 0)
for i := range issues {
p.printIssue(&issues[i], w)

View File

@ -3,6 +3,7 @@ package printers
import (
"context"
"fmt"
"io"
"strings"
"github.com/fatih/color"
@ -17,14 +18,16 @@ type Text struct {
printLinterName bool
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{
printIssuedLine: printIssuedLine,
useColors: useColors,
printLinterName: printLinterName,
log: log,
w: w,
}
}
@ -61,12 +64,12 @@ func (p Text) printIssue(i *result.Issue) {
if i.Pos.Column != 0 {
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) {
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 (
"bufio"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
@ -97,6 +99,64 @@ func TestGciLocal(t *testing.T) {
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()) {
f, err := os.CreateTemp("", "golangci_lint_test")
require.NoError(t, err)

View File

@ -76,6 +76,11 @@ func (r *RunResult) ExpectOutputContains(s string) *RunResult {
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 {
assert.Equal(r.t, s, r.output, "exit code is %d", r.exitCode)
return r