Skip to content

Commit d209389

Browse files
authored
Add support for multiple outputs (#2386)
1 parent 669852e commit d209389

File tree

12 files changed

+186
-56
lines changed

12 files changed

+186
-56
lines changed

.golangci.example.yml

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ run:
6262
output:
6363
# colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
6464
# default is "colored-line-number"
65+
# multiple can be specified by separating them by comma, output can be provided
66+
# for each of them by separating format name and path by colon symbol.
67+
# Output path can be either `stdout`, `stderr` or path to the file to write to.
68+
# Example "checkstyle:report.json,colored-line-number"
6569
format: colored-line-number
6670

6771
# print lines of code with issue, default is true

pkg/commands/run.go

+61-14
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
"github.com/golangci/golangci-lint/pkg/result/processors"
2727
)
2828

29+
const defaultFileMode = 0644
30+
2931
func getDefaultIssueExcludeHelp() string {
3032
parts := []string{"Use or not use default excludes:"}
3133
for _, ep := range config.DefaultExcludePatterns {
@@ -400,44 +402,89 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
400402
return err // XXX: don't loose type
401403
}
402404

403-
p, err := e.createPrinter()
404-
if err != nil {
405-
return err
405+
formats := strings.Split(e.cfg.Output.Format, ",")
406+
for _, format := range formats {
407+
out := strings.SplitN(format, ":", 2)
408+
if len(out) < 2 {
409+
out = append(out, "")
410+
}
411+
412+
err := e.printReports(ctx, issues, out[1], out[0])
413+
if err != nil {
414+
return err
415+
}
406416
}
407417

408418
e.setExitCodeIfIssuesFound(issues)
409419

420+
e.fileCache.PrintStats(e.log)
421+
422+
return nil
423+
}
424+
425+
func (e *Executor) printReports(ctx context.Context, issues []result.Issue, path, format string) error {
426+
w, shouldClose, err := e.createWriter(path)
427+
if err != nil {
428+
return fmt.Errorf("can't create output for %s: %w", path, err)
429+
}
430+
431+
p, err := e.createPrinter(format, w)
432+
if err != nil {
433+
if file, ok := w.(io.Closer); shouldClose && ok {
434+
_ = file.Close()
435+
}
436+
return err
437+
}
438+
410439
if err = p.Print(ctx, issues); err != nil {
440+
if file, ok := w.(io.Closer); shouldClose && ok {
441+
_ = file.Close()
442+
}
411443
return fmt.Errorf("can't print %d issues: %s", len(issues), err)
412444
}
413445

414-
e.fileCache.PrintStats(e.log)
446+
if file, ok := w.(io.Closer); shouldClose && ok {
447+
_ = file.Close()
448+
}
415449

416450
return nil
417451
}
418452

419-
func (e *Executor) createPrinter() (printers.Printer, error) {
453+
func (e *Executor) createWriter(path string) (io.Writer, bool, error) {
454+
if path == "" || path == "stdout" {
455+
return logutils.StdOut, false, nil
456+
}
457+
if path == "stderr" {
458+
return logutils.StdErr, false, nil
459+
}
460+
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultFileMode)
461+
if err != nil {
462+
return nil, false, err
463+
}
464+
return f, true, nil
465+
}
466+
467+
func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer, error) {
420468
var p printers.Printer
421-
format := e.cfg.Output.Format
422469
switch format {
423470
case config.OutFormatJSON:
424-
p = printers.NewJSON(&e.reportData)
471+
p = printers.NewJSON(&e.reportData, w)
425472
case config.OutFormatColoredLineNumber, config.OutFormatLineNumber:
426473
p = printers.NewText(e.cfg.Output.PrintIssuedLine,
427474
format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName,
428-
e.log.Child("text_printer"))
475+
e.log.Child("text_printer"), w)
429476
case config.OutFormatTab:
430-
p = printers.NewTab(e.cfg.Output.PrintLinterName, e.log.Child("tab_printer"))
477+
p = printers.NewTab(e.cfg.Output.PrintLinterName, e.log.Child("tab_printer"), w)
431478
case config.OutFormatCheckstyle:
432-
p = printers.NewCheckstyle()
479+
p = printers.NewCheckstyle(w)
433480
case config.OutFormatCodeClimate:
434-
p = printers.NewCodeClimate()
481+
p = printers.NewCodeClimate(w)
435482
case config.OutFormatHTML:
436-
p = printers.NewHTML()
483+
p = printers.NewHTML(w)
437484
case config.OutFormatJunitXML:
438-
p = printers.NewJunitXML()
485+
p = printers.NewJunitXML(w)
439486
case config.OutFormatGithubActions:
440-
p = printers.NewGithub()
487+
p = printers.NewGithub(w)
441488
default:
442489
return nil, fmt.Errorf("unknown output format %s", format)
443490
}

