From 0ead74b2b2cedb967c388a3fd6afd6ed9b28c72f Mon Sep 17 00:00:00 2001 From: Timo Furrer Date: Fri, 19 Dec 2025 12:09:09 +0100 Subject: [PATCH] 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. --- docs/data/cli_help.json | 2 +- pkg/commands/flagsets.go | 4 + pkg/config/loader.go | 99 +++++++- pkg/config/loader_test.go | 216 ++++++++++++++++++ test/output_test.go | 183 +++++++++++++++ test/testdata/configs/output_with_formats.yml | 19 ++ 6 files changed, 521 insertions(+), 2 deletions(-) create mode 100644 pkg/config/loader_test.go create mode 100644 test/testdata/configs/output_with_formats.yml diff --git a/docs/data/cli_help.json b/docs/data/cli_help.json index 7a16d378070b..ca73156c7a18 100644 --- a/docs/data/cli_help.json +++ b/docs/data/cli_help.json @@ -1,7 +1,7 @@ { "defaultEnabledLinters": "Enabled by default linters:\nerrcheck: Errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases.\ngovet: Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes. [auto-fix]\nineffassign: Detects when assignments to existing variables are not used. [fast]\nstaticcheck: It's the set of rules from staticcheck. [auto-fix]\nunused: Checks Go code for unused constants, variables, functions and types.", "rootOutput": "Smart, fast linters runner.\n\nUsage:\n golangci-lint [flags]\n golangci-lint [command]\n\nAvailable Commands:\n cache Cache control and information.\n completion Generate the autocompletion script for the specified shell\n config Configuration file information and verification.\n custom Build a version of golangci-lint with custom linters.\n fmt Format Go source files.\n formatters List current formatters configuration.\n help Display extra help\n linters List current linters configuration.\n migrate Migrate configuration file from v1 to v2.\n run Lint the code.\n version Display the golangci-lint version.\n\nFlags:\n --color string Use color when printing; can be 'always', 'auto', or 'never' (default \"auto\")\n -h, --help Help for a command\n -v, --verbose Verbose output\n --version Print version\n\nUse \"golangci-lint [command] --help\" for more information about a command.\n", - "runOutput": "Lint the code.\n\nUsage:\n golangci-lint run [flags]\n\nFlags:\n -c, --config PATH Read config from file path PATH\n --no-config Don't read config file\n --default string Default set of linters to enable (default \"standard\")\n -D, --disable strings Disable specific linter\n -E, --enable strings Enable specific linter\n --enable-only strings Override linters configuration section to only run the specific linter(s)\n --fast-only Filter enabled linters to run only fast linters\n -j, --concurrency int Number of CPUs to use (Default: Automatically set to match Linux container CPU quota and fall back to the number of logical CPUs in the machine)\n --modules-download-mode string Modules download mode. If not empty, passed as -mod=\u003cmode\u003e to go tools\n --issues-exit-code int Exit code when issues were found (default 1)\n --build-tags strings Build tags\n --timeout duration Timeout for total work. Disabled by default\n --tests Analyze tests (*_test.go) (default true)\n --allow-parallel-runners Allow multiple parallel golangci-lint instances running.\n If false (default) - golangci-lint acquires file lock on start.\n --allow-serial-runners Allow multiple golangci-lint instances running, but serialize them around a lock.\n If false (default) - golangci-lint exits with an error if it fails to acquire file lock on start.\n --path-prefix string Path prefix to add to output\n --path-mode string Path mode to use (empty, or 'abs')\n --show-stats Show statistics per linter (default true)\n --output.text.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.text.print-linter-name Print linter name in the end of issue text. (default true)\n --output.text.print-issued-lines Print lines of code with issue. (default true)\n --output.text.colors Use colors. (default true)\n --output.json.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.tab.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.tab.print-linter-name Print linter name in the end of issue text. (default true)\n --output.tab.colors Use colors. (default true)\n --output.html.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.checkstyle.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.code-climate.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.junit-xml.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.junit-xml.extended Support extra JUnit XML fields.\n --output.teamcity.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.sarif.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --max-issues-per-linter int Maximum issues count per one linter. Set to 0 to disable (default 50)\n --max-same-issues int Maximum count of issues with the same text. Set to 0 to disable (default 3)\n --uniq-by-line Make issues output unique by line (default true)\n -n, --new Show only new issues: if there are unstaged changes or untracked files, only those changes are analyzed, else only changes in HEAD~ are analyzed.\n It's a super-useful option for integration of golangci-lint into existing large codebase.\n It's not practical to fix all existing issues at the moment of integration: much better to not allow issues in new code.\n For CI setups, prefer --new-from-rev=HEAD~, as --new can skip linting the current patch if any scripts generate unstaged files before golangci-lint runs.\n --new-from-rev REV Show only new issues created after git revision REV\n --new-from-patch PATH Show only new issues created in git patch with file path PATH\n --new-from-merge-base string Show only new issues created after the best common ancestor (merge-base against HEAD)\n --whole-files Show issues in any part of update files (requires new-from-rev or new-from-patch)\n --fix Apply the fixes detected by the linters and formatters (if it's supported by the linter)\n --cpu-profile-path string Path to CPU profile output file\n --mem-profile-path string Path to memory profile output file\n --trace-path string Path to trace output file\n\nGlobal Flags:\n --color string Use color when printing; can be 'always', 'auto', or 'never' (default \"auto\")\n -h, --help Help for a command\n -v, --verbose Verbose output\n", + "runOutput": "Lint the code.\n\nUsage:\n golangci-lint run [flags]\n\nFlags:\n -c, --config PATH Read config from file path PATH\n --no-config Don't read config file\n --default string Default set of linters to enable (default \"standard\")\n -D, --disable strings Disable specific linter\n -E, --enable strings Enable specific linter\n --enable-only strings Override linters configuration section to only run the specific linter(s)\n --fast-only Filter enabled linters to run only fast linters\n -j, --concurrency int Number of CPUs to use (Default: Automatically set to match Linux container CPU quota and fall back to the number of logical CPUs in the machine)\n --modules-download-mode string Modules download mode. If not empty, passed as -mod=\u003cmode\u003e to go tools\n --issues-exit-code int Exit code when issues were found (default 1)\n --build-tags strings Build tags\n --timeout duration Timeout for total work. Disabled by default\n --tests Analyze tests (*_test.go) (default true)\n --allow-parallel-runners Allow multiple parallel golangci-lint instances running.\n If false (default) - golangci-lint acquires file lock on start.\n --allow-serial-runners Allow multiple golangci-lint instances running, but serialize them around a lock.\n If false (default) - golangci-lint exits with an error if it fails to acquire file lock on start.\n --path-prefix string Path prefix to add to output\n --path-mode string Path mode to use (empty, or 'abs')\n --show-stats Show statistics per linter (default true)\n --clear-config-outputs Clear all output formats from the configuration file. If no output formats are specified on the command line, the default text format will be used.\n --output.text.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.text.print-linter-name Print linter name in the end of issue text. (default true)\n --output.text.print-issued-lines Print lines of code with issue. (default true)\n --output.text.colors Use colors. (default true)\n --output.json.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.tab.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.tab.print-linter-name Print linter name in the end of issue text. (default true)\n --output.tab.colors Use colors. (default true)\n --output.html.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.checkstyle.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.code-climate.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.junit-xml.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.junit-xml.extended Support extra JUnit XML fields.\n --output.teamcity.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --output.sarif.path stdout Output path can be either stdout, `stderr` or path to the file to write to.\n --max-issues-per-linter int Maximum issues count per one linter. Set to 0 to disable (default 50)\n --max-same-issues int Maximum count of issues with the same text. Set to 0 to disable (default 3)\n --uniq-by-line Make issues output unique by line (default true)\n -n, --new Show only new issues: if there are unstaged changes or untracked files, only those changes are analyzed, else only changes in HEAD~ are analyzed.\n It's a super-useful option for integration of golangci-lint into existing large codebase.\n It's not practical to fix all existing issues at the moment of integration: much better to not allow issues in new code.\n For CI setups, prefer --new-from-rev=HEAD~, as --new can skip linting the current patch if any scripts generate unstaged files before golangci-lint runs.\n --new-from-rev REV Show only new issues created after git revision REV\n --new-from-patch PATH Show only new issues created in git patch with file path PATH\n --new-from-merge-base string Show only new issues created after the best common ancestor (merge-base against HEAD)\n --whole-files Show issues in any part of update files (requires new-from-rev or new-from-patch)\n --fix Apply the fixes detected by the linters and formatters (if it's supported by the linter)\n --cpu-profile-path string Path to CPU profile output file\n --mem-profile-path string Path to memory profile output file\n --trace-path string Path to trace output file\n\nGlobal Flags:\n --color string Use color when printing; can be 'always', 'auto', or 'never' (default \"auto\")\n -h, --help Help for a command\n -v, --verbose Verbose output\n", "lintersOutput": "List current linters configuration.\n\nUsage:\n golangci-lint linters [flags]\n\nFlags:\n -c, --config PATH Read config from file path PATH\n --no-config Don't read config file\n --default string Default set of linters to enable (default \"standard\")\n -D, --disable strings Disable specific linter\n -E, --enable strings Enable specific linter\n --enable-only strings Override linters configuration section to only run the specific linter(s)\n --fast-only Filter enabled linters to run only fast linters\n --json Display as JSON\n\nGlobal Flags:\n --color string Use color when printing; can be 'always', 'auto', or 'never' (default \"auto\")\n -h, --help Help for a command\n -v, --verbose Verbose output\n", "fmtOutput": "Format Go source files.\n\nUsage:\n golangci-lint fmt [flags]\n\nFlags:\n -c, --config PATH Read config from file path PATH\n --no-config Don't read config file\n -E, --enable strings Enable specific formatter\n -d, --diff Display diffs instead of rewriting files\n --diff-colored Display diffs instead of rewriting files (with colors)\n --stdin Use standard input for piping source files\n\nGlobal Flags:\n --color string Use color when printing; can be 'always', 'auto', or 'never' (default \"auto\")\n -h, --help Help for a command\n -v, --verbose Verbose output\n", "formattersOutput": "List current formatters configuration.\n\nUsage:\n golangci-lint formatters [flags]\n\nFlags:\n -c, --config PATH Read config from file path PATH\n --no-config Don't read config file\n -E, --enable strings Enable specific formatter\n --json Display as JSON\n\nGlobal Flags:\n --color string Use color when printing; can be 'always', 'auto', or 'never' (default \"auto\")\n -h, --help Help for a command\n -v, --verbose Verbose output\n", diff --git a/pkg/commands/flagsets.go b/pkg/commands/flagsets.go index 2b61217c6589..76369ecc5af8 100644 --- a/pkg/commands/flagsets.go +++ b/pkg/commands/flagsets.go @@ -66,6 +66,10 @@ func setupOutputFlagSet(v *viper.Viper, fs *pflag.FlagSet) { color.GreenString("Path mode to use (empty, or 'abs')")) internal.AddFlagAndBind(v, fs, fs.Bool, "show-stats", "output.show-stats", true, color.GreenString("Show statistics per linter")) + const clearConfigOutputsDesc = "Clear all output formats from the configuration file. " + + "If no output formats are specified on the command line, the default text format will be used." + fs.Bool("clear-config-outputs", false, color.GreenString(clearConfigOutputsDesc)) // Flags only, no config file binding + setupOutputFormatsFlagSet(v, fs) } diff --git a/pkg/config/loader.go b/pkg/config/loader.go index 511c3ab7da70..0223a17fd48b 100644 --- a/pkg/config/loader.go +++ b/pkg/config/loader.go @@ -64,7 +64,7 @@ func newLoader(log logutils.Log, v *viper.Viper, fs *pflag.FlagSet, opts LoaderO } } -func (l *Loader) Load(opts LoadOptions) error { +func (l *Loader) Load(opts LoadOptions) error { //nolint:gocyclo // it's not too complex err := l.BaseLoader.Load() if err != nil { return err @@ -119,6 +119,11 @@ func (l *Loader) Load(opts LoadOptions) error { return err } + err = l.handleClearConfigOutputs() + if err != nil { + return err + } + if opts.Validation { err = l.cfg.Validate() if err != nil { @@ -226,6 +231,98 @@ func (l *Loader) handleEnableOnlyOption() error { return nil } +func (l *Loader) handleClearConfigOutputs() error { //nolint:gocyclo // just having to check all the flags, it's fine. + if l.fs == nil { + return nil + } + + // Check if the flag is defined (it's only defined for the run command) + flag := l.fs.Lookup("clear-config-outputs") + if flag == nil { + return nil + } + + clearConfigOutputs, err := l.fs.GetBool("clear-config-outputs") + if err != nil { + return err + } + + if !clearConfigOutputs { + return nil + } + + // Save CLI-provided output format settings by checking which flags were explicitly set + cliFormats := Formats{} + + // Text format + if l.fs.Changed("output.text.path") { + cliFormats.Text.Path, _ = l.fs.GetString("output.text.path") + } + if l.fs.Changed("output.text.print-linter-name") { + cliFormats.Text.PrintLinterName, _ = l.fs.GetBool("output.text.print-linter-name") + } + if l.fs.Changed("output.text.print-issued-lines") { + cliFormats.Text.PrintIssuedLine, _ = l.fs.GetBool("output.text.print-issued-lines") + } + if l.fs.Changed("output.text.colors") { + cliFormats.Text.Colors, _ = l.fs.GetBool("output.text.colors") + } + + // JSON format + if l.fs.Changed("output.json.path") { + cliFormats.JSON.Path, _ = l.fs.GetString("output.json.path") + } + + // Tab format + if l.fs.Changed("output.tab.path") { + cliFormats.Tab.Path, _ = l.fs.GetString("output.tab.path") + } + if l.fs.Changed("output.tab.print-linter-name") { + cliFormats.Tab.PrintLinterName, _ = l.fs.GetBool("output.tab.print-linter-name") + } + if l.fs.Changed("output.tab.colors") { + cliFormats.Tab.Colors, _ = l.fs.GetBool("output.tab.colors") + } + + // HTML format + if l.fs.Changed("output.html.path") { + cliFormats.HTML.Path, _ = l.fs.GetString("output.html.path") + } + + // Checkstyle format + if l.fs.Changed("output.checkstyle.path") { + cliFormats.Checkstyle.Path, _ = l.fs.GetString("output.checkstyle.path") + } + + // Code Climate format + if l.fs.Changed("output.code-climate.path") { + cliFormats.CodeClimate.Path, _ = l.fs.GetString("output.code-climate.path") + } + + // JUnit XML format + if l.fs.Changed("output.junit-xml.path") { + cliFormats.JUnitXML.Path, _ = l.fs.GetString("output.junit-xml.path") + } + if l.fs.Changed("output.junit-xml.extended") { + cliFormats.JUnitXML.Extended, _ = l.fs.GetBool("output.junit-xml.extended") + } + + // TeamCity format + if l.fs.Changed("output.teamcity.path") { + cliFormats.TeamCity.Path, _ = l.fs.GetString("output.teamcity.path") + } + + // SARIF format + if l.fs.Changed("output.sarif.path") { + cliFormats.Sarif.Path, _ = l.fs.GetString("output.sarif.path") + } + + // Replace the config's output formats with only the CLI-provided ones + l.cfg.Output.Formats = cliFormats + + return nil +} + func (l *Loader) handleFormatters() { l.handleFormatterOverrides() l.handleFormatterExclusions() diff --git a/pkg/config/loader_test.go b/pkg/config/loader_test.go new file mode 100644 index 000000000000..5e9402bc7d72 --- /dev/null +++ b/pkg/config/loader_test.go @@ -0,0 +1,216 @@ +package config + +import ( + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/golangci/golangci-lint/v2/pkg/logutils" +) + +func TestLoader_handleClearConfigOutputs(t *testing.T) { + t.Run("flag not set", func(t *testing.T) { + // Setup + cfg := &Config{ + Output: Output{ + Formats: Formats{ + JSON: SimpleFormat{Path: "/tmp/config.json"}, + HTML: SimpleFormat{Path: "/tmp/config.html"}, + }, + }, + } + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.Bool("clear-config-outputs", false, "test flag") + + loader := &Loader{ + BaseLoader: &BaseLoader{ + log: logutils.NewStderrLog(logutils.DebugKeyEmpty), + }, + fs: fs, + cfg: cfg, + } + + // Execute + err := loader.handleClearConfigOutputs() + require.NoError(t, err) + + // Verify - config outputs should remain unchanged + assert.Equal(t, "/tmp/config.json", cfg.Output.Formats.JSON.Path) + assert.Equal(t, "/tmp/config.html", cfg.Output.Formats.HTML.Path) + }) + + t.Run("flag set with no CLI outputs", func(t *testing.T) { + // Setup + cfg := &Config{ + Output: Output{ + Formats: Formats{ + JSON: SimpleFormat{Path: "/tmp/config.json"}, + HTML: SimpleFormat{Path: "/tmp/config.html"}, + }, + }, + } + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.Bool("clear-config-outputs", false, "test flag") + fs.String("output.json.path", "", "json output path") + fs.String("output.html.path", "", "html output path") + + // Set the flag + err := fs.Set("clear-config-outputs", "true") + require.NoError(t, err) + + loader := &Loader{ + BaseLoader: &BaseLoader{ + log: logutils.NewStderrLog(logutils.DebugKeyEmpty), + }, + fs: fs, + cfg: cfg, + } + + // Execute + err = loader.handleClearConfigOutputs() + require.NoError(t, err) + + // Verify - all config outputs should be cleared + assert.Empty(t, cfg.Output.Formats.JSON.Path) + assert.Empty(t, cfg.Output.Formats.HTML.Path) + }) + + t.Run("flag set with CLI JSON output", func(t *testing.T) { + // Setup + cfg := &Config{ + Output: Output{ + Formats: Formats{ + JSON: SimpleFormat{Path: "/tmp/config.json"}, + HTML: SimpleFormat{Path: "/tmp/config.html"}, + }, + }, + } + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.Bool("clear-config-outputs", false, "test flag") + fs.String("output.json.path", "", "json output path") + fs.String("output.html.path", "", "html output path") + + // Set the flag and CLI output + err := fs.Set("clear-config-outputs", "true") + require.NoError(t, err) + err = fs.Set("output.json.path", "/tmp/cli.json") + require.NoError(t, err) + + loader := &Loader{ + BaseLoader: &BaseLoader{ + log: logutils.NewStderrLog(logutils.DebugKeyEmpty), + }, + fs: fs, + cfg: cfg, + } + + // Execute + err = loader.handleClearConfigOutputs() + require.NoError(t, err) + + // Verify - only CLI output should remain + assert.Equal(t, "/tmp/cli.json", cfg.Output.Formats.JSON.Path) + assert.Empty(t, cfg.Output.Formats.HTML.Path) + }) + + t.Run("flag set with multiple CLI outputs", func(t *testing.T) { + // Setup + cfg := &Config{ + Output: Output{ + Formats: Formats{ + JSON: SimpleFormat{Path: "/tmp/config.json"}, + HTML: SimpleFormat{Path: "/tmp/config.html"}, + Text: Text{ + SimpleFormat: SimpleFormat{Path: "/tmp/config.txt"}, + }, + }, + }, + } + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.Bool("clear-config-outputs", false, "test flag") + fs.String("output.json.path", "", "json output path") + fs.String("output.html.path", "", "html output path") + fs.String("output.text.path", "", "text output path") + + // Set the flag and CLI outputs + err := fs.Set("clear-config-outputs", "true") + require.NoError(t, err) + err = fs.Set("output.json.path", "/tmp/cli.json") + require.NoError(t, err) + err = fs.Set("output.html.path", "/tmp/cli.html") + require.NoError(t, err) + + loader := &Loader{ + BaseLoader: &BaseLoader{ + log: logutils.NewStderrLog(logutils.DebugKeyEmpty), + }, + fs: fs, + cfg: cfg, + } + + // Execute + err = loader.handleClearConfigOutputs() + require.NoError(t, err) + + // Verify - only CLI outputs should remain + assert.Equal(t, "/tmp/cli.json", cfg.Output.Formats.JSON.Path) + assert.Equal(t, "/tmp/cli.html", cfg.Output.Formats.HTML.Path) + assert.Empty(t, cfg.Output.Formats.Text.Path) + }) + + t.Run("flag set with CLI format options", func(t *testing.T) { + // Setup + cfg := &Config{ + Output: Output{ + Formats: Formats{ + Text: Text{ + SimpleFormat: SimpleFormat{Path: "/tmp/config.txt"}, + PrintLinterName: false, + PrintIssuedLine: false, + Colors: false, + }, + }, + }, + } + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.Bool("clear-config-outputs", false, "test flag") + fs.String("output.text.path", "", "text output path") + fs.Bool("output.text.print-linter-name", true, "print linter name") + fs.Bool("output.text.colors", true, "use colors") + + // Set the flag and CLI outputs with options + err := fs.Set("clear-config-outputs", "true") + require.NoError(t, err) + err = fs.Set("output.text.path", "/tmp/cli.txt") + require.NoError(t, err) + err = fs.Set("output.text.print-linter-name", "true") + require.NoError(t, err) + err = fs.Set("output.text.colors", "false") + require.NoError(t, err) + + loader := &Loader{ + BaseLoader: &BaseLoader{ + log: logutils.NewStderrLog(logutils.DebugKeyEmpty), + }, + fs: fs, + cfg: cfg, + } + + // Execute + err = loader.handleClearConfigOutputs() + require.NoError(t, err) + + // Verify - CLI output with options should be preserved + assert.Equal(t, "/tmp/cli.txt", cfg.Output.Formats.Text.Path) + assert.True(t, cfg.Output.Formats.Text.PrintLinterName) + assert.False(t, cfg.Output.Formats.Text.Colors) + assert.False(t, cfg.Output.Formats.Text.PrintIssuedLine) // Not set via CLI, should be default false + }) +} diff --git a/test/output_test.go b/test/output_test.go index 25ee4715a1bf..e737068ac45e 100644 --- a/test/output_test.go +++ b/test/output_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" @@ -92,3 +93,185 @@ func TestOutput_Multiple(t *testing.T) { ExpectHasIssue("testdata/output.go:6:38: `occured` is a misspelling of `occurred`"). ExpectOutputContains(testshared.NormalizeFilePathInJSON(expectedJSONOutput)) } + +func TestOutput_ClearConfigOutputs_WithoutFlag(t *testing.T) { + // Test that config file outputs are used when the flag is not present + tempDir := t.TempDir() + configTemplatePath := filepath.Join(testdataDir, "configs", "output_with_formats.yml") + configPath := filepath.Join(tempDir, "test-config.yml") + sourcePath := filepath.Join(testdataDir, "output.go") + + jsonOutput := filepath.Join(tempDir, "config-json-output.json") + htmlOutput := filepath.Join(tempDir, "config-html-output.html") + + // Read template config and replace placeholder with temp directory + configTemplate, err := os.ReadFile(configTemplatePath) + require.NoError(t, err) + + configContent := strings.ReplaceAll(string(configTemplate), "{{TEMPDIR}}", tempDir) + + // Write the modified config + err = os.WriteFile(configPath, []byte(configContent), 0o400) + require.NoError(t, err) + + testshared.NewRunnerBuilder(t). + WithArgs( + "--default=none", + fmt.Sprintf("--config=%s", configPath), + ). + WithDirectives(sourcePath). + WithTargetPath(sourcePath). + Runner(). + Install(). + Run(). + ExpectExitCode(exitcodes.IssuesFound) + + // Verify both config-specified files were created + _, err = os.Stat(jsonOutput) + require.NoError(t, err, "JSON output from config should exist") + + _, err = os.Stat(htmlOutput) + require.NoError(t, err, "HTML output from config should exist") +} + +func TestOutput_ClearConfigOutputs_WithFlag(t *testing.T) { + // Test that config file outputs are cleared when the flag is present + tempDir := t.TempDir() + configTemplatePath := filepath.Join(testdataDir, "configs", "output_with_formats.yml") + configPath := filepath.Join(tempDir, "test-config.yml") + sourcePath := filepath.Join(testdataDir, "output.go") + + jsonOutput := filepath.Join(tempDir, "config-json-output.json") + htmlOutput := filepath.Join(tempDir, "config-html-output.html") + + // Read template config and replace placeholder with temp directory + configTemplate, err := os.ReadFile(configTemplatePath) + require.NoError(t, err) + + configContent := strings.ReplaceAll(string(configTemplate), "{{TEMPDIR}}", tempDir) + + // Write the modified config + err = os.WriteFile(configPath, []byte(configContent), 0o400) + require.NoError(t, err) + + testshared.NewRunnerBuilder(t). + WithArgs( + "--default=none", + fmt.Sprintf("--config=%s", configPath), + "--clear-config-outputs", + ). + WithDirectives(sourcePath). + WithTargetPath(sourcePath). + Runner(). + Install(). + Run(). + //nolint:misspell // misspelling is intentional + ExpectHasIssue("testdata/output.go:6:38: `occured` is a misspelling of `occurred`") + + // Verify config-specified files were NOT created + _, err = os.Stat(jsonOutput) + require.True(t, os.IsNotExist(err), "JSON output from config should not exist") + + _, err = os.Stat(htmlOutput) + require.True(t, os.IsNotExist(err), "HTML output from config should not exist") +} + +func TestOutput_ClearConfigOutputs_WithCLIOutput(t *testing.T) { + // Test that CLI outputs are used when --clear-config-outputs is present + tempDir := t.TempDir() + configTemplatePath := filepath.Join(testdataDir, "configs", "output_with_formats.yml") + configPath := filepath.Join(tempDir, "test-config.yml") + sourcePath := filepath.Join(testdataDir, "output.go") + cliResultPath := filepath.Join(tempDir, "cli_result.json") + + configJsonOutput := filepath.Join(tempDir, "config-json-output.json") + configHtmlOutput := filepath.Join(tempDir, "config-html-output.html") + + // Read template config and replace placeholder with temp directory + configTemplate, err := os.ReadFile(configTemplatePath) + require.NoError(t, err) + + configContent := strings.ReplaceAll(string(configTemplate), "{{TEMPDIR}}", tempDir) + + // Write the modified config + err = os.WriteFile(configPath, []byte(configContent), 0o400) + require.NoError(t, err) + + testshared.NewRunnerBuilder(t). + WithArgs( + "--default=none", + fmt.Sprintf("--config=%s", configPath), + "--clear-config-outputs", + fmt.Sprintf("--output.json.path=%s", cliResultPath), + ). + WithDirectives(sourcePath). + WithTargetPath(sourcePath). + Runner(). + Install(). + Run(). + ExpectExitCode(exitcodes.IssuesFound) + + // Verify CLI output was created + b, err := os.ReadFile(cliResultPath) + require.NoError(t, err, "CLI JSON output should exist") + require.Contains(t, string(b), testshared.NormalizeFilePathInJSON(expectedJSONOutput)) + + // Verify config-specified files were NOT created + _, err = os.Stat(configJsonOutput) + require.True(t, os.IsNotExist(err), "JSON output from config should not exist") + + _, err = os.Stat(configHtmlOutput) + require.True(t, os.IsNotExist(err), "HTML output from config should not exist") +} + +func TestOutput_ClearConfigOutputs_WithMultipleCLIOutputs(t *testing.T) { + // Test that multiple CLI outputs work with --clear-config-outputs + tempDir := t.TempDir() + configTemplatePath := filepath.Join(testdataDir, "configs", "output_with_formats.yml") + configPath := filepath.Join(tempDir, "test-config.yml") + sourcePath := filepath.Join(testdataDir, "output.go") + cliJsonPath := filepath.Join(tempDir, "cli_result.json") + cliHtmlPath := filepath.Join(tempDir, "cli_result.html") + + configJsonOutput := filepath.Join(tempDir, "config-json-output.json") + configHtmlOutput := filepath.Join(tempDir, "config-html-output.html") + + // Read template config and replace placeholder with temp directory + configTemplate, err := os.ReadFile(configTemplatePath) + require.NoError(t, err) + + configContent := strings.ReplaceAll(string(configTemplate), "{{TEMPDIR}}", tempDir) + + // Write the modified config + err = os.WriteFile(configPath, []byte(configContent), 0o400) + require.NoError(t, err) + + testshared.NewRunnerBuilder(t). + WithArgs( + "--default=none", + fmt.Sprintf("--config=%s", configPath), + "--clear-config-outputs", + fmt.Sprintf("--output.json.path=%s", cliJsonPath), + fmt.Sprintf("--output.html.path=%s", cliHtmlPath), + ). + WithDirectives(sourcePath). + WithTargetPath(sourcePath). + Runner(). + Install(). + Run(). + ExpectExitCode(exitcodes.IssuesFound) + + // Verify CLI outputs were created + _, err = os.Stat(cliJsonPath) + require.NoError(t, err, "CLI JSON output should exist") + + _, err = os.Stat(cliHtmlPath) + require.NoError(t, err, "CLI HTML output should exist") + + // Verify config-specified files were NOT created + _, err = os.Stat(configJsonOutput) + require.True(t, os.IsNotExist(err), "JSON output from config should not exist") + + _, err = os.Stat(configHtmlOutput) + require.True(t, os.IsNotExist(err), "HTML output from config should not exist") +} diff --git a/test/testdata/configs/output_with_formats.yml b/test/testdata/configs/output_with_formats.yml new file mode 100644 index 000000000000..70d1445ebff3 --- /dev/null +++ b/test/testdata/configs/output_with_formats.yml @@ -0,0 +1,19 @@ +version: "2" + +linters: + settings: + misspell: + locale: US + ignore-rules: + - langauge + - Dialogue + +output: + formats: + json: + path: '{{TEMPDIR}}/config-json-output.json' + html: + path: '{{TEMPDIR}}/config-html-output.html' + +run: + relative-path-mode: wd