boolnames: make default whitelist include 'ok'
This commit is contained in:
120
pkg/boolnames/analyzer.go
Normal file
120
pkg/boolnames/analyzer.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package boolnames
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/types"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/go/analysis/passes/inspect"
|
||||
"golang.org/x/tools/go/ast/inspector"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultBoolNamePrefixes = []string{"is", "has", "can", "was"}
|
||||
defaultBoolNameWhitelist = []string{"ok"}
|
||||
)
|
||||
|
||||
var Analyzer = NewAnalyzer(Settings{})
|
||||
|
||||
func NewAnalyzer(settings Settings) *analysis.Analyzer {
|
||||
if len(settings.Whitelist) == 0 {
|
||||
settings.Whitelist = defaultBoolNameWhitelist
|
||||
}
|
||||
|
||||
if len(settings.AllowedPrefixes) == 0 {
|
||||
settings.AllowedPrefixes = defaultBoolNamePrefixes
|
||||
}
|
||||
return &analysis.Analyzer{
|
||||
Name: "boolnames",
|
||||
Doc: `checks that boolean values and functions use appropriate names`,
|
||||
Requires: []*analysis.Analyzer{inspect.Analyzer},
|
||||
Run: func(pass *analysis.Pass) (any, error) {
|
||||
insp, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
|
||||
if !ok {
|
||||
panic("inspect.Analyzer did not produce *inspector.Inspector")
|
||||
}
|
||||
|
||||
insp.Preorder([]ast.Node{(*ast.Ident)(nil)}, func(n ast.Node) {
|
||||
ident, ok := n.(*ast.Ident)
|
||||
if !ok {
|
||||
panic("inspector passed a non-Identifier node to an Identifier preorder")
|
||||
}
|
||||
obj := pass.TypesInfo.Defs[ident]
|
||||
if obj == nil || ident.Name == "_" {
|
||||
return
|
||||
}
|
||||
|
||||
switch obj.(type) {
|
||||
case *types.Var, *types.Const, *types.Func:
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if !hasBoolType(obj.Type()) {
|
||||
return
|
||||
}
|
||||
|
||||
if settings.IsAcceptableBoolName(ident.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
pass.Reportf(ident.Pos(),
|
||||
"boolean name %q must start with %s",
|
||||
ident.Name,
|
||||
strings.Join(settings.AllowedPrefixes, ", "),
|
||||
)
|
||||
})
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func hasBoolType(t types.Type) bool {
|
||||
isBool := func(t types.Type) bool {
|
||||
basic, ok := types.Unalias(t).Underlying().(*types.Basic)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return basic.Kind() == types.Bool || basic.Kind() == types.UntypedBool
|
||||
}
|
||||
if isBool(t) {
|
||||
return true
|
||||
}
|
||||
|
||||
sig, ok := types.Unalias(t).Underlying().(*types.Signature)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
results := sig.Results()
|
||||
return results.Len() == 1 && isBool(results.At(0).Type())
|
||||
}
|
||||
|
||||
func (settings Settings) IsAcceptableBoolName(s string) bool {
|
||||
if slices.Contains(settings.Whitelist, s) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, pfix := range settings.AllowedPrefixes {
|
||||
if pfix == "" || len(s) <= len(pfix) || s[1:len(pfix)] != pfix[1:] {
|
||||
continue
|
||||
}
|
||||
|
||||
first := s[0]
|
||||
if first != pfix[0] && first != pfix[0]-'a'+'A' {
|
||||
continue
|
||||
}
|
||||
|
||||
sfix := s[len(pfix):]
|
||||
r, _ := utf8.DecodeRuneInString(sfix)
|
||||
return unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
22
pkg/boolnames/analyzer_test.go
Normal file
22
pkg/boolnames/analyzer_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package boolnames_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
|
||||
"git.offline-twitter.com/offline-labs/go-lint/pkg/boolnames"
|
||||
)
|
||||
|
||||
func TestAnalyzer(t *testing.T) {
|
||||
analysistest.Run(t, analysistest.TestData(), boolnames.Analyzer, "cases")
|
||||
}
|
||||
|
||||
func TestAnalyzerWithCustomSettings(t *testing.T) {
|
||||
analyzer := boolnames.NewAnalyzer(boolnames.Settings{
|
||||
Whitelist: []string{"ok"},
|
||||
AllowedPrefixes: []string{"truthy"},
|
||||
})
|
||||
|
||||
analysistest.Run(t, analysistest.TestData(), analyzer, "casescustom")
|
||||
}
|
||||
31
pkg/boolnames/plugin.go
Normal file
31
pkg/boolnames/plugin.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package boolnames
|
||||
|
||||
import (
|
||||
"github.com/golangci/plugin-module-register/register"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register.Plugin("boolnames", New)
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
settings Settings
|
||||
}
|
||||
|
||||
func New(rawSettings any) (register.LinterPlugin, error) {
|
||||
settings, err := decodeSettings(rawSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &plugin{settings: settings}, nil
|
||||
}
|
||||
|
||||
func (p *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
|
||||
return []*analysis.Analyzer{NewAnalyzer(p.settings)}, nil
|
||||
}
|
||||
|
||||
func (p *plugin) GetLoadMode() string {
|
||||
return register.LoadModeTypesInfo
|
||||
}
|
||||
16
pkg/boolnames/settings.go
Normal file
16
pkg/boolnames/settings.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package boolnames
|
||||
|
||||
import "github.com/golangci/plugin-module-register/register"
|
||||
|
||||
type Settings struct {
|
||||
Whitelist []string `json:"whitelist"`
|
||||
AllowedPrefixes []string `json:"allowed_prefixes"`
|
||||
}
|
||||
|
||||
func decodeSettings(rawSettings any) (Settings, error) {
|
||||
if rawSettings == nil {
|
||||
return Settings{}, nil
|
||||
}
|
||||
|
||||
return register.DecodeSettings[Settings](rawSettings)
|
||||
}
|
||||
56
pkg/boolnames/testdata/src/cases/boolnames.go
vendored
Normal file
56
pkg/boolnames/testdata/src/cases/boolnames.go
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
package cases
|
||||
|
||||
var isPackageReady bool
|
||||
var packageReady bool // want `boolean name "packageReady" must`
|
||||
|
||||
const hasPackageValue = true
|
||||
const packageValue = true // want `boolean name "packageValue" must`
|
||||
|
||||
type FeatureFlag bool
|
||||
|
||||
type Predicate func() bool
|
||||
|
||||
var canEvaluate Predicate
|
||||
var evaluate Predicate // want `boolean name "evaluate" must`
|
||||
|
||||
type Config struct {
|
||||
HasOwner bool
|
||||
owner bool // want `boolean name "owner" must`
|
||||
|
||||
CanRun func() bool
|
||||
run func() bool // want `boolean name "run" must`
|
||||
}
|
||||
|
||||
func IsReady() bool { return true }
|
||||
|
||||
func ready() bool { return true } // want `boolean name "ready" must`
|
||||
|
||||
func Find() (int, bool) { return 0, true }
|
||||
func FindPair() (bool, bool) { return true, false }
|
||||
|
||||
func consume(active bool, hasActive bool, predicate func() bool, canCheck func() bool) (ok bool) { // want `boolean name "consume" must` `boolean name "active" must` `boolean name "predicate" must`
|
||||
ready := true // want `boolean name "ready" must`
|
||||
isReady := true
|
||||
|
||||
var enabled bool // want `boolean name "enabled" must`
|
||||
var wasEnabled bool
|
||||
|
||||
var namedFlag FeatureFlag // want `boolean name "namedFlag" must`
|
||||
var isNamedFlag FeatureFlag
|
||||
|
||||
checker := func() bool { return true } // want `boolean name "checker" must`
|
||||
hasChecker := func() bool { return true }
|
||||
|
||||
_ = func(active bool) (ok bool) { // want `boolean name "active" must`
|
||||
return ok
|
||||
}
|
||||
|
||||
return ready || isReady || enabled || wasEnabled || bool(namedFlag) || bool(isNamedFlag) || checker() || hasChecker() || active || hasActive || predicate() || canCheck()
|
||||
}
|
||||
|
||||
func (flag FeatureFlag) String() string { // want `boolean name "flag" must`
|
||||
if flag {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
12
pkg/boolnames/testdata/src/casescustom/boolnames.go
vendored
Normal file
12
pkg/boolnames/testdata/src/casescustom/boolnames.go
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
package casescustom
|
||||
|
||||
var ok bool
|
||||
var truthyValue bool
|
||||
|
||||
var ready bool // want `boolean name "ready" must start with truthy`
|
||||
var isReady bool // want `boolean name "isReady" must start with truthy`
|
||||
var hasOwner bool // want `boolean name "hasOwner" must start with truthy`
|
||||
var truthy bool // want `boolean name "truthy" must start with truthy`
|
||||
|
||||
func truthyCheck() bool { return true }
|
||||
func canCheck() bool { return true } // want `boolean name "canCheck" must start with truthy`
|
||||
Reference in New Issue
Block a user