diff --git a/.golangci.example.yml b/.golangci.example.yml index 32572982..dcd760fc 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -5,6 +5,10 @@ run: tests: true build-tags: - mytag + skip-dirs: + - external_libs + skip-files: + - ".*\\.pb\\.go$" output: format: colored-line-number diff --git a/README.md b/README.md index 2bc6a12d..a2324be3 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,8 @@ Flags: --print-resources-usage Print avg and max memory usage of golangci-lint and total time -c, --config PATH Read config from file path PATH --no-config Don't read config + --skip-dirs strings Regexps of directory names to skip + --skip-files strings Regexps of file names to skip -E, --enable strings Enable specific linter -D, --disable strings Disable specific linter --enable-all Enable all linters diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 97b39374..32c90694 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -71,6 +71,8 @@ func initFlagSet(fs *pflag.FlagSet, cfg *config.Config) { fs.BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false, wh("Print avg and max memory usage of golangci-lint and total time")) fs.StringVarP(&rc.Config, "config", "c", "", wh("Read config from file path `PATH`")) fs.BoolVar(&rc.NoConfig, "no-config", false, wh("Don't read config")) + fs.StringSliceVar(&rc.SkipDirs, "skip-dirs", nil, wh("Regexps of directory names to skip")) + fs.StringSliceVar(&rc.SkipFiles, "skip-files", nil, wh("Regexps of file names to skip")) // Linters settings config lsc := &cfg.LintersSettings @@ -194,12 +196,21 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan resul if lintCtx.Program != nil { fset = lintCtx.Program.Fset } + + skipFilesProcessor, err := processors.NewSkipFiles(e.cfg.Run.SkipFiles) + if err != nil { + return nil, err + } + runner := lint.SimpleRunner{ Processors: []processors.Processor{ processors.NewPathPrettifier(), // must be before diff processor at least - processors.NewExclude(excludeTotalPattern), processors.NewCgo(), + skipFilesProcessor, + + processors.NewExclude(excludeTotalPattern), processors.NewNolint(fset), + processors.NewUniqByLine(), processors.NewDiff(e.cfg.Issues.Diff, e.cfg.Issues.DiffFromRevision, e.cfg.Issues.DiffPatchFilePath), processors.NewMaxPerFileFromLinter(), diff --git a/pkg/config/config.go b/pkg/config/config.go index ab172d15..30cff3fb 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -101,6 +101,9 @@ type Run struct { AnalyzeTests bool `mapstructure:"tests"` Deadline time.Duration PrintVersion bool + + SkipFiles []string `mapstructure:"skip-files"` + SkipDirs []string `mapstructure:"skip-dirs"` } type LintersSettings struct { diff --git a/pkg/fsutils/fsutils.go b/pkg/fsutils/fsutils.go index 3de4d7ef..561f9ffd 100644 --- a/pkg/fsutils/fsutils.go +++ b/pkg/fsutils/fsutils.go @@ -13,7 +13,12 @@ import ( "github.com/sirupsen/logrus" ) -var stdExcludeDirs = []string{"vendor", "testdata", "examples", "Godeps", "builtin"} +var stdExcludeDirRegexps = []string{ + "^vendor$", "^third_party$", + "^testdata$", "^examples$", + "^Godeps$", + "^builtin$", +} func GetProjectRoot() string { return path.Join(build.Default.GOPATH, "src", "github.com", "golangci", "golangci-worker") @@ -101,7 +106,7 @@ func processResolvedPaths(paths *PathResolveResult) (*ProjectPaths, error) { }, nil } -func GetPathsForAnalysis(ctx context.Context, inputPaths []string, includeTests bool) (ret *ProjectPaths, err error) { +func GetPathsForAnalysis(ctx context.Context, inputPaths []string, includeTests bool, skipDirRegexps []string) (ret *ProjectPaths, err error) { defer func(startedAt time.Time) { if ret != nil { logrus.Infof("Found paths for analysis for %s: %s", time.Since(startedAt), ret.MixedPaths()) @@ -114,7 +119,13 @@ func GetPathsForAnalysis(ctx context.Context, inputPaths []string, includeTests } } - pr := NewPathResolver(stdExcludeDirs, []string{".go"}, includeTests) + // TODO: don't analyze skipped files also, when be able to do it + excludeDirs := append([]string{}, stdExcludeDirRegexps...) + excludeDirs = append(excludeDirs, skipDirRegexps...) + pr, err := NewPathResolver(excludeDirs, []string{".go"}, includeTests) + if err != nil { + return nil, fmt.Errorf("can't make path resolver: %s", err) + } paths, err := pr.Resolve(inputPaths...) if err != nil { return nil, fmt.Errorf("can't resolve paths %v: %s", inputPaths, err) diff --git a/pkg/fsutils/path_resolver.go b/pkg/fsutils/path_resolver.go index 9bef6633..9c77287d 100644 --- a/pkg/fsutils/path_resolver.go +++ b/pkg/fsutils/path_resolver.go @@ -4,12 +4,13 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sort" "strings" ) type PathResolver struct { - excludeDirs map[string]bool + excludeDirs map[string]*regexp.Regexp allowedFileExtensions map[string]bool includeTests bool } @@ -57,10 +58,15 @@ func (s pathResolveState) toResult() *PathResolveResult { return res } -func NewPathResolver(excludeDirs, allowedFileExtensions []string, includeTests bool) *PathResolver { - excludeDirsMap := map[string]bool{} +func NewPathResolver(excludeDirs, allowedFileExtensions []string, includeTests bool) (*PathResolver, error) { + excludeDirsMap := map[string]*regexp.Regexp{} for _, dir := range excludeDirs { - excludeDirsMap[dir] = true + re, err := regexp.Compile(dir) + if err != nil { + return nil, fmt.Errorf("can't compile regexp %q: %s", dir, err) + } + + excludeDirsMap[dir] = re } allowedFileExtensionsMap := map[string]bool{} @@ -72,7 +78,7 @@ func NewPathResolver(excludeDirs, allowedFileExtensions []string, includeTests b excludeDirs: excludeDirsMap, allowedFileExtensions: allowedFileExtensionsMap, includeTests: includeTests, - } + }, nil } func (pr PathResolver) isIgnoredDir(dir string) bool { @@ -87,7 +93,13 @@ func (pr PathResolver) isIgnoredDir(dir string) bool { return true } - return pr.excludeDirs[dirName] + for _, dirExludeRe := range pr.excludeDirs { + if dirExludeRe.MatchString(dirName) { + return true + } + } + + return false } func (pr PathResolver) isAllowedFile(path string) bool { diff --git a/pkg/fsutils/path_resolver_test.go b/pkg/fsutils/path_resolver_test.go index bd8e5c22..ae3d383a 100644 --- a/pkg/fsutils/path_resolver_test.go +++ b/pkg/fsutils/path_resolver_test.go @@ -53,12 +53,15 @@ func prepareFS(t *testing.T, paths ...string) *fsPreparer { } } -func newPR() *PathResolver { - return NewPathResolver([]string{}, []string{}, false) +func newPR(t *testing.T) *PathResolver { + pr, err := NewPathResolver([]string{}, []string{}, false) + assert.NoError(t, err) + + return pr } func TestPathResolverNoPaths(t *testing.T) { - _, err := newPR().Resolve() + _, err := newPR(t).Resolve() assert.EqualError(t, err, "no paths are set") } @@ -66,7 +69,7 @@ func TestPathResolverNotExistingPath(t *testing.T) { fp := prepareFS(t) defer fp.clean() - _, err := newPR().Resolve("a") + _, err := newPR(t).Resolve("a") assert.EqualError(t, err, "can't find path a: stat a: no such file or directory") } @@ -187,6 +190,11 @@ func TestPathResolverCommonCases(t *testing.T) { expDirs: []string{".", "a/c"}, expFiles: []string{"a/c/d.go", "e.go"}, }, + { + name: "vendor dir is excluded by regexp, not the exact match", + prepare: []string{"vendors/a.go", "novendor/b.go"}, + resolve: []string{"./..."}, + }, } for _, tc := range testCases { @@ -194,7 +202,9 @@ func TestPathResolverCommonCases(t *testing.T) { fp := prepareFS(t, tc.prepare...) defer fp.clean() - pr := NewPathResolver([]string{"vendor"}, []string{".go"}, tc.includeTests) + pr, err := NewPathResolver([]string{"vendor"}, []string{".go"}, tc.includeTests) + assert.NoError(t, err) + res, err := pr.Resolve(tc.resolve...) assert.NoError(t, err) diff --git a/pkg/lint/load.go b/pkg/lint/load.go index 8ca3da13..420f0b44 100644 --- a/pkg/lint/load.go +++ b/pkg/lint/load.go @@ -154,7 +154,7 @@ func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Confi args = []string{"./..."} } - paths, err := fsutils.GetPathsForAnalysis(ctx, args, cfg.Run.AnalyzeTests) + paths, err := fsutils.GetPathsForAnalysis(ctx, args, cfg.Run.AnalyzeTests, cfg.Run.SkipDirs) if err != nil { return nil, err } diff --git a/pkg/lint/load_test.go b/pkg/lint/load_test.go index edd721d1..099d028a 100644 --- a/pkg/lint/load_test.go +++ b/pkg/lint/load_test.go @@ -19,7 +19,7 @@ func TestASTCacheLoading(t *testing.T) { inputPaths := []string{"./...", "./", "./load.go", "load.go"} for _, inputPath := range inputPaths { - paths, err := fsutils.GetPathsForAnalysis(ctx, []string{inputPath}, true) + paths, err := fsutils.GetPathsForAnalysis(ctx, []string{inputPath}, true, nil) assert.NoError(t, err) assert.NotEmpty(t, paths.Files) diff --git a/pkg/result/processors/skip_files.go b/pkg/result/processors/skip_files.go new file mode 100644 index 00000000..5b9fa652 --- /dev/null +++ b/pkg/result/processors/skip_files.go @@ -0,0 +1,53 @@ +package processors + +import ( + "fmt" + "path/filepath" + "regexp" + + "github.com/golangci/golangci-lint/pkg/result" +) + +type SkipFiles struct { + patterns []*regexp.Regexp +} + +var _ Processor = SkipFiles{} + +func NewSkipFiles(patterns []string) (*SkipFiles, error) { + var patternsRe []*regexp.Regexp + for _, p := range patterns { + patternRe, err := regexp.Compile(p) + if err != nil { + return nil, fmt.Errorf("can't compile regexp %q: %s", p, err) + } + patternsRe = append(patternsRe, patternRe) + } + + return &SkipFiles{ + patterns: patternsRe, + }, nil +} + +func (p SkipFiles) Name() string { + return "skip_files" +} + +func (p SkipFiles) Process(issues []result.Issue) ([]result.Issue, error) { + if len(p.patterns) == 0 { + return issues, nil + } + + return filterIssues(issues, func(i *result.Issue) bool { + fileName := filepath.Base(i.FilePath()) + for _, p := range p.patterns { + if p.MatchString(fileName) { + return false + } + } + + return true + }), nil +} + +func (p SkipFiles) Finish() {} diff --git a/pkg/result/processors/skip_files_test.go b/pkg/result/processors/skip_files_test.go new file mode 100644 index 00000000..b416cab9 --- /dev/null +++ b/pkg/result/processors/skip_files_test.go @@ -0,0 +1,43 @@ +package processors + +import ( + "go/token" + "testing" + + "github.com/golangci/golangci-lint/pkg/result" + "github.com/stretchr/testify/assert" +) + +func newFileIssue(file string) result.Issue { + return result.Issue{ + Pos: token.Position{ + Filename: file, + }, + } +} + +func newTestSkipFiles(t *testing.T, patterns ...string) *SkipFiles { + p, err := NewSkipFiles(patterns) + assert.NoError(t, err) + return p +} + +func TestSkipFiles(t *testing.T) { + p := newTestSkipFiles(t) + processAssertSame(t, p, newFileIssue("any.go")) + + p = newTestSkipFiles(t, "file") + processAssertEmpty(t, p, + newFileIssue("file.go"), + newFileIssue("file"), + newFileIssue("nofile.go")) + + p = newTestSkipFiles(t, ".*") + processAssertEmpty(t, p, newFileIssue("any.go")) +} + +func TestSkipFilesInvalidPattern(t *testing.T) { + p, err := NewSkipFiles([]string{"\\o"}) + assert.Error(t, err) + assert.Nil(t, p) +}