pkg/printers/checkstyle.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import (
44
"context"
55
"encoding/xml"
66
"fmt"
7+
"io"
78

89
"github.com/go-xmlfmt/xmlfmt"
910

10-
"github.com/golangci/golangci-lint/pkg/logutils"
1111
"github.com/golangci/golangci-lint/pkg/result"
1212
)
1313

@@ -32,13 +32,15 @@ type checkstyleError struct {
3232

3333
const defaultCheckstyleSeverity = "error"
3434

35-
type Checkstyle struct{}
35+
type Checkstyle struct {
36+
w io.Writer
37+
}
3638

37-
func NewCheckstyle() *Checkstyle {
38-
return &Checkstyle{}
39+
func NewCheckstyle(w io.Writer) *Checkstyle {
40+
return &Checkstyle{w: w}
3941
}
4042

41-
func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
43+
func (p Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
4244
out := checkstyleOutput{
4345
Version: "5.0",
4446
}
@@ -82,6 +84,10 @@ func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
8284
return err
8385
}
8486

85-
fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, xmlfmt.FormatXML(string(data), "", " "))
87+
_, err = fmt.Fprintf(p.w, "%s%s\n", xml.Header, xmlfmt.FormatXML(string(data), "", " "))
88+
if err != nil {
89+
return err
90+
}
91+
8692
return nil
8793
}

pkg/printers/codeclimate.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78

8-
"github.com/golangci/golangci-lint/pkg/logutils"
99
"github.com/golangci/golangci-lint/pkg/result"
1010
)
1111

@@ -24,10 +24,11 @@ type CodeClimateIssue struct {
2424
}
2525

2626
type CodeClimate struct {
27+
w io.Writer
2728
}
2829

29-
func NewCodeClimate() *CodeClimate {
30-
return &CodeClimate{}
30+
func NewCodeClimate(w io.Writer) *CodeClimate {
31+
return &CodeClimate{w: w}
3132
}
3233

3334
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 {
5253
return err
5354
}
5455

55-
fmt.Fprint(logutils.StdOut, string(outputJSON))
56+
_, err = fmt.Fprint(p.w, string(outputJSON))
57+
if err != nil {
58+
return err
59+
}
5660
return nil
5761
}

pkg/printers/github.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ package printers
33
import (
44
"context"
55
"fmt"
6+
"io"
67

7-
"github.com/golangci/golangci-lint/pkg/logutils"
88
"github.com/golangci/golangci-lint/pkg/result"
99
)
1010

1111
type github struct {
12+
w io.Writer
1213
}
1314

1415
const defaultGithubSeverity = "error"
1516

1617
// NewGithub output format outputs issues according to GitHub actions format:
1718
// https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
18-
func NewGithub() Printer {
19-
return &github{}
19+
func NewGithub(w io.Writer) Printer {
20+
return &github{w: w}
2021
}
2122

2223
// 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 {
3536
return ret
3637
}
3738

