feat: new output.formats file configuration syntax (#4521)
This commit is contained in:
parent
b91c194126
commit
e3ed3ba1d6
@ -59,15 +59,25 @@ run:
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
# Format: colored-line-number|line-number|json|colored-tab|tab|checkstyle|code-climate|junit-xml|github-actions|teamcity
|
||||
#
|
||||
# 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.
|
||||
# The formats used to render issues.
|
||||
# Format: `colored-line-number`, `line-number`, `json`, `colored-tab`, `tab`, `checkstyle`, `code-climate`, `junit-xml`, `github-actions`, `teamcity`
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Example: "checkstyle:report.xml,json:stdout,colored-line-number"
|
||||
#
|
||||
# Default: colored-line-number
|
||||
format: json
|
||||
# For the CLI flag (`--out-format`), multiple formats can be specified by separating them by comma.
|
||||
# The output can be specified for each of them by separating format name and path by colon symbol.
|
||||
# Example: "--out-format=checkstyle:report.xml,json:stdout,colored-line-number"
|
||||
# The CLI flag (`--out-format`) override the configuration file.
|
||||
#
|
||||
# Default:
|
||||
# formats:
|
||||
# - format: colored-line-number
|
||||
# path: stdout
|
||||
formats:
|
||||
- format: json
|
||||
path: stderr
|
||||
- format: checkstyle
|
||||
path: report.xml
|
||||
- format: colored-line-number
|
||||
|
||||
# Print lines of code with issue.
|
||||
# Default: true
|
||||
|
@ -451,16 +451,43 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"format": {
|
||||
"description": "Output format to use.",
|
||||
"pattern": "^(,?(colored-line-number|line-number|json|colored-tab|tab|checkstyle|code-climate|junit-xml|github-actions|teamcity)(:[^,]+)?)+$",
|
||||
"default": "colored-line-number",
|
||||
"examples": [
|
||||
"colored-line-number",
|
||||
"checkstyle:report.json,colored-line-number",
|
||||
"line-number:golangci-lint.out,colored-line-number:stdout"
|
||||
"formats": {
|
||||
"description": "Output formats to use.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": {
|
||||
"default": "stdout",
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [ "stdout", "stderr" ]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"default": "colored-line-number",
|
||||
"enum": [
|
||||
"colored-line-number",
|
||||
"line-number",
|
||||
"json",
|
||||
"colored-tab",
|
||||
"tab",
|
||||
"checkstyle",
|
||||
"code-climate",
|
||||
"junit-xml",
|
||||
"github-actions",
|
||||
"teamcity"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["format"]
|
||||
}
|
||||
},
|
||||
"print-issued-lines": {
|
||||
"description": "Print lines of code with issue.",
|
||||
"type": "boolean",
|
||||
|
@ -63,8 +63,8 @@ func setupRunFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
func setupOutputFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
|
||||
internal.AddFlagAndBind(v, fs, fs.String, "out-format", "output.format", config.OutFormatColoredLineNumber,
|
||||
color.GreenString(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|"))))
|
||||
internal.AddFlagAndBind(v, fs, fs.String, "out-format", "output.formats", config.OutFormatColoredLineNumber,
|
||||
color.GreenString(fmt.Sprintf("Formats of output: %s", strings.Join(config.AllOutputFormats, "|"))))
|
||||
internal.AddFlagAndBind(v, fs, fs.Bool, "print-issued-lines", "output.print-issued-lines", true,
|
||||
color.GreenString("Print lines of code with issue"))
|
||||
internal.AddFlagAndBind(v, fs, fs.Bool, "print-linter-name", "output.print-linter-name", true,
|
||||
|
@ -61,7 +61,10 @@ func (l *Loader) Load() error {
|
||||
|
||||
l.handleGoVersion()
|
||||
|
||||
l.handleDeprecation()
|
||||
err = l.handleDeprecation()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = l.handleEnableOnlyOption()
|
||||
if err != nil {
|
||||
@ -164,7 +167,7 @@ func (l *Loader) parseConfig() error {
|
||||
var configFileNotFoundError viper.ConfigFileNotFoundError
|
||||
if errors.As(err, &configFileNotFoundError) {
|
||||
// Load configuration from flags only.
|
||||
err = l.viper.Unmarshal(l.cfg)
|
||||
err = l.viper.Unmarshal(l.cfg, customDecoderHook())
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't unmarshal config by viper (flags): %w", err)
|
||||
}
|
||||
@ -181,7 +184,7 @@ func (l *Loader) parseConfig() error {
|
||||
}
|
||||
|
||||
// Load configuration from all sources (flags, file).
|
||||
if err := l.viper.Unmarshal(l.cfg, fileDecoderHook()); err != nil {
|
||||
if err := l.viper.Unmarshal(l.cfg, customDecoderHook()); err != nil {
|
||||
return fmt.Errorf("can't unmarshal config by viper (flags, file): %w", err)
|
||||
}
|
||||
|
||||
@ -279,28 +282,47 @@ func (l *Loader) handleGoVersion() {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loader) handleDeprecation() {
|
||||
func (l *Loader) handleDeprecation() error {
|
||||
// Deprecated since v1.57.0
|
||||
if len(l.cfg.Run.SkipFiles) > 0 {
|
||||
l.warn("The configuration option `run.skip-files` is deprecated, please use `issues.exclude-files`.")
|
||||
l.cfg.Issues.ExcludeFiles = l.cfg.Run.SkipFiles
|
||||
}
|
||||
|
||||
// Deprecated since v1.57.0
|
||||
if len(l.cfg.Run.SkipDirs) > 0 {
|
||||
l.warn("The configuration option `run.skip-dirs` is deprecated, please use `issues.exclude-dirs`.")
|
||||
l.cfg.Issues.ExcludeDirs = l.cfg.Run.SkipDirs
|
||||
}
|
||||
|
||||
// The 2 options are true by default.
|
||||
// Deprecated since v1.57.0
|
||||
if !l.cfg.Run.UseDefaultSkipDirs {
|
||||
l.warn("The configuration option `run.skip-dirs-use-default` is deprecated, please use `issues.exclude-dirs-use-default`.")
|
||||
}
|
||||
l.cfg.Issues.UseDefaultExcludeDirs = l.cfg.Run.UseDefaultSkipDirs && l.cfg.Issues.UseDefaultExcludeDirs
|
||||
|
||||
// The 2 options are false by default.
|
||||
// Deprecated since v1.57.0
|
||||
if l.cfg.Run.ShowStats {
|
||||
l.warn("The configuration option `run.show-stats` is deprecated, please use `output.show-stats`")
|
||||
}
|
||||
l.cfg.Output.ShowStats = l.cfg.Run.ShowStats || l.cfg.Output.ShowStats
|
||||
|
||||
// Deprecated since v1.57.0
|
||||
if l.cfg.Output.Format != "" {
|
||||
l.warn("The configuration option `output.format` is deprecated, please use `output.formats`")
|
||||
|
||||
var f OutputFormats
|
||||
err := f.UnmarshalText([]byte(l.cfg.Output.Format))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal output format: %w", err)
|
||||
}
|
||||
|
||||
l.cfg.Output.Formats = f
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Loader) handleEnableOnlyOption() error {
|
||||
@ -332,13 +354,13 @@ func (l *Loader) warn(format string) {
|
||||
l.log.Warnf(format)
|
||||
}
|
||||
|
||||
func fileDecoderHook() viper.DecoderConfigOption {
|
||||
func customDecoderHook() viper.DecoderConfigOption {
|
||||
return viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
|
||||
// Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138).
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
|
||||
// Needed for forbidigo.
|
||||
// Needed for forbidigo, and output.formats.
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
))
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ const (
|
||||
OutFormatTeamCity = "teamcity"
|
||||
)
|
||||
|
||||
var OutFormats = []string{
|
||||
var AllOutputFormats = []string{
|
||||
OutFormatColoredLineNumber,
|
||||
OutFormatLineNumber,
|
||||
OutFormatJSON,
|
||||
@ -35,7 +35,7 @@ var OutFormats = []string{
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Format string `mapstructure:"format"`
|
||||
Formats OutputFormats `mapstructure:"formats"`
|
||||
PrintIssuedLine bool `mapstructure:"print-issued-lines"`
|
||||
PrintLinterName bool `mapstructure:"print-linter-name"`
|
||||
UniqByLine bool `mapstructure:"uniq-by-line"`
|
||||
@ -43,6 +43,9 @@ type Output struct {
|
||||
SortOrder []string `mapstructure:"sort-order"`
|
||||
PathPrefix string `mapstructure:"path-prefix"`
|
||||
ShowStats bool `mapstructure:"show-stats"`
|
||||
|
||||
// Deprecated: use Formats instead.
|
||||
Format string `mapstructure:"format"`
|
||||
}
|
||||
|
||||
func (o *Output) Validate() error {
|
||||
@ -64,5 +67,46 @@ func (o *Output) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, format := range o.Formats {
|
||||
err := format.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type OutputFormat struct {
|
||||
Format string `mapstructure:"format"`
|
||||
Path string `mapstructure:"path"`
|
||||
}
|
||||
|
||||
func (o *OutputFormat) Validate() error {
|
||||
if o.Format == "" {
|
||||
return errors.New("the format is required")
|
||||
}
|
||||
|
||||
if !slices.Contains(AllOutputFormats, o.Format) {
|
||||
return fmt.Errorf("unsupported output format %q", o.Format)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type OutputFormats []OutputFormat
|
||||
|
||||
func (p *OutputFormats) UnmarshalText(text []byte) error {
|
||||
formats := strings.Split(string(text), ",")
|
||||
|
||||
for _, item := range formats {
|
||||
format, path, _ := strings.Cut(item, ":")
|
||||
|
||||
*p = append(*p, OutputFormat{
|
||||
Path: path,
|
||||
Format: format,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -81,6 +81,86 @@ func TestOutput_Validate_error(t *testing.T) {
|
||||
},
|
||||
expected: `the sort-order name "linter" is repeated several times`,
|
||||
},
|
||||
{
|
||||
desc: "unsupported format",
|
||||
settings: &Output{
|
||||
Formats: []OutputFormat{
|
||||
{
|
||||
Format: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `unsupported output format "test"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.settings.Validate()
|
||||
require.EqualError(t, err, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputFormat_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
settings *OutputFormat
|
||||
}{
|
||||
{
|
||||
desc: "only format",
|
||||
settings: &OutputFormat{
|
||||
Format: "json",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "format and path (relative)",
|
||||
settings: &OutputFormat{
|
||||
Format: "json",
|
||||
Path: "./example.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "format and path (absolute)",
|
||||
settings: &OutputFormat{
|
||||
Format: "json",
|
||||
Path: "/tmp/example.json",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.settings.Validate()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputFormat_Validate_error(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
settings *OutputFormat
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
settings: &OutputFormat{},
|
||||
expected: "the format is required",
|
||||
},
|
||||
{
|
||||
desc: "unsupported format",
|
||||
settings: &OutputFormat{
|
||||
Format: "test",
|
||||
},
|
||||
expected: `unsupported output format "test"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
"github.com/golangci/golangci-lint/pkg/logutils"
|
||||
@ -54,11 +53,8 @@ func NewPrinter(log logutils.Log, cfg *config.Output, reportData *report.Data) (
|
||||
|
||||
// Print prints issues based on the formats defined
|
||||
func (c *Printer) Print(issues []result.Issue) error {
|
||||
formats := strings.Split(c.cfg.Format, ",")
|
||||
|
||||
for _, item := range formats {
|
||||
format, path, _ := strings.Cut(item, ":")
|
||||
err := c.printReports(issues, path, format)
|
||||
for _, format := range c.cfg.Formats {
|
||||
err := c.printReports(issues, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -67,10 +63,10 @@ func (c *Printer) Print(issues []result.Issue) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Printer) printReports(issues []result.Issue, path, format string) error {
|
||||
w, shouldClose, err := c.createWriter(path)
|
||||
func (c *Printer) printReports(issues []result.Issue, format config.OutputFormat) error {
|
||||
w, shouldClose, err := c.createWriter(format.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create output for %s: %w", path, err)
|
||||
return fmt.Errorf("can't create output for %s: %w", format.Path, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
@ -79,7 +75,7 @@ func (c *Printer) printReports(issues []result.Issue, path, format string) error
|
||||
}
|
||||
}()
|
||||
|
||||
p, err := c.createPrinter(format, w)
|
||||
p, err := c.createPrinter(format.Format, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -140,7 +136,7 @@ func (c *Printer) createPrinter(format string, w io.Writer) (issuePrinter, error
|
||||
case config.OutFormatTeamCity:
|
||||
p = NewTeamCity(w)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown output format %s", format)
|
||||
return nil, fmt.Errorf("unknown output format %q", format)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
|
@ -43,14 +43,21 @@ func TestPrinter_Print_stdout(t *testing.T) {
|
||||
{
|
||||
desc: "stdout (implicit)",
|
||||
cfg: &config.Output{
|
||||
Format: "line-number",
|
||||
Formats: []config.OutputFormat{
|
||||
{Format: "line-number"},
|
||||
},
|
||||
},
|
||||
expected: "golden-line-number.txt",
|
||||
},
|
||||
{
|
||||
desc: "stdout (explicit)",
|
||||
cfg: &config.Output{
|
||||
Format: "line-number:stdout",
|
||||
Formats: []config.OutputFormat{
|
||||
{
|
||||
Format: "line-number",
|
||||
Path: "stdout",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "golden-line-number.txt",
|
||||
},
|
||||
@ -92,7 +99,12 @@ func TestPrinter_Print_stderr(t *testing.T) {
|
||||
unmarshalFile(t, "in-report-data.json", data)
|
||||
|
||||
cfg := &config.Output{
|
||||
Format: "line-number:stderr",
|
||||
Formats: []config.OutputFormat{
|
||||
{
|
||||
Format: "line-number",
|
||||
Path: "stderr",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := NewPrinter(logger, cfg, data)
|
||||
@ -126,7 +138,12 @@ func TestPrinter_Print_file(t *testing.T) {
|
||||
outputPath := filepath.Join(t.TempDir(), "report.txt")
|
||||
|
||||
cfg := &config.Output{
|
||||
Format: "line-number:" + outputPath,
|
||||
Formats: []config.OutputFormat{
|
||||
{
|
||||
Format: "line-number",
|
||||
Path: outputPath,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := NewPrinter(logger, cfg, data)
|
||||
@ -165,9 +182,20 @@ func TestPrinter_Print_multiple(t *testing.T) {
|
||||
outputPath := filepath.Join(t.TempDir(), "github-actions.txt")
|
||||
|
||||
cfg := &config.Output{
|
||||
Format: "github-actions:" + outputPath +
|
||||
",json" +
|
||||
",line-number:stderr",
|
||||
Formats: []config.OutputFormat{
|
||||
{
|
||||
Format: "github-actions",
|
||||
Path: outputPath,
|
||||
},
|
||||
{
|
||||
Format: "json",
|
||||
Path: "",
|
||||
},
|
||||
{
|
||||
Format: "line-number",
|
||||
Path: "stderr",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := NewPrinter(logger, cfg, data)
|
||||
|
Loading…
x
Reference in New Issue
Block a user