diff --git a/.golangci.example.yml b/.golangci.example.yml index 8e58deda502b..ec664b4107dd 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -388,6 +388,11 @@ linters-settings: - shadow disable-all: false + bannedfunc: + # 禁用函数采用键值对形式 + # 包名需要用引号扩起 + (time).Now: "不能使用 time.Now(),请使用 MiaoSiLa/missevan-go/util 下 TimeNow()" + depguard: list-type: blacklist include-go-root: false diff --git a/pkg/golinters/bannedfunc.go b/pkg/golinters/bannedfunc.go new file mode 100644 index 000000000000..c3c9327c14e9 --- /dev/null +++ b/pkg/golinters/bannedfunc.go @@ -0,0 +1,161 @@ +package golinters + +import ( + "go/ast" + "io/ioutil" + "os" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "gopkg.in/yaml.v2" + + "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" + "github.com/golangci/golangci-lint/pkg/lint/linter" +) + +// Analyzer lint 插件的结构体 +var Analyzer = &analysis.Analyzer{ + Name: "bandfunc", + Doc: "Checks that cannot use func", + Requires: []*analysis.Analyzer{inspect.Analyzer}, +} + +type configSetting struct { + LinterSettings BandFunc `yaml:"linters-settings"` +} + +// BandFunc 读取配置的结构体 +type BandFunc struct { + Funcs map[string]string `yaml:"bannedfunc,flow"` +} + +// NewCheckBannedFunc 返回检查函数 +func NewCheckBannedFunc() *goanalysis.Linter { + return goanalysis.NewLinter( + "bannedfunc", + "Checks that cannot use func", + []*analysis.Analyzer{Analyzer}, + nil, + ).WithContextSetter(linterCtx).WithLoadMode(goanalysis.LoadModeSyntax) +} + +func linterCtx(lintCtx *linter.Context) { + // 读取配置文件 + config := loadConfigFile() + + configMap := configToConfigMap(config) + + Analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { + useMap := getUsedMap(pass, configMap) + for _, f := range pass.Files { + ast.Inspect(f, astFunc(pass, useMap)) + } + return nil, nil + } +} + +func astFunc(pass *analysis.Pass, usedMap map[string]map[string]string) func(node ast.Node) bool { + return func(node ast.Node) bool { + selector, ok := node.(*ast.SelectorExpr) + if !ok { + return true + } + + ident, ok := selector.X.(*ast.Ident) + if !ok { + return true + } + + m := usedMap[ident.Name] + if m == nil { + return true + } + + sel := selector.Sel + value, ok := m[sel.Name] + if !ok { + return true + } + pass.Reportf(node.Pos(), value) + return true + } +} + +// configToConfigMap 将配置文件转成 map +// map[包名]map[函数名]错误提示 +// example: +// { +// time: { +// Now: 不能使用 time.Now() 请使用 MiaoSiLa/missevan-go/util 下 TimeNow() +// Date: xxxx +// }, +// github.com/MiaoSiLa/missevan-go/util/time: { +// TimeNow: xxxxxx +// SetTimeNow: xxxxx +// } +// } +func configToConfigMap(config configSetting) map[string]map[string]string { + configMap := make(map[string]map[string]string) + for k, v := range config.LinterSettings.Funcs { + strs := strings.Split(k, ").") + if len(strs) != 2 { + continue + } + if len(strs[0]) <= 1 || strs[0][0] != '(' { + continue + } + pkg, name := strs[0][1:], strs[1] + if name == "" { + continue + } + m := configMap[pkg] + if m == nil { + m = make(map[string]string) + configMap[pkg] = m + } + m[name] = v + } + return configMap +} + +func loadConfigFile() configSetting { + wd, _ := os.Getwd() + f, err := ioutil.ReadFile(wd + "/.golangci.yml") + if err != nil { + panic(err) + } + return decodeFile(f) +} + +func decodeFile(b []byte) configSetting { + var config configSetting + err := yaml.Unmarshal(b, &config) + if err != nil { + panic(err) + } + return config +} + +// getUsedMap 将配置文件的 map 转成文件下实际变量名的 map +// map[包的别名]map[函数名]错误提示 +// example: +// { +// time: { +// Now: 不能使用 time.Now() 请使用 MiaoSiLa/missevan-go/util 下 TimeNow() +// Date: xxxx +// }, +// util: { +// TimeNow: xxxxxx +// SetTimeNow: xxxxx +// } +// } +func getUsedMap(pass *analysis.Pass, configMap map[string]map[string]string) map[string]map[string]string { + useMap := make(map[string]map[string]string) + for _, item := range pass.Pkg.Imports() { + if m, ok := configMap[item.Path()]; ok { + useMap[item.Name()] = m + } + } + return useMap +} diff --git a/pkg/golinters/bannedfunc_test.go b/pkg/golinters/bannedfunc_test.go new file mode 100644 index 000000000000..376f89ad90b3 --- /dev/null +++ b/pkg/golinters/bannedfunc_test.go @@ -0,0 +1,170 @@ +package golinters + +import ( + "fmt" + "go/ast" + "go/types" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestDecodeFile(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + b := strings.TrimSpace(` +linters-settings: + bannedfunc: + (time).Now: "不能使用 time.Now() 请使用 MiaoSiLa/missevan-go/util 下 TimeNow()" + (github.com/MiaoSiLa/missevan-go/util).TimeNow: "aaa" +`) + var setting configSetting + require.NotPanics(func() { setting = decodeFile([]byte(b)) }) + require.NotNil(setting.LinterSettings) + val := setting.LinterSettings.Funcs["(time).Now"] + assert.NotEmpty(val) + val = setting.LinterSettings.Funcs["(github.com/MiaoSiLa/missevan-go/util).TimeNow"] + assert.NotEmpty(val) +} + +func TestConfigToConfigMap(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + m := map[string]string{ + "(time).Now": "不能使用 time.Now() 请使用 MiaoSiLa/missevan-go/util 下 TimeNow()", + "(github.com/MiaoSiLa/missevan-go/util).TimeNow": "xxxx", + "().": "(). 情况", + ").": "). 情况", + } + s := configSetting{LinterSettings: BandFunc{Funcs: m}} + setting := configToConfigMap(s) + require.Len(setting, 2) + require.NotNil(setting["time"]) + require.NotNil(setting["time"]["Now"]) + assert.Equal("不能使用 time.Now() 请使用 MiaoSiLa/missevan-go/util 下 TimeNow()", setting["time"]["Now"]) + require.NotNil(setting["github.com/MiaoSiLa/missevan-go/util"]) + require.NotNil(setting["github.com/MiaoSiLa/missevan-go/util"]["TimeNow"]) + assert.Equal("xxxx", setting["github.com/MiaoSiLa/missevan-go/util"]["TimeNow"]) + assert.Nil(setting["()."]) + assert.Nil(setting[")."]) +} + +func TestGetUsedMap(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + pkg := types.NewPackage("test", "test") + importPkg := []*types.Package{types.NewPackage("time", "time"), + types.NewPackage("github.com/MiaoSiLa/missevan-go/util", "util")} + pkg.SetImports(importPkg) + pass := analysis.Pass{Pkg: pkg} + m := map[string]map[string]string{ + "time": {"Now": "xxxx", "Date": "xxxx"}, + "assert": {"New": "xxxx"}, + "github.com/MiaoSiLa/missevan-go/util": {"TimeNow": "xxxx"}, + } + usedMap := getUsedMap(&pass, m) + require.Len(usedMap, 2) + require.Len(usedMap["time"], 2) + assert.NotEmpty(usedMap["time"]["Now"]) + assert.NotEmpty(usedMap["time"]["Date"]) + require.Len(usedMap["util"], 1) + assert.NotEmpty(usedMap["util"]["TimeNow"]) +} + +func TestAstFunc(t *testing.T) { + assert := assert.New(t) + + // 初始化测试用参数 + var testStr string + pass := analysis.Pass{Report: func(diagnostic analysis.Diagnostic) { + testStr = diagnostic.Message + }} + m := map[string]map[string]string{ + "time": {"Now": "time.Now"}, + "util": {"TimeNow": "util.TimeNow"}, + } + f := astFunc(&pass, m) + + // 测试不符合情况 + node := ast.SelectorExpr{X: &ast.Ident{Name: "time"}, Sel: &ast.Ident{Name: "Date"}} + f(&node) + assert.Empty(testStr) + node = ast.SelectorExpr{X: &ast.Ident{Name: "assert"}, Sel: &ast.Ident{Name: "New"}} + f(&node) + assert.Empty(testStr) + + // 测试符合情况 + node = ast.SelectorExpr{X: &ast.Ident{Name: "time"}, Sel: &ast.Ident{Name: "Now"}} + f(&node) + assert.Equal("time.Now", testStr) + node = ast.SelectorExpr{X: &ast.Ident{Name: "util"}, Sel: &ast.Ident{Name: "TimeNow"}} + f(&node) + assert.Equal("util.TimeNow", testStr) +} + +func TestFile(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + b := strings.TrimSpace(` +linters-settings: + bannedfunc: + (time).Now: "禁止使用 time.Now" + (time).Unix: "禁止使用 time.Unix" +`) + var setting configSetting + require.NotPanics(func() { setting = decodeFile([]byte(b)) }) + var m map[string]map[string]string + m = configToConfigMap(setting) + Analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { + useMap := getUsedMap(pass, m) + for _, f := range pass.Files { + ast.Inspect(f, astFunc(pass, useMap)) + } + return nil, nil + } + // NOTICE: 因为配置的初始化函数将 GOPATH 设置为当前目录 + // 所以只能 import 内置包和当前目录下的包 + files := map[string]string{ + "a/b.go": `package main + +import ( + "fmt" + "time" +) + +func main() { + fmt.Println(time.Now()) + _ = time.Now() + _ = time.Now().Unix() + _ = time.Unix(10000,0) +} + +`} + dir, cleanup, err := analysistest.WriteFiles(files) + require.NoError(err) + defer cleanup() + var got []string + t2 := errorfunc(func(s string) { got = append(got, s) }) // a fake *testing.T + analysistest.Run(t2, dir, Analyzer, "a") + want := []string{ + `a/b.go:9:14: unexpected diagnostic: 禁止使用 time.Now`, + `a/b.go:10:6: unexpected diagnostic: 禁止使用 time.Now`, + `a/b.go:11:6: unexpected diagnostic: 禁止使用 time.Now`, + `a/b.go:12:6: unexpected diagnostic: 禁止使用 time.Unix`, + } + assert.Equal(want, got) +} + +type errorfunc func(string) + +func (f errorfunc) Errorf(format string, args ...interface{}) { + f(fmt.Sprintf(format, args...)) +} diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index 8b057b342b62..464f70b68cf1 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -501,6 +501,9 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { WithPresets(linter.PresetStyle). WithURL("https://github.com/ldez/tagliatelle"), + linter.NewConfig(golinters.NewCheckBannedFunc()). + WithPresets(linter.PresetStyle), + // nolintlint must be last because it looks at the results of all the previous linters for unused nolint directives linter.NewConfig(golinters.NewNoLintLint()). WithSince("v1.26.0").