38-
func (g *github) Print(_ context.Context, issues []result.Issue) error {
39+
func (p *github) Print(_ context.Context, issues []result.Issue) error {
3940
for ind := range issues {
40-
_, err := fmt.Fprintln(logutils.StdOut, formatIssueAsGithub(&issues[ind]))
41+
_, err := fmt.Fprintln(p.w, formatIssueAsGithub(&issues[ind]))
4142
if err != nil {
4243
return err
4344
}

pkg/printers/html.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import (
44
"context"
55
"fmt"
66
"html/template"
7+
"io"
78
"strings"
89

9-
"github.com/golangci/golangci-lint/pkg/logutils"
1010
"github.com/golangci/golangci-lint/pkg/result"
1111
)
1212

@@ -123,13 +123,15 @@ type htmlIssue struct {
123123
Code string
124124
}
125125

126-
type HTML struct{}
126+
type HTML struct {
127+
w io.Writer
128+
}
127129

128-
func NewHTML() *HTML {
129-
return &HTML{}
130+
func NewHTML(w io.Writer) *HTML {
131+
return &HTML{w: w}
130132
}
131133

132-
func (h HTML) Print(_ context.Context, issues []result.Issue) error {
134+
func (p HTML) Print(_ context.Context, issues []result.Issue) error {
133135
var htmlIssues []htmlIssue
134136

135137
for i := range issues {
@@ -151,5 +153,5 @@ func (h HTML) Print(_ context.Context, issues []result.Issue) error {
151153
return err
152154
}
153155

154-
return t.Execute(logutils.StdOut, struct{ Issues []htmlIssue }{Issues: htmlIssues})
156+
return t.Execute(p.w, struct{ Issues []htmlIssue }{Issues: htmlIssues})
155157
}

pkg/printers/json.go

+5-10
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ package printers
33
import (
44
"context"
55
"encoding/json"
6-
"fmt"
6+
"io"
77

8-
"github.com/golangci/golangci-lint/pkg/logutils"
98
"github.com/golangci/golangci-lint/pkg/report"
109
"github.com/golangci/golangci-lint/pkg/result"
1110
)
1211

1312
type JSON struct {
1413
rd *report.Data
14+
w io.Writer
1515
}
1616

17-
func NewJSON(rd *report.Data) *JSON {
17+
func NewJSON(rd *report.Data, w io.Writer) *JSON {
1818
return &JSON{
1919
rd: rd,
20+
w: w,
2021
}
2122
}
2223

@@ -34,11 +35,5 @@ func (p JSON) Print(ctx context.Context, issues []result.Issue) error {
3435
res.Issues = []result.Issue{}
3536
}
3637

37-
outputJSON, err := json.Marshal(res)
38-
if err != nil {
39-
return err
40-
}
41-
42-
fmt.Fprint(logutils.StdOut, string(outputJSON))
43-
return nil
38+
return json.NewEncoder(p.w).Encode(res)
4439
}

pkg/printers/junitxml.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package printers
33
import (
44
"context"
55
"encoding/xml"
6+
"io"
67
"strings"
78

8-
"github.com/golangci/golangci-lint/pkg/logutils"
99
"github.com/golangci/golangci-lint/pkg/result"
1010
)
1111

@@ -35,13 +35,14 @@ type failureXML struct {
3535
}
3636

3737
type JunitXML struct {
38+
w io.Writer
3839
}
3940

40-
func NewJunitXML() *JunitXML {
41-
return &JunitXML{}
41+
func NewJunitXML(w io.Writer) *JunitXML {
42+
return &JunitXML{w: w}
4243
}
4344

44-
func (JunitXML) Print(ctx context.Context, issues []result.Issue) error {
45+
func (p JunitXML) Print(ctx context.Context, issues []result.Issue) error {
4546
suites := make(map[string]testSuiteXML) // use a map to group by file
4647

4748
for ind := range issues {
@@ -70,7 +71,7 @@ func (JunitXML) Print(ctx context.Context, issues []result.Issue) error {
7071
res.TestSuites = append(res.TestSuites, val)
7172
}
7273

73-
enc := xml.NewEncoder(logutils.StdOut)
74+
enc := xml.NewEncoder(p.w)
7475
enc.Indent("", " ")
7576
if err := enc.Encode(res); err != nil {
7677
return err

pkg/printers/tab.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ import (
1515
type Tab struct {
1616
printLinterName bool
1717
log logutils.Log
18+
w io.Writer
1819
}
1920

20-
func NewTab(printLinterName bool, log logutils.Log) *Tab {
21+
func NewTab(printLinterName bool, log logutils.Log, w io.Writer) *Tab {
2122
return &Tab{
2223
printLinterName: printLinterName,
2324
log: log,
25+
w: w,
2426
}
2527
}
2628

@@ -30,7 +32,7 @@ func (p Tab) SprintfColored(ca color.Attribute, format string, args ...interface
3032
}
3133

3234
func (p *Tab) Print(ctx context.Context, issues []result.Issue) error {
33-
w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0)
35+
w := tabwriter.NewWriter(p.w, 0, 0, 2, ' ', 0)
3436

3537
for i := range issues {
3638
p.printIssue(&issues[i], w)

0 commit comments

Comments
 (0)