boolnames: make default whitelist include 'ok'

This commit is contained in:
2026-10-14 13:23:41 -07:00
parent 6d23a47815
commit 020eb7ab7f
15 changed files with 14 additions and 10 deletions

120
pkg/boolnames/analyzer.go Normal file
View 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
}

View 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
View 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
View 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)
}

View 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"
}

View 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`