Skip to content

Commit d209389

Browse files
authoredJan 4, 2022
Add support for multiple outputs (#2386)
1 parent 669852e commit d209389

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)

‎pkg/printers/text.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package printers
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"strings"
78

89
"github.com/fatih/color"
@@ -17,14 +18,16 @@ type Text struct {
1718
printLinterName bool
1819

1920
log logutils.Log
21+
w io.Writer
2022
}
2123

22-
func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log) *Text {
24+
func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log, w io.Writer) *Text {
2325
return &Text{
2426
printIssuedLine: printIssuedLine,
2527
useColors: useColors,
2628
printLinterName: printLinterName,
2729
log: log,
30+
w: w,
2831
}
2932
}
3033

@@ -61,12 +64,12 @@ func (p Text) printIssue(i *result.Issue) {
6164
if i.Pos.Column != 0 {
6265
pos += fmt.Sprintf(":%d", i.Pos.Column)
6366
}
64-
fmt.Fprintf(logutils.StdOut, "%s: %s\n", pos, text)
67+
fmt.Fprintf(p.w, "%s: %s\n", pos, text)
6568
}
6669

6770
func (p Text) printSourceCode(i *result.Issue) {
6871
for _, line := range i.SourceLines {
69-
fmt.Fprintln(logutils.StdOut, line)
72+
fmt.Fprintln(p.w, line)
7073
}
7174
}
7275

@@ -87,5 +90,5 @@ func (p Text) printUnderLinePointer(i *result.Issue) {
8790
}
8891
}
8992

90-
fmt.Fprintf(logutils.StdOut, "%s%s\n", string(prefixRunes), p.SprintfColored(color.FgYellow, "^"))
93+
fmt.Fprintf(p.w, "%s%s\n", string(prefixRunes), p.SprintfColored(color.FgYellow, "^"))
9194
}

‎test/linters_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package test
22

33
import (
44
"bufio"
5+
"fmt"
56
"os"
67
"os/exec"
8+
"path"
79
"path/filepath"
810
"strings"
911
"testing"
@@ -97,6 +99,64 @@ func TestGciLocal(t *testing.T) {
9799
ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed")
98100
}
99101

102+
func TestMultipleOutputs(t *testing.T) {
103+
sourcePath := filepath.Join(testdataDir, "gci", "gci.go")
104+
args := []string{
105+
"--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number,json:stdout",
106+
sourcePath,
107+
}
108+
rc := extractRunContextFromComments(t, sourcePath)
109+
args = append(args, rc.args...)
110+
111+
cfg, err := yaml.Marshal(rc.config)
112+
require.NoError(t, err)
113+
114+
testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...).
115+
ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed").
116+
ExpectOutputContains(`"Issues":[`)
117+
}
118+
119+
func TestStderrOutput(t *testing.T) {
120+
sourcePath := filepath.Join(testdataDir, "gci", "gci.go")
121+
args := []string{
122+
"--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number,json:stderr",
123+
sourcePath,
124+
}
125+
rc := extractRunContextFromComments(t, sourcePath)
126+
args = append(args, rc.args...)
127+
128+
cfg, err := yaml.Marshal(rc.config)
129+
require.NoError(t, err)
130+
131+
testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...).
132+
ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed").
133+
ExpectOutputContains(`"Issues":[`)
134+
}
135+
136+
func TestFileOutput(t *testing.T) {
137+
resultPath := path.Join(t.TempDir(), "golangci_lint_test_result")
138+
139+
sourcePath := filepath.Join(testdataDir, "gci", "gci.go")
140+
args := []string{
141+
"--disable-all", "--print-issued-lines=false", "--print-linter-name=false",
142+
fmt.Sprintf("--out-format=json:%s,line-number", resultPath),
143+
sourcePath,
144+
}
145+
rc := extractRunContextFromComments(t, sourcePath)
146+
args = append(args, rc.args...)
147+
148+
cfg, err := yaml.Marshal(rc.config)
149+
require.NoError(t, err)
150+
151+
testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...).
152+
ExpectHasIssue("testdata/gci/gci.go:7: File is not `gci`-ed").
153+
ExpectOutputNotContains(`"Issues":[`)
154+
155+
b, err := os.ReadFile(resultPath)
156+
require.NoError(t, err)
157+
require.Contains(t, string(b), `"Issues":[`)
158+
}
159+
100160
func saveConfig(t *testing.T, cfg map[string]interface{}) (cfgPath string, finishFunc func()) {
101161
f, err := os.CreateTemp("", "golangci_lint_test")
102162
require.NoError(t, err)

‎test/testshared/testshared.go

+5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ func (r *RunResult) ExpectOutputContains(s string) *RunResult {
7676
return r
7777
}
7878

79+
func (r *RunResult) ExpectOutputNotContains(s string) *RunResult {
80+
assert.NotContains(r.t, r.output, s, "exit code is %d", r.exitCode)
81+
return r
82+
}
83+
7984
func (r *RunResult) ExpectOutputEq(s string) *RunResult {
8085
assert.Equal(r.t, s, r.output, "exit code is %d", r.exitCode)
8186
return r

0 commit comments

Comments
 (0)
Please sign in to comment.