Skip to content

Commit 007442a

Browse files
committed
Add NoBools analyzer to prevent the usage of booleans
1 parent 16e070a commit 007442a

File tree

8 files changed

+163
-0
lines changed

8 files changed

+163
-0
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ lintersConfig:
194194
jsonTagRegex: "^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$" # Provide a custom regex, which the json tag must match.
195195
```
196196

197+
## NoBools
198+
199+
The `nobools` linter checks that fields in the API types do not contain a `bool` type.
200+
201+
Booleans are limited and do not evolve well over time.
202+
It is recommended instead to create a string alias with meaningful values, as an enum.
203+
197204
## Nophase
198205

199206
The `nophase` linter checks that the fields in the API types don't contain a 'Phase', or any field which contains 'Phase' as a substring, e.g MachinePhase.

pkg/analysis/nobools/analyzer.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package nobools
2+
3+
import (
4+
"errors"
5+
"go/ast"
6+
7+
"github.com/JoelSpeed/kal/pkg/analysis/utils"
8+
"golang.org/x/tools/go/analysis"
9+
"golang.org/x/tools/go/analysis/passes/inspect"
10+
"golang.org/x/tools/go/ast/inspector"
11+
)
12+
13+
const name = "nobools"
14+
15+
var (
16+
errCouldNotGetInspector = errors.New("could not get inspector")
17+
)
18+
19+
// Analyzer is the analyzer for the nobools package.
20+
// It checks that no struct fields are `bool`.
21+
var Analyzer = &analysis.Analyzer{
22+
Name: name,
23+
Doc: "Boolean values cannot evolve over time, use an enum with meaningful values instead",
24+
Run: run,
25+
Requires: []*analysis.Analyzer{inspect.Analyzer},
26+
}
27+
28+
func run(pass *analysis.Pass) (interface{}, error) {
29+
inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
30+
if !ok {
31+
return nil, errCouldNotGetInspector
32+
}
33+
34+
// Filter to structs so that we can iterate over fields in a struct.
35+
// Filter typespecs so that we can look at type aliases.
36+
nodeFilter := []ast.Node{
37+
(*ast.StructType)(nil),
38+
(*ast.TypeSpec)(nil),
39+
}
40+
41+
typeChecker := utils.NewTypeChecker(checkBool)
42+
43+
// Preorder visits all the nodes of the AST in depth-first order. It calls
44+
// f(n) for each node n before it visits n's children.
45+
//
46+
// We use the filter defined above, ensuring we only look at struct fields and type declarations.
47+
inspect.Preorder(nodeFilter, func(n ast.Node) {
48+
typeChecker.CheckNode(pass, n)
49+
})
50+
51+
return nil, nil //nolint:nilnil
52+
}
53+
54+
func checkBool(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) {
55+
if ident.Name == "bool" {
56+
pass.Reportf(node.Pos(), "%s should not use a bool. Use a string type with meaningful constant values as an enum.", prefix)
57+
}
58+
}

pkg/analysis/nobools/analyzer_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package nobools_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/JoelSpeed/kal/pkg/analysis/nobools"
7+
"golang.org/x/tools/go/analysis/analysistest"
8+
)
9+
10+
func Test(t *testing.T) {
11+
testdata := analysistest.TestData()
12+
analysistest.Run(t, testdata, nobools.Analyzer, "a")
13+
}

pkg/analysis/nobools/doc.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
nobools is an analyzer that checks for usage of bool types.
3+
4+
Boolean values can only ever have 2 states, true or false.
5+
Over time, needs may change, and with a bool type, there is no way to add additional states.
6+
This problem then leads to pairs of bools, where values of one are only valid given the value of another.
7+
This is confusing and error-prone.
8+
9+
It is recommended instead to use a string type with a set of constants to represent the different states,
10+
creating an enum.
11+
12+
By using an enum, not only can you provide meaningul names for the various states of the API,
13+
but you can also add additional states in the future without breaking the API.
14+
*/
15+
package nobools

pkg/analysis/nobools/initializer.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package nobools
2+
3+
import (
4+
"github.com/JoelSpeed/kal/pkg/config"
5+
"golang.org/x/tools/go/analysis"
6+
)
7+
8+
// Initializer returns the AnalyzerInitializer for this
9+
// Analyzer so that it can be added to the registry.
10+
func Initializer() initializer {
11+
return initializer{}
12+
}
13+
14+
// intializer implements the AnalyzerInitializer interface.
15+
type initializer struct{}
16+
17+
// Name returns the name of the Analyzer.
18+
func (initializer) Name() string {
19+
return name
20+
}
21+
22+
// Init returns the intialized Analyzer.
23+
func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) {
24+
return Analyzer, nil
25+
}
26+
27+
// Default determines whether this Analyzer is on by default, or not.
28+
func (initializer) Default() bool {
29+
// Bools avoidance in the Kube conventions is not a must.
30+
// Make this opt in depending on the projects own preference.
31+
return false
32+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package a
2+
3+
type Integers struct {
4+
ValidString string
5+
6+
ValidMap map[string]string
7+
8+
ValidInt32 int32
9+
10+
ValidInt64 int64
11+
12+
InvalidBool bool // want "field InvalidBool should not use a bool. Use a string type with meaningful constant values as an enum."
13+
14+
InvalidBoolPtr *bool // want "field InvalidBoolPtr pointer should not use a bool. Use a string type with meaningful constant values as an enum."
15+
16+
InvalidBoolSlice []bool // want "field InvalidBoolSlice array element should not use a bool. Use a string type with meaningful constant values as an enum."
17+
18+
InvalidBoolPtrSlice []*bool // want "field InvalidBoolPtrSlice array element pointer should not use a bool. Use a string type with meaningful constant values as an enum."
19+
20+
InvalidBoolAlias BoolAlias // want "field InvalidBoolAlias type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum."
21+
22+
InvalidBoolPtrAlias *BoolAlias // want "field InvalidBoolPtrAlias pointer type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum."
23+
24+
InvalidBoolSliceAlias []BoolAlias // want "field InvalidBoolSliceAlias array element type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum."
25+
26+
InvalidBoolPtrSliceAlias []*BoolAlias // want "field InvalidBoolPtrSliceAlias array element pointer type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum."
27+
}
28+
29+
type BoolAlias bool // want "type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum."
30+
31+
type BoolAliasPtr *bool // want "type BoolAliasPtr pointer should not use a bool. Use a string type with meaningful constant values as an enum."
32+
33+
type BoolAliasSlice []bool // want "type BoolAliasSlice array element should not use a bool. Use a string type with meaningful constant values as an enum."
34+
35+
type BoolAliasPtrSlice []*bool // want "type BoolAliasPtrSlice array element pointer should not use a bool. Use a string type with meaningful constant values as an enum."

pkg/analysis/registry.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/JoelSpeed/kal/pkg/analysis/conditions"
88
"github.com/JoelSpeed/kal/pkg/analysis/integers"
99
"github.com/JoelSpeed/kal/pkg/analysis/jsontags"
10+
"github.com/JoelSpeed/kal/pkg/analysis/nobools"
1011
"github.com/JoelSpeed/kal/pkg/analysis/nophase"
1112
"github.com/JoelSpeed/kal/pkg/analysis/optionalorrequired"
1213
"github.com/JoelSpeed/kal/pkg/analysis/requiredfields"
@@ -56,6 +57,7 @@ func NewRegistry() Registry {
5657
commentstart.Initializer(),
5758
integers.Initializer(),
5859
jsontags.Initializer(),
60+
nobools.Initializer(),
5961
nophase.Initializer(),
6062
optionalorrequired.Initializer(),
6163
requiredfields.Initializer(),

pkg/analysis/registry_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var _ = Describe("Registry", func() {
3535
"commentstart",
3636
"integers",
3737
"jsontags",
38+
"nobools",
3839
"nophase",
3940
"optionalorrequired",
4041
"requiredfields",

0 commit comments

Comments
 (0)