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 }