Skip to content

Commit

Permalink
添加禁用函数检查的 lint (golangci#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
SeigeC authored and icylight committed May 10, 2021
1 parent 5c6adb6 commit 8c6b540
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .golangci.example.yml
Expand Up @@ -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
Expand Down
161 changes: 161 additions & 0 deletions 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
}
170 changes: 170 additions & 0 deletions 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...))
}
3 changes: 3 additions & 0 deletions pkg/lint/lintersdb/manager.go
Expand Up @@ -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").
Expand Down

0 comments on commit 8c6b540

Please sign in to comment.