Skip to content

Commit 937e69d

Browse files
committed
Support flag to clear outputs from config
This change set adds a new command line flag called `--clear-config-outputs`. When setting this flag all configured outputs from the config file will be cleared and only the ones from the command line respected or if none, fallback to the default happens. This is useful for applications like the golangci-lint LSP that invoke golangci-lint and want to ensure that only the JSON output is enabled without respecting the users config.
1 parent 6441d5c commit 937e69d

File tree

5 files changed

+514
-1
lines changed

5 files changed

+514
-1
lines changed

pkg/commands/flagsets.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ func setupOutputFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
6666
color.GreenString("Path mode to use (empty, or 'abs')"))
6767
internal.AddFlagAndBind(v, fs, fs.Bool, "show-stats", "output.show-stats", true, color.GreenString("Show statistics per linter"))
6868

69+
const clearConfigOutputsDesc = "Clear all output formats from the configuration file. " +
70+
"If no output formats are specified on the command line, the default text format will be used."
71+
fs.Bool("clear-config-outputs", false, color.GreenString(clearConfigOutputsDesc)) // Flags only, no config file binding
72+
6973
setupOutputFormatsFlagSet(v, fs)
7074
}
7175

pkg/config/loader.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func newLoader(log logutils.Log, v *viper.Viper, fs *pflag.FlagSet, opts LoaderO
6464
}
6565
}
6666

