mirror of
				https://github.com/mgechev/revive.git
				synced 2025-10-30 23:37:49 +02:00 
			
		
		
		
	
							
								
								
									
										31
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								README.md
									
									
									
									
									
								
							| @@ -425,6 +425,37 @@ warningCode = 0 | ||||
| [rule.redefines-builtin-id] | ||||
| ``` | ||||
|  | ||||
| ### Rule-level file excludes | ||||
|  | ||||
| You also can setup custom excludes for each rule. | ||||
|  | ||||
| It's alternative for global `-exclude` program arg. | ||||
|  | ||||
| ```toml | ||||
| ignoreGeneratedHeader = false | ||||
| severity = "warning" | ||||
| confidence = 0.8 | ||||
| errorCode = 0 | ||||
| warningCode = 0 | ||||
|  | ||||
| [rule.blank-imports] | ||||
|    Exclude=["**/*.pb.go"] | ||||
| [rule.context-as-argument] | ||||
|    Exclude=["src/somepkg/*.go", "TEST"] | ||||
| ``` | ||||
|  | ||||
| You can use following exclude patterns | ||||
|  | ||||
| 1. full paths to files `src/pkg/mypkg/some.go` | ||||
| 2. globs `src/**/*.pb.go` | ||||
| 3. regexes (should have prefix ~) `~\.(pb|auto|generated)\.go$` | ||||
| 4. well-known `TEST` (same as `**/*_test.go`) | ||||
| 5. special cases: | ||||
|   a. `*` and `~` patterns exclude all files (same effect than disabling the rule) | ||||
|   b. `""` (empty) pattern excludes nothing | ||||
|  | ||||
| > NOTE: do not mess with `exclude` that can  be used at top level of TOML file, that mean "exclude package patterns", not "exclude file patterns" | ||||
|  | ||||
| ## Available Rules | ||||
|  | ||||
| List of all available rules. The rules ported from `golint` are left unchanged and indicated in the `golint` column. | ||||
|   | ||||
| @@ -149,6 +149,14 @@ func parseConfig(path string, config *lint.Config) error { | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("cannot parse the config file: %v", err) | ||||
| 	} | ||||
| 	for k, r := range config.Rules { | ||||
| 		err := r.Initialize() | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error in config of rule [%s] : [%v]", k, err) | ||||
| 		} | ||||
| 		config.Rules[k] = r | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -61,6 +61,27 @@ func TestGetConfig(t *testing.T) { | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	t.Run("rule-level file filter excludes", func(t *testing.T) { | ||||
| 		cfg, err := GetConfig("testdata/rule-level-exclude-850.toml") | ||||
| 		if err != nil { | ||||
| 			t.Fatal("should be valid config") | ||||
| 		} | ||||
| 		r1 := cfg.Rules["r1"] | ||||
| 		if len(r1.Exclude) > 0 { | ||||
| 			t.Fatal("r1 should have empty excludes") | ||||
| 		} | ||||
| 		r2 := cfg.Rules["r2"] | ||||
| 		if len(r2.Exclude) != 1 { | ||||
| 			t.Fatal("r2 should have exclude set") | ||||
| 		} | ||||
| 		if !r2.MustExclude("some/file.go") { | ||||
| 			t.Fatal("r2 should be initialized and exclude some/file.go") | ||||
| 		} | ||||
| 		if r2.MustExclude("some/any-other.go") { | ||||
| 			t.Fatal("r2 should not exclude some/any-other.go") | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestGetLintingRules(t *testing.T) { | ||||
|   | ||||
							
								
								
									
										13
									
								
								config/testdata/rule-level-exclude-850.toml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								config/testdata/rule-level-exclude-850.toml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| ignoreGeneratedHeader = false | ||||
| severity = "warning" | ||||
| confidence = 0.8 | ||||
| errorCode = 0 | ||||
| warningCode = 0 | ||||
|  | ||||
| enableAllRules = false | ||||
|  | ||||
| [rule.r1] | ||||
|     # no excludes | ||||
|  | ||||
| [rule.r2] | ||||
|     exclude=["some/file.go"] | ||||
| @@ -3,16 +3,44 @@ package lint | ||||
| // Arguments is type used for the arguments of a rule. | ||||
| type Arguments = []interface{} | ||||
|  | ||||
| type FileFilters = []*FileFilter | ||||
|  | ||||
| // RuleConfig is type used for the rule configuration. | ||||
| type RuleConfig struct { | ||||
| 	Arguments Arguments | ||||
| 	Severity  Severity | ||||
| 	Disabled  bool | ||||
| 	// Exclude - rule-level file excludes, TOML related (strings) | ||||
| 	Exclude []string | ||||
| 	// excludeFilters - regex-based file filters, initialized from Exclude | ||||
| 	excludeFilters []*FileFilter | ||||
| } | ||||
|  | ||||
| // Initialize - should be called after reading from TOML file | ||||
| func (rc *RuleConfig) Initialize() error { | ||||
| 	for _, f := range rc.Exclude { | ||||
| 		ff, err := ParseFileFilter(f) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		rc.excludeFilters = append(rc.excludeFilters, ff) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // RulesConfig defines the config for all rules. | ||||
| type RulesConfig = map[string]RuleConfig | ||||
|  | ||||
| // MustExclude - checks if given filename `name` must be excluded | ||||
| func (rcfg *RuleConfig) MustExclude(name string) bool { | ||||
| 	for _, exclude := range rcfg.excludeFilters { | ||||
| 		if exclude.MatchFileName(name) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // DirectiveConfig is type used for the linter directive configuration. | ||||
| type DirectiveConfig struct { | ||||
| 	Severity Severity | ||||
|   | ||||
| @@ -102,6 +102,9 @@ func (f *File) lint(rules []Rule, config Config, failures chan Failure) { | ||||
| 	disabledIntervals := f.disabledIntervals(rules, mustSpecifyDisableReason, failures) | ||||
| 	for _, currentRule := range rules { | ||||
| 		ruleConfig := rulesConfig[currentRule.Name()] | ||||
| 		if ruleConfig.MustExclude(f.Name) { | ||||
| 			continue | ||||
| 		} | ||||
| 		currentFailures := currentRule.Apply(f, ruleConfig.Arguments) | ||||
| 		for idx, failure := range currentFailures { | ||||
| 			if failure.RuleName == "" { | ||||
|   | ||||
							
								
								
									
										128
									
								
								lint/filefilter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								lint/filefilter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| package lint | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // FileFilter - file filter to exclude some files for rule | ||||
| // supports whole | ||||
| // 1. file/dir names : pkg/mypkg/my.go, | ||||
| // 2. globs: **/*.pb.go, | ||||
| // 3. regexes (~ prefix) ~-tmp\.\d+\.go | ||||
| // 4. special test marker `TEST` - treats as `~_test\.go` | ||||
| type FileFilter struct { | ||||
| 	// raw definition of filter inside config | ||||
| 	raw string | ||||
| 	// don't care what was at start, will use regexes inside | ||||
| 	rx *regexp.Regexp | ||||
| 	// marks filter as matching everything | ||||
| 	matchesAll bool | ||||
| 	// marks filter as matching nothing | ||||
| 	matchesNothing bool | ||||
| } | ||||
|  | ||||
| // ParseFileFilter - creates [FileFilter] for given raw filter | ||||
| // if empty string, it matches nothing | ||||
| // if `*`, or `~`, it matches everything | ||||
| // while regexp could be invalid, it could return it's compilation error | ||||
| func ParseFileFilter(rawFilter string) (*FileFilter, error) { | ||||
| 	rawFilter = strings.TrimSpace(rawFilter) | ||||
| 	result := new(FileFilter) | ||||
| 	result.raw = rawFilter | ||||
| 	result.matchesNothing = len(result.raw) == 0 | ||||
| 	result.matchesAll = result.raw == "*" || result.raw == "~" | ||||
| 	if !result.matchesAll && !result.matchesNothing { | ||||
| 		if err := result.prepareRegexp(); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| func (ff *FileFilter) String() string { return ff.raw } | ||||
|  | ||||
| // MatchFileName - checks if file name matches filter | ||||
| func (ff *FileFilter) MatchFileName(name string) bool { | ||||
| 	if ff.matchesAll { | ||||
| 		return true | ||||
| 	} | ||||
| 	if ff.matchesNothing { | ||||
| 		return false | ||||
| 	} | ||||
| 	name = strings.ReplaceAll(name, "\\", "/") | ||||
| 	return ff.rx.MatchString(name) | ||||
| } | ||||
|  | ||||
| var fileFilterInvalidGlobRegexp = regexp.MustCompile(`[^/]\*\*[^/]`) | ||||
| var escapeRegexSymbols = ".+{}()[]^$" | ||||
|  | ||||
| func (ff *FileFilter) prepareRegexp() error { | ||||
| 	var err error | ||||
| 	var src = ff.raw | ||||
| 	if src == "TEST" { | ||||
| 		src = "~_test\\.go" | ||||
| 	} | ||||
| 	if strings.HasPrefix(src, "~") { | ||||
| 		ff.rx, err = regexp.Compile(src[1:]) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("invalid file filter [%s], regexp compile error: [%v]", ff.raw, err) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 	/* globs */ | ||||
| 	if strings.Contains(src, "*") { | ||||
| 		if fileFilterInvalidGlobRegexp.MatchString(src) { | ||||
| 			return fmt.Errorf("invalid file filter [%s], invalid glob pattern", ff.raw) | ||||
| 		} | ||||
| 		var rxBuild strings.Builder | ||||
| 		rxBuild.WriteByte('^') | ||||
| 		wasStar := false | ||||
| 		justDirGlob := false | ||||
| 		for _, c := range src { | ||||
| 			if c == '*' { | ||||
| 				if wasStar { | ||||
| 					rxBuild.WriteString(`[\s\S]*`) | ||||
| 					wasStar = false | ||||
| 					justDirGlob = true | ||||
| 					continue | ||||
| 				} | ||||
| 				wasStar = true | ||||
| 				continue | ||||
| 			} | ||||
| 			if wasStar { | ||||
| 				rxBuild.WriteString("[^/]*") | ||||
| 				wasStar = false | ||||
| 			} | ||||
| 			if strings.ContainsRune(escapeRegexSymbols, c) { | ||||
| 				rxBuild.WriteByte('\\') | ||||
| 			} | ||||
| 			rxBuild.WriteRune(c) | ||||
| 			if c == '/' && justDirGlob { | ||||
| 				rxBuild.WriteRune('?') | ||||
| 			} | ||||
| 			justDirGlob = false | ||||
| 		} | ||||
| 		if wasStar { | ||||
| 			rxBuild.WriteString("[^/]*") | ||||
| 		} | ||||
| 		rxBuild.WriteByte('$') | ||||
| 		ff.rx, err = regexp.Compile(rxBuild.String()) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("invalid file filter [%s], regexp compile error after glob expand: [%v]", ff.raw, err) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// it's whole file mask, just escape dots and normilze separators | ||||
| 	fillRx := src | ||||
| 	fillRx = strings.ReplaceAll(fillRx, "\\", "/") | ||||
| 	fillRx = strings.ReplaceAll(fillRx, ".", `\.`) | ||||
| 	fillRx = "^" + fillRx + "$" | ||||
| 	ff.rx, err = regexp.Compile(fillRx) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("invalid file filter [%s], regexp compile full path: [%v]", ff.raw, err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										128
									
								
								lint/filefilter_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								lint/filefilter_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| package lint_test | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mgechev/revive/lint" | ||||
| ) | ||||
|  | ||||
| func TestFileFilter(t *testing.T) { | ||||
| 	t.Run("whole file name", func(t *testing.T) { | ||||
| 		ff, err := lint.ParseFileFilter("a/b/c.go") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if !ff.MatchFileName("a/b/c.go") { | ||||
| 			t.Fatal("should match a/b/c.go") | ||||
| 		} | ||||
| 		if ff.MatchFileName("a/b/d.go") { | ||||
| 			t.Fatal("should not match") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("regex", func(t *testing.T) { | ||||
| 		ff, err := lint.ParseFileFilter("~b/[cd].go$") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if !ff.MatchFileName("a/b/c.go") { | ||||
| 			t.Fatal("should match a/b/c.go") | ||||
| 		} | ||||
| 		if !ff.MatchFileName("b/d.go") { | ||||
| 			t.Fatal("should match b/d.go") | ||||
| 		} | ||||
| 		if ff.MatchFileName("b/x.go") { | ||||
| 			t.Fatal("should not match b/x.go") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("TEST well-known", func(t *testing.T) { | ||||
| 		ff, err := lint.ParseFileFilter("TEST") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if !ff.MatchFileName("a/b/c_test.go") { | ||||
| 			t.Fatal("should match a/b/c_test.go") | ||||
| 		} | ||||
| 		if ff.MatchFileName("a/b/c_test_no.go") { | ||||
| 			t.Fatal("should not match a/b/c_test_no.go") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("glob *", func(t *testing.T) { | ||||
| 		ff, err := lint.ParseFileFilter("a/b/*.pb.go") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if !ff.MatchFileName("a/b/xxx.pb.go") { | ||||
| 			t.Fatal("should match a/b/xxx.pb.go") | ||||
| 		} | ||||
| 		if !ff.MatchFileName("a/b/yyy.pb.go") { | ||||
| 			t.Fatal("should match a/b/yyy.pb.go") | ||||
| 		} | ||||
| 		if ff.MatchFileName("a/b/xxx.nopb.go") { | ||||
| 			t.Fatal("should not match a/b/xxx.nopb.go") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("glob **", func(t *testing.T) { | ||||
| 		ff, err := lint.ParseFileFilter("a/**/*.pb.go") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if !ff.MatchFileName("a/x/xxx.pb.go") { | ||||
| 			t.Fatal("should match a/x/xxx.pb.go") | ||||
| 		} | ||||
| 		if !ff.MatchFileName("a/xxx.pb.go") { | ||||
| 			t.Fatal("should match a/xxx.pb.go") | ||||
| 		} | ||||
| 		if !ff.MatchFileName("a/x/y/z/yyy.pb.go") { | ||||
| 			t.Fatal("should match a/x/y/z/yyy.pb.go") | ||||
| 		} | ||||
| 		if ff.MatchFileName("a/b/xxx.nopb.go") { | ||||
| 			t.Fatal("should not match a/b/xxx.nopb.go") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("empty", func(t *testing.T) { | ||||
| 		ff, err := lint.ParseFileFilter("") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		fileNames := []string{"pb.go", "a/pb.go", "a/x/xxx.pb.go", "a/x/xxx.pb_test.go"} | ||||
| 		for _, fn := range fileNames { | ||||
| 			if ff.MatchFileName(fn) { | ||||
| 				t.Fatalf("should not match %s", fn) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("just *", func(t *testing.T) { | ||||
| 		ff, err := lint.ParseFileFilter("*") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		fileNames := []string{"pb.go", "a/pb.go", "a/x/xxx.pb.go", "a/x/xxx.pb_test.go"} | ||||
| 		for _, fn := range fileNames { | ||||
| 			if !ff.MatchFileName(fn) { | ||||
| 				t.Fatalf("should match %s", fn) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("just ~", func(t *testing.T) { | ||||
| 		ff, err := lint.ParseFileFilter("~") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		fileNames := []string{"pb.go", "a/pb.go", "a/x/xxx.pb.go", "a/x/xxx.pb_test.go"} | ||||
| 		for _, fn := range fileNames { | ||||
| 			if !ff.MatchFileName(fn) { | ||||
| 				t.Fatalf("should match %s", fn) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										48
									
								
								test/file-filter_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								test/file-filter_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| package test | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mgechev/revive/lint" | ||||
| ) | ||||
|  | ||||
| type TestFileFilterRule struct { | ||||
| 	WasApplyed bool | ||||
| } | ||||
|  | ||||
| var _ lint.Rule = (*TestFileFilterRule)(nil) | ||||
|  | ||||
| func (*TestFileFilterRule) Name() string { return "test-file-filter" } | ||||
| func (tfr *TestFileFilterRule) Apply(*lint.File, lint.Arguments) []lint.Failure { | ||||
| 	tfr.WasApplyed = true | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestFileExcludeFilterAtRuleLevel(t *testing.T) { | ||||
| 	t.Run("is called if no excludes", func(t *testing.T) { | ||||
| 		rule := &TestFileFilterRule{} | ||||
| 		testRule(t, "file-to-exclude", rule, &lint.RuleConfig{}) | ||||
| 		if !rule.WasApplyed { | ||||
| 			t.Fatal("should call rule if no excludes") | ||||
| 		} | ||||
| 	}) | ||||
| 	t.Run("is called if exclude not match", func(t *testing.T) { | ||||
| 		rule := &TestFileFilterRule{} | ||||
| 		cfg := &lint.RuleConfig{Exclude: []string{"no-matched.go"}} | ||||
| 		cfg.Initialize() | ||||
| 		testRule(t, "file-to-exclude", rule, cfg) | ||||
| 		if !rule.WasApplyed { | ||||
| 			t.Fatal("should call rule if no excludes") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("not called if exclude not match", func(t *testing.T) { | ||||
| 		rule := &TestFileFilterRule{} | ||||
| 		cfg := &lint.RuleConfig{Exclude: []string{"file-to-exclude.go"}} | ||||
| 		cfg.Initialize() | ||||
| 		testRule(t, "file-to-exclude", rule, cfg) | ||||
| 		if rule.WasApplyed { | ||||
| 			t.Fatal("should not call rule if excluded") | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										2
									
								
								testdata/file-to-exclude.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								testdata/file-to-exclude.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| // just to check FileFilter | ||||
| package fixtures | ||||
		Reference in New Issue
	
	Block a user