Files
go-lint/boolnames/analyzer.go

120 lines
2.5 KiB
Go

package boolnames
import (
"fmt"
"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) {
result, ok := pass.ResultOf[inspect.Analyzer]
if !ok {
return nil, fmt.Errorf("inspect analyzer result is unavailable")
}
insp, ok := result.(*inspector.Inspector)
if !ok {
return nil, fmt.Errorf("inspect analyzer returned %T, want *inspector.Inspector", result)
}
insp.Preorder([]ast.Node{(*ast.Ident)(nil)}, func(n ast.Node) {
ident, ok := n.(*ast.Ident)
if !ok {
return
}
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
}