67-
func (l *Loader) Load(opts LoadOptions) error {
67+
func (l *Loader) Load(opts LoadOptions) error { //nolint:gocyclo // it's not too complex
6868
err := l.BaseLoader.Load()
6969
if err != nil {
7070
return err
@@ -119,6 +119,11 @@ func (l *Loader) Load(opts LoadOptions) error {
119119
return err
120120
}
121121

122+
err = l.handleClearConfigOutputs()
123+
if err != nil {
124+
return err
125+
}
126+
122127
if opts.Validation {
123128
err = l.cfg.Validate()
124129
if err != nil {
@@ -226,6 +231,92 @@ func (l *Loader) handleEnableOnlyOption() error {
226231
return nil
227232
}
228233

234+
func (l *Loader) handleClearConfigOutputs() error { //nolint:gocyclo // just having to check all the flags, it's fine.
235+
if l.fs == nil {
236+
return nil
237+
}
238+
239+
clearConfigOutputs, err := l.fs.GetBool("clear-config-outputs")
240+
if err != nil {
241+
return err
242+
}
243+
244+
if !clearConfigOutputs {
245+
return nil
246+
}
247+
248+
// Save CLI-provided output format settings by checking which flags were explicitly set
249+
cliFormats := Formats{}
250+
251+
// Text format
252+
if l.fs.Changed("output.text.path") {
253+
cliFormats.Text.Path, _ = l.fs.GetString("output.text.path")
254+
}
255+
if l.fs.Changed("output.text.print-linter-name") {
256+
cliFormats.Text.PrintLinterName, _ = l.fs.GetBool("output.text.print-linter-name")
257+
}
258+
if l.fs.Changed("output.text.print-issued-lines") {
259+
cliFormats.Text.PrintIssuedLine, _ = l.fs.GetBool("output.text.print-issued-lines")
260+
}
261+
if l.fs.Changed("output.text.colors") {
262+
cliFormats.Text.Colors, _ = l.fs.GetBool("output.text.colors")
263+
}
264+
265+
// JSON format
266+
if l.fs.Changed("output.json.path") {
267+
cliFormats.JSON.Path, _ = l.fs.GetString("output.json.path")
268+
}
269+
270+
// Tab format
271+
if l.fs.Changed("output.tab.path") {
272+
cliFormats.Tab.Path, _ = l.fs.GetString("output.tab.path")
273+
}
274+
if l.fs.Changed("output.tab.print-linter-name") {
275+
cliFormats.Tab.PrintLinterName, _ = l.fs.GetBool("output.tab.print-linter-name")
276+
}
277+
if l.fs.Changed("output.tab.colors") {
278+
cliFormats.Tab.Colors, _ = l.fs.GetBool("output.tab.colors")
279+
}
280+
281+
// HTML format
282+
if l.fs.Changed("output.html.path") {
283+
cliFormats.HTML.Path, _ = l.fs.GetString("output.html.path")
284+
}
285+
286+
// Checkstyle format
287+
if l.fs.Changed("output.checkstyle.path") {
288+
cliFormats.Checkstyle.Path, _ = l.fs.GetString("output.checkstyle.path")
289+
}
290+
291+
// Code Climate format
292+
if l.fs.Changed("output.code-climate.path") {
293+
cliFormats.CodeClimate.Path, _ = l.fs.GetString("output.code-climate.path")
294+
}
295+
296+
// JUnit XML format
297+
if l.fs.Changed("output.junit-xml.path") {
298+
cliFormats.JUnitXML.Path, _ = l.fs.GetString("output.junit-xml.path")
299+
}
300+
if l.fs.Changed("output.junit-xml.extended") {
301+
cliFormats.JUnitXML.Extended, _ = l.fs.GetBool("output.junit-xml.extended")
302+
}
303+
304+
// TeamCity format
305+
if l.fs.Changed("output.teamcity.path") {
306+
cliFormats.TeamCity.Path, _ = l.fs.GetString("output.teamcity.path")
307+
}
308+
309+
// SARIF format
310+
if l.fs.Changed("output.sarif.path") {
311+
cliFormats.Sarif.Path, _ = l.fs.GetString("output.sarif.path")
312+
}
313+
314+
// Replace the config's output formats with only the CLI-provided ones
315+
l.cfg.Output.Formats = cliFormats
316+
317+
return nil
318+
}
319+
229320
func (l *Loader) handleFormatters() {
230321
l.handleFormatterOverrides()
231322
l.handleFormatterExclusions()

pkg/config/loader_test.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/pflag"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/golangci/golangci-lint/v2/pkg/logutils"
11+
)
12+
13+
func TestLoader_handleClearConfigOutputs(t *testing.T) {
14+
t.Run("flag not set", func(t *testing.T) {
15+
// Setup
16+
cfg := &Config{
17+
Output: Output{
18+
Formats: Formats{
19+
JSON: SimpleFormat{Path: "/tmp/config.json"},
20+
HTML: SimpleFormat{Path: "/tmp/config.html"},
21+
},
22+
},
23+
}
24+
25+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
26+
fs.Bool("clear-config-outputs", false, "test flag")
27+
28+
loader := &Loader{
29+
BaseLoader: &BaseLoader{
30+
log: logutils.NewStderrLog(logutils.DebugKeyEmpty),
31+
},
32+
fs: fs,
33+
cfg: cfg,
34+
}
35+
36+
// Execute
37+
err := loader.handleClearConfigOutputs()
38+
require.NoError(t, err)
39+
40+
// Verify - config outputs should remain unchanged
41+
assert.Equal(t, "/tmp/config.json", cfg.Output.Formats.JSON.Path)
42+
assert.Equal(t, "/tmp/config.html", cfg.Output.Formats.HTML.Path)
43+
})
44+
45+
t.Run("flag set with no CLI outputs", func(t *testing.T) {
46+
// Setup
47+
cfg := &Config{
48+
Output: Output{
49+
Formats: Formats{
50+
JSON: SimpleFormat{Path: "/tmp/config.json"},
51+
HTML: SimpleFormat{Path: "/tmp/config.html"},
52+
},
53+
},
54+
}
55+
56+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
57+
fs.Bool("clear-config-outputs", false, "test flag")
58+
fs.String("output.json.path", "", "json output path")
59+
fs.String("output.html.path", "", "html output path")
60+
61+
// Set the flag
62+
err := fs.Set("clear-config-outputs", "true")
63+
require.NoError(t, err)
64+
65+
loader := &Loader{
66+
BaseLoader: &BaseLoader{
67+
log: logutils.NewStderrLog(logutils.DebugKeyEmpty),
68+
},
69+
fs: fs,
70+
cfg: cfg,
71+
}
72+
73+
// Execute
74+
err = loader.handleClearConfigOutputs()
75+
require.NoError(t, err)
76+
77+
// Verify - all config outputs should be cleared
78+
assert.Empty(t, cfg.Output.Formats.JSON.Path)
79+
assert.Empty(t, cfg.Output.Formats.HTML.Path)
80+
})
81+
82+
t.Run("flag set with CLI JSON output", func(t *testing.T) {
83+
// Setup
84+
cfg := &Config{
85+
Output: Output{
86+
Formats: Formats{
87+
JSON: SimpleFormat{Path: "/tmp/config.json"},
88+
HTML: SimpleFormat{Path: "/tmp/config.html"},
89+
},
90+
},
91+
}
92+
93+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
94+
fs.Bool("clear-config-outputs", false, "test flag")
95+
fs.String("output.json.path", "", "json output path")
96+
fs.String("output.html.path", "", "html output path")
97+
98+
// Set the flag and CLI output
99+
err := fs.Set("clear-config-outputs", "true")
100+
require.NoError(t, err)
101+
err = fs.Set("output.json.path", "/tmp/cli.json")
102+
require.NoError(t, err)
103+
104+
loader := &Loader{
105+
BaseLoader: &BaseLoader{
106+
log: logutils.NewStderrLog(logutils.DebugKeyEmpty),
107+
},
108+
fs: fs,
109+
cfg: cfg,
110+
}
111+
112+
// Execute
113+
err = loader.handleClearConfigOutputs()
114+
require.NoError(t, err)
115+
116+
// Verify - only CLI output should remain
117+
assert.Equal(t, "/tmp/cli.json", cfg.Output.Formats.JSON.Path)
118+
assert.Empty(t, cfg.Output.Formats.HTML.Path)
119+
})
120+
121+
t.Run("flag set with multiple CLI outputs", func(t *testing.T) {
122+
// Setup
123+
cfg := &Config{
124+
Output: Output{
125+
Formats: Formats{
126+
JSON: SimpleFormat{Path: "/tmp/config.json"},
127+
HTML: SimpleFormat{Path: "/tmp/config.html"},
128+
Text: Text{
129+
SimpleFormat: SimpleFormat{Path: "/tmp/config.txt"},
130+
},
131+
},
132+
},
133+
}
134+
135+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
136+
fs.Bool("clear-config-outputs", false, "test flag")
137+
fs.String("output.json.path", "", "json output path")
138+
fs.String("output.html.path", "", "html output path")
139+
fs.String("output.text.path", "", "text output path")
140+
141+
// Set the flag and CLI outputs
142+
err := fs.Set("clear-config-outputs", "true")
143+
require.NoError(t, err)
144+
err = fs.Set("output.json.path", "/tmp/cli.json")
145+
require.NoError(t, err)
146+
err = fs.Set("output.html.path", "/tmp/cli.html")
147+
require.NoError(t, err)
148+
149+
loader := &Loader{
150+
BaseLoader: &BaseLoader{
151+
log: logutils.NewStderrLog(logutils.DebugKeyEmpty),
152+
},
153+
fs: fs,
154+
cfg: cfg,
155+
}
156+
157+
// Execute
158+
err = loader.handleClearConfigOutputs()
159+
require.NoError(t, err)
160+
161+
// Verify - only CLI outputs should remain
162+
assert.Equal(t, "/tmp/cli.json", cfg.Output.Formats.JSON.Path)
163+
assert.Equal(t, "/tmp/cli.html", cfg.Output.Formats.HTML.Path)
164+
assert.Empty(t, cfg.Output.Formats.Text.Path)
165+
})
166+
167+
t.Run("flag set with CLI format options", func(t *testing.T) {
168+
// Setup
169+
cfg := &Config{
170+
Output: Output{
171+
Formats: Formats{
172+
Text: Text{
173+
SimpleFormat: SimpleFormat{Path: "/tmp/config.txt"},
174+
PrintLinterName: false,
175+
PrintIssuedLine: false,
176+
Colors: false,
177+
},
178+
},
179+
},
180+
}
181+
182+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
183+
fs.Bool("clear-config-outputs", false, "test flag")
184+
fs.String("output.text.path", "", "text output path")
185+
fs.Bool("output.text.print-linter-name", true, "print linter name")
186+
fs.Bool("output.text.colors", true, "use colors")
187+
188+
// Set the flag and CLI outputs with options
189+
err := fs.Set("clear-config-outputs", "true")
190+
require.NoError(t, err)
191+
err = fs.Set("output.text.path", "/tmp/cli.txt")
192+
require.NoError(t, err)
193+
err = fs.Set("output.text.print-linter-name", "true")
194+
require.NoError(t, err)
195+
err = fs.Set("output.text.colors", "false")
196+
require.NoError(t, err)
197+
198+
loader := &Loader{
199+
BaseLoader: &BaseLoader{
200+
log: logutils.NewStderrLog(logutils.DebugKeyEmpty),
201+
},
202+
fs: fs,
203+
cfg: cfg,
204+
}
205+
206+
// Execute
207+
err = loader.handleClearConfigOutputs()
208+
require.NoError(t, err)
209+
210+
// Verify - CLI output with options should be preserved
211+
assert.Equal(t, "/tmp/cli.txt", cfg.Output.Formats.Text.Path)
212+
assert.True(t, cfg.Output.Formats.Text.PrintLinterName)
213+
assert.False(t, cfg.Output.Formats.Text.Colors)
214+
assert.False(t, cfg.Output.Formats.Text.PrintIssuedLine) // Not set via CLI, should be default false
215+
})
216+
}

0 commit comments

Comments
 (0)