forbidigo: better support for configuring complex rules (#3612)
This commit is contained in:
		
							parent
							
								
									8fde4632fa
								
							
						
					
					
						commit
						afd0ba5278
					
				| @ -351,14 +351,31 @@ linters-settings: | |||||||
|     # Forbid the following identifiers (list of regexp). |     # Forbid the following identifiers (list of regexp). | ||||||
|     # Default: ["^(fmt\\.Print(|f|ln)|print|println)$"] |     # Default: ["^(fmt\\.Print(|f|ln)|print|println)$"] | ||||||
|     forbid: |     forbid: | ||||||
|  |       # Builtin function: | ||||||
|       - ^print.*$ |       - ^print.*$ | ||||||
|       - 'fmt\.Print.*' |       # Optional message that gets included in error reports. | ||||||
|       # Optionally put comments at the end of the regex, surrounded by `(# )?` |       - p: ^fmt\.Print.*$ | ||||||
|       # Escape any special characters. |         msg: Do not commit print statements. | ||||||
|  |       # Alternatively, put messages at the end of the regex, surrounded by `(# )?` | ||||||
|  |       # Escape any special characters. Those messages get included in error reports. | ||||||
|       - 'fmt\.Print.*(# Do not commit print statements\.)?' |       - 'fmt\.Print.*(# Do not commit print statements\.)?' | ||||||
|  |       # Forbid spew Dump, whether it is called as function or method. | ||||||
|  |       # Depends on analyze-types below. | ||||||
|  |       - ^spew\.(ConfigState\.)?Dump$ | ||||||
|  |       # The package name might be ambiguous. | ||||||
|  |       # The full import path can be used as additional criteria. | ||||||
|  |       # Depends on analyze-types below. | ||||||
|  |       - p: ^v1.Dump$ | ||||||
|  |         pkg: ^example.com/pkg/api/v1$ | ||||||
|     # Exclude godoc examples from forbidigo checks. |     # Exclude godoc examples from forbidigo checks. | ||||||
|     # Default: true |     # Default: true | ||||||
|     exclude_godoc_examples: false |     exclude-godoc-examples: false | ||||||
|  |     # Instead of matching the literal source code, | ||||||
|  |     # use type information to replace expressions with strings that contain the package name | ||||||
|  |     # and (for methods and fields) the type name. | ||||||
|  |     # This makes it possible to handle import renaming and forbid struct fields and methods. | ||||||
|  |     # Default: false | ||||||
|  |     analyze-types: true | ||||||
| 
 | 
 | ||||||
|   funlen: |   funlen: | ||||||
|     # Checks the number of lines in a function. |     # Checks the number of lines in a function. | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -71,6 +71,7 @@ require ( | |||||||
| 	github.com/mgechev/revive v1.3.2 | 	github.com/mgechev/revive v1.3.2 | ||||||
| 	github.com/mitchellh/go-homedir v1.1.0 | 	github.com/mitchellh/go-homedir v1.1.0 | ||||||
| 	github.com/mitchellh/go-ps v1.0.0 | 	github.com/mitchellh/go-ps v1.0.0 | ||||||
|  | 	github.com/mitchellh/mapstructure v1.5.0 | ||||||
| 	github.com/moricho/tparallel v0.3.1 | 	github.com/moricho/tparallel v0.3.1 | ||||||
| 	github.com/nakabonne/nestif v0.3.1 | 	github.com/nakabonne/nestif v0.3.1 | ||||||
| 	github.com/nishanths/exhaustive v0.10.0 | 	github.com/nishanths/exhaustive v0.10.0 | ||||||
| @ -153,7 +154,6 @@ require ( | |||||||
| 	github.com/mattn/go-isatty v0.0.17 // indirect | 	github.com/mattn/go-isatty v0.0.17 // indirect | ||||||
| 	github.com/mattn/go-runewidth v0.0.9 // indirect | 	github.com/mattn/go-runewidth v0.0.9 // indirect | ||||||
| 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect | 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect | ||||||
| 	github.com/mitchellh/mapstructure v1.5.0 // indirect |  | ||||||
| 	github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect | 	github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect | ||||||
| 	github.com/olekukonko/tablewriter v0.0.5 // indirect | 	github.com/olekukonko/tablewriter v0.0.5 // indirect | ||||||
| 	github.com/pelletier/go-toml v1.9.5 // indirect | 	github.com/pelletier/go-toml v1.9.5 // indirect | ||||||
|  | |||||||
| @ -1,8 +1,11 @@ | |||||||
| package config | package config | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"encoding" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"runtime" | 	"runtime" | ||||||
|  | 
 | ||||||
|  | 	"gopkg.in/yaml.v3" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var defaultLintersSettings = LintersSettings{ | var defaultLintersSettings = LintersSettings{ | ||||||
| @ -330,8 +333,45 @@ type ExhaustructSettings struct { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ForbidigoSettings struct { | type ForbidigoSettings struct { | ||||||
| 	Forbid               []string `mapstructure:"forbid"` | 	Forbid               []ForbidigoPattern `mapstructure:"forbid"` | ||||||
| 	ExcludeGodocExamples bool     `mapstructure:"exclude-godoc-examples"` | 	ExcludeGodocExamples bool               `mapstructure:"exclude-godoc-examples"` | ||||||
|  | 	AnalyzeTypes         bool               `mapstructure:"analyze-types"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ encoding.TextUnmarshaler = &ForbidigoPattern{} | ||||||
|  | 
 | ||||||
|  | // ForbidigoPattern corresponds to forbidigo.pattern and adds mapstructure support. | ||||||
|  | // The YAML field names must match what forbidigo expects. | ||||||
|  | type ForbidigoPattern struct { | ||||||
|  | 	// patternString gets populated when the config contains a string as entry in ForbidigoSettings.Forbid[] | ||||||
|  | 	// because ForbidigoPattern implements encoding.TextUnmarshaler | ||||||
|  | 	// and the reader uses the mapstructure.TextUnmarshallerHookFunc as decoder hook. | ||||||
|  | 	// | ||||||
|  | 	// If the entry is a map, then the other fields are set as usual by mapstructure. | ||||||
|  | 	patternString string | ||||||
|  | 
 | ||||||
|  | 	Pattern string `yaml:"p" mapstructure:"p"` | ||||||
|  | 	Package string `yaml:"pkg,omitempty" mapstructure:"pkg,omitempty"` | ||||||
|  | 	Msg     string `yaml:"msg,omitempty" mapstructure:"msg,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *ForbidigoPattern) UnmarshalText(text []byte) error { | ||||||
|  | 	// Validation happens when instantiating forbidigo. | ||||||
|  | 	p.patternString = string(text) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalString converts the pattern into a string as needed by forbidigo.NewLinter. | ||||||
|  | // | ||||||
|  | // MarshalString is intentionally not called MarshalText, | ||||||
|  | // although it has the same signature | ||||||
|  | // because implementing encoding.TextMarshaler led to infinite recursion when yaml.Marshal called MarshalText. | ||||||
|  | func (p *ForbidigoPattern) MarshalString() ([]byte, error) { | ||||||
|  | 	if p.patternString != "" { | ||||||
|  | 		return []byte(p.patternString), nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return yaml.Marshal(p) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type FunlenSettings struct { | type FunlenSettings struct { | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mitchellh/go-homedir" | 	"github.com/mitchellh/go-homedir" | ||||||
|  | 	"github.com/mitchellh/mapstructure" | ||||||
| 	"github.com/spf13/viper" | 	"github.com/spf13/viper" | ||||||
| 	"golang.org/x/exp/slices" | 	"golang.org/x/exp/slices" | ||||||
| 
 | 
 | ||||||
| @ -91,7 +92,14 @@ func (r *FileReader) parseConfig() error { | |||||||
| 	} | 	} | ||||||
| 	r.cfg.cfgDir = usedConfigDir | 	r.cfg.cfgDir = usedConfigDir | ||||||
| 
 | 
 | ||||||
| 	if err := viper.Unmarshal(r.cfg); err != nil { | 	if err := viper.Unmarshal(r.cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( | ||||||
|  | 		// Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138). | ||||||
|  | 		mapstructure.StringToTimeDurationHookFunc(), | ||||||
|  | 		mapstructure.StringToSliceHookFunc(","), | ||||||
|  | 
 | ||||||
|  | 		// Needed for forbidigo. | ||||||
|  | 		mapstructure.TextUnmarshallerHookFunc(), | ||||||
|  | 	))); err != nil { | ||||||
| 		return fmt.Errorf("can't unmarshal config by viper: %s", err) | 		return fmt.Errorf("can't unmarshal config by viper: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ import ( | |||||||
| 	"github.com/golangci/golangci-lint/pkg/config" | 	"github.com/golangci/golangci-lint/pkg/config" | ||||||
| 	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis" | 	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis" | ||||||
| 	"github.com/golangci/golangci-lint/pkg/lint/linter" | 	"github.com/golangci/golangci-lint/pkg/lint/linter" | ||||||
|  | 	"github.com/golangci/golangci-lint/pkg/logutils" | ||||||
| 	"github.com/golangci/golangci-lint/pkg/result" | 	"github.com/golangci/golangci-lint/pkg/result" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -40,6 +41,9 @@ func NewForbidigo(settings *config.ForbidigoSettings) *goanalysis.Linter { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Without AnalyzeTypes, LoadModeSyntax is enough. | ||||||
|  | 	// But we cannot make this depend on the settings and have to mirror the mode chosen in GetAllSupportedLinterConfigs, | ||||||
|  | 	// therefore we have to use LoadModeTypesInfo in all cases. | ||||||
| 	return goanalysis.NewLinter( | 	return goanalysis.NewLinter( | ||||||
| 		forbidigoName, | 		forbidigoName, | ||||||
| 		"Forbids identifiers", | 		"Forbids identifiers", | ||||||
| @ -55,16 +59,34 @@ func runForbidigo(pass *analysis.Pass, settings *config.ForbidigoSettings) ([]go | |||||||
| 		forbidigo.OptionExcludeGodocExamples(settings.ExcludeGodocExamples), | 		forbidigo.OptionExcludeGodocExamples(settings.ExcludeGodocExamples), | ||||||
| 		// disable "//permit" directives so only "//nolint" directives matters within golangci-lint | 		// disable "//permit" directives so only "//nolint" directives matters within golangci-lint | ||||||
| 		forbidigo.OptionIgnorePermitDirectives(true), | 		forbidigo.OptionIgnorePermitDirectives(true), | ||||||
|  | 		forbidigo.OptionAnalyzeTypes(settings.AnalyzeTypes), | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	forbid, err := forbidigo.NewLinter(settings.Forbid, options...) | 	// Convert patterns back to strings because that is what NewLinter accepts. | ||||||
|  | 	var patterns []string | ||||||
|  | 	for _, pattern := range settings.Forbid { | ||||||
|  | 		buffer, err := pattern.MarshalString() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		patterns = append(patterns, string(buffer)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	forbid, err := forbidigo.NewLinter(patterns, options...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("failed to create linter %q: %w", forbidigoName, err) | 		return nil, fmt.Errorf("failed to create linter %q: %w", forbidigoName, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var issues []goanalysis.Issue | 	var issues []goanalysis.Issue | ||||||
| 	for _, file := range pass.Files { | 	for _, file := range pass.Files { | ||||||
| 		hints, err := forbid.RunWithConfig(forbidigo.RunConfig{Fset: pass.Fset}, file) | 		runConfig := forbidigo.RunConfig{ | ||||||
|  | 			Fset:     pass.Fset, | ||||||
|  | 			DebugLog: logutils.Debug(logutils.DebugKeyForbidigo), | ||||||
|  | 		} | ||||||
|  | 		if settings != nil && settings.AnalyzeTypes { | ||||||
|  | 			runConfig.TypesInfo = pass.TypesInfo | ||||||
|  | 		} | ||||||
|  | 		hints, err := forbid.RunWithConfig(runConfig, file) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("forbidigo linter failed on file %q: %w", file.Name.String(), err) | 			return nil, fmt.Errorf("forbidigo linter failed on file %q: %w", file.Name.String(), err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -427,6 +427,10 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { | |||||||
| 		linter.NewConfig(golinters.NewForbidigo(forbidigoCfg)). | 		linter.NewConfig(golinters.NewForbidigo(forbidigoCfg)). | ||||||
| 			WithSince("v1.34.0"). | 			WithSince("v1.34.0"). | ||||||
| 			WithPresets(linter.PresetStyle). | 			WithPresets(linter.PresetStyle). | ||||||
|  | 			// Strictly speaking, | ||||||
|  | 			// the additional information is only needed when forbidigoCfg.AnalyzeTypes is chosen by the user. | ||||||
|  | 			// But we don't know that here in all cases (sometimes config is not loaded), | ||||||
|  | 			// so we have to assume that it is needed to be on the safe side. | ||||||
| 			WithLoadForGoAnalysis(). | 			WithLoadForGoAnalysis(). | ||||||
| 			WithURL("https://github.com/ashanbrown/forbidigo"), | 			WithURL("https://github.com/ashanbrown/forbidigo"), | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ const ( | |||||||
| 	DebugKeyExcludeRules       = "exclude_rules" | 	DebugKeyExcludeRules       = "exclude_rules" | ||||||
| 	DebugKeyExec               = "exec" | 	DebugKeyExec               = "exec" | ||||||
| 	DebugKeyFilenameUnadjuster = "filename_unadjuster" | 	DebugKeyFilenameUnadjuster = "filename_unadjuster" | ||||||
|  | 	DebugKeyForbidigo          = "forbidigo" | ||||||
| 	DebugKeyGoEnv              = "goenv" | 	DebugKeyGoEnv              = "goenv" | ||||||
| 	DebugKeyLinter             = "linter" | 	DebugKeyLinter             = "linter" | ||||||
| 	DebugKeyLintersContext     = "linters_context" | 	DebugKeyLintersContext     = "linters_context" | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								test/testdata/configs/forbidigo_struct.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/testdata/configs/forbidigo_struct.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | linters-settings: | ||||||
|  |   forbidigo: | ||||||
|  |     analyze-types: true | ||||||
|  |     forbid: | ||||||
|  |       - p: fmt\.Print.* | ||||||
|  |         pkg: ^fmt$ | ||||||
|  |       - p: time.Sleep | ||||||
|  |         msg: no sleeping! | ||||||
							
								
								
									
										2
									
								
								test/testdata/forbidigo_example.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								test/testdata/forbidigo_example.go
									
									
									
									
										vendored
									
									
								
							| @ -4,10 +4,12 @@ package testdata | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	fmt2 "fmt" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func Forbidigo() { | func Forbidigo() { | ||||||
| 	fmt.Printf("too noisy!!!")  // want "use of `fmt\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`" | 	fmt.Printf("too noisy!!!")  // want "use of `fmt\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`" | ||||||
|  | 	fmt2.Printf("too noisy!!!") // Not detected because analyze-types is false by default for backward compatbility. | ||||||
| 	time.Sleep(time.Nanosecond) // want "no sleeping!" | 	time.Sleep(time.Nanosecond) // want "no sleeping!" | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								test/testdata/forbidigo_struct_config.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								test/testdata/forbidigo_struct_config.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | //golangcitest:args -Eforbidigo | ||||||
|  | //golangcitest:config_path testdata/configs/forbidigo_struct.yml | ||||||
|  | package testdata | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	fmt2 "fmt" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func Forbidigo() { | ||||||
|  | 	fmt2.Printf("too noisy!!!") // want "use of `fmt2\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`" | ||||||
|  | 	time.Sleep(time.Nanosecond) // want "no sleeping!" | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Patrick Ohly
						Patrick Ohly