diff --git a/.golangci.example.yml b/.golangci.example.yml index 28e2715d897f..be5b865cf21e 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -72,6 +72,9 @@ output: # make issues output unique by line, default is true uniq-by-line: true + # add a prefix to the output file references; default is no prefix + path-prefix: "" + # all available settings of specific linters linters-settings: diff --git a/pkg/commands/run.go b/pkg/commands/run.go index e4887d587a50..31f52fa78c36 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -81,6 +81,7 @@ func initFlagSet(fs *pflag.FlagSet, cfg *config.Config, m *lintersdb.Manager, is fs.BoolVar(&oc.PrintLinterName, "print-linter-name", true, wh("Print linter name in issue line")) fs.BoolVar(&oc.UniqByLine, "uniq-by-line", true, wh("Make issues output unique by line")) fs.BoolVar(&oc.PrintWelcomeMessage, "print-welcome", false, wh("Print welcome message")) + fs.StringVar(&oc.PathPrefix, "path-prefix", "", wh("Path prefix to add to output")) hideFlag("print-welcome") // no longer used // Run config diff --git a/pkg/config/config.go b/pkg/config/config.go index 3afd40406b08..5717bb669d3f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -516,10 +516,11 @@ type Config struct { Output struct { Format string Color string - PrintIssuedLine bool `mapstructure:"print-issued-lines"` - PrintLinterName bool `mapstructure:"print-linter-name"` - UniqByLine bool `mapstructure:"uniq-by-line"` - PrintWelcomeMessage bool `mapstructure:"print-welcome"` + PrintIssuedLine bool `mapstructure:"print-issued-lines"` + PrintLinterName bool `mapstructure:"print-linter-name"` + UniqByLine bool `mapstructure:"uniq-by-line"` + PrintWelcomeMessage bool `mapstructure:"print-welcome"` + PathPrefix string `mapstructure:"path-prefix"` } LintersSettings LintersSettings `mapstructure:"linters-settings"` diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index f77778b286f9..14baecc06c01 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -79,6 +79,7 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint processors.NewSourceCode(lineCache, log.Child("source_code")), processors.NewPathShortener(), getSeverityRulesProcessor(&cfg.Severity, log, lineCache), + processors.NewPathPrefixer(cfg.Output.PathPrefix), }, Log: log, }, nil diff --git a/pkg/result/processors/path_prefixer.go b/pkg/result/processors/path_prefixer.go new file mode 100644 index 000000000000..5ce940b39bf0 --- /dev/null +++ b/pkg/result/processors/path_prefixer.go @@ -0,0 +1,37 @@ +package processors + +import ( + "path" + + "github.com/golangci/golangci-lint/pkg/result" +) + +// PathPrefixer adds a customizable prefix to every output path +type PathPrefixer struct { + prefix string +} + +var _ Processor = new(PathPrefixer) + +// NewPathPrefixer returns a new path prefixer for the provided string +func NewPathPrefixer(prefix string) *PathPrefixer { + return &PathPrefixer{prefix: prefix} +} + +// Name returns the name of this processor +func (*PathPrefixer) Name() string { + return "path_prefixer" +} + +// Process adds the prefix to each path +func (p *PathPrefixer) Process(issues []result.Issue) ([]result.Issue, error) { + if p.prefix != "" { + for i := range issues { + issues[i].Pos.Filename = path.Join(p.prefix, issues[i].Pos.Filename) + } + } + return issues, nil +} + +// Finish is implemented to satisfy the Processor interface +func (*PathPrefixer) Finish() {} diff --git a/pkg/result/processors/path_prefixer_test.go b/pkg/result/processors/path_prefixer_test.go new file mode 100644 index 000000000000..e4b4c86e30af --- /dev/null +++ b/pkg/result/processors/path_prefixer_test.go @@ -0,0 +1,37 @@ +package processors + +import ( + "go/token" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/golangci/golangci-lint/pkg/result" +) + +func TestPathPrefixer_Process(t *testing.T) { + paths := func(ps ...string) (issues []result.Issue) { + for _, p := range ps { + issues = append(issues, result.Issue{Pos: token.Position{Filename: p}}) + } + return + } + for _, tt := range []struct { + name, prefix string + issues, want []result.Issue + }{ + {"empty prefix", "", paths("some/path", "cool"), paths("some/path", "cool")}, + {"prefix", "ok", paths("some/path", "cool"), paths("ok/some/path", "ok/cool")}, + {"prefix slashed", "ok/", paths("some/path", "cool"), paths("ok/some/path", "ok/cool")}, + } { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + p := NewPathPrefixer(tt.prefix) //nolint:scopelint + got, err := p.Process(tt.issues) //nolint:scopelint + r.NoError(err, "prefixer should never error") + + r.Equal(got, tt.want) //nolint:scopelint + }) + } +} diff --git a/test/run_test.go b/test/run_test.go index e20b54fa1aca..878a273bca1b 100644 --- a/test/run_test.go +++ b/test/run_test.go @@ -292,3 +292,22 @@ func TestDisallowedOptionsInConfig(t *testing.T) { r.RunWithYamlConfig(c.cfg, withCommonRunArgs(args...)...).ExpectExitCode(exitcodes.Failure) } } + +func TestPathPrefix(t *testing.T) { + for _, tt := range []struct { + Name string + Args []string + Pattern string + }{ + {"empty", nil, "^testdata/withTests/"}, + {"prefixed", []string{"--path-prefix=cool"}, "^cool/testdata/withTests"}, + } { + t.Run(tt.Name, func(t *testing.T) { + testshared.NewLintRunner(t).Run( + append(tt.Args, getTestDataDir("withTests"))..., //nolint:scopelint + ).ExpectOutputRegexp( + tt.Pattern, //nolint:scopelint + ) + }) + } +} diff --git a/test/testshared/testshared.go b/test/testshared/testshared.go index 8fd6185038ab..8effe2bad0bf 100644 --- a/test/testshared/testshared.go +++ b/test/testshared/testshared.go @@ -66,6 +66,12 @@ func (r *RunResult) ExpectExitCode(possibleCodes ...int) *RunResult { return r } +// ExpectOutputRegexp can be called with either a string or compiled regexp +func (r *RunResult) ExpectOutputRegexp(s interface{}) *RunResult { + assert.Regexp(r.t, s, r.output, "exit code is %d", r.exitCode) + return r +} + func (r *RunResult) ExpectOutputContains(s string) *RunResult { assert.Contains(r.t, r.output, s, "exit code is %d", r.exitCode) return r