114 lines
2.4 KiB
Go
114 lines
2.4 KiB
Go
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"}
|
|
|
|
var Analyzer = NewAnalyzer(Settings{})
|
|
|
|
func NewAnalyzer(settings Settings) *analysis.Analyzer {
|
|
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
|
|
}
|