Skip to content

Commit 379949d

Browse files
sivchariJoelSpeed
authored andcommitted
add nomaps linter
Signed-off-by: sivchari <shibuuuu5@gmail.com>
1 parent abd233a commit 379949d

File tree

15 files changed

+609
-0
lines changed

15 files changed

+609
-0
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ vendor: ## Ensure the vendor directory is up to date.
4343
lint: ## Run golangci-lint over the codebase.
4444
${GOLANGCI_LINT} run ./... --timeout 5m -v
4545

46+
.PHONY: lint-fix
47+
lint-fix: $(GOLANGCI_LINT) ## Run golangci-lint over the codebase and run auto-fixers if supported by the linter
48+
${GOLANGCI_LINT} run ./... --timeout 5m -v --fix
49+
4650
.PHONY: test
4751
test: fmt vet unit ## Run tests.
4852

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,20 @@ Floating-point values cannot be reliably round-tripped without changing and have
239239
Their use should be avoided as much as possible.
240240
They should never be used in spec.
241241

242+
## Nomaps
243+
244+
The `nomaps` linter checks the usage of map types.
245+
246+
Maps are discouraged apart from `map[string]string` which is used for labels and annotations in Kubernetes APIs since it's hard to distinguish between structs and maps in spec. Instead of plain map, lists of named subobjects are preferred.
247+
248+
### Configuration
249+
250+
```yaml
251+
lintersConfig:
252+
nomaps:
253+
policy: Enforce | AllowStringToStringMaps | Ignore # Determines how the linter should handle maps of simple types. Defaults to AllowStringToStringMaps.
254+
```
255+
242256
## Nophase
243257

244258
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/nomaps/analyzer.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package nomaps
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"go/ast"
7+
"go/token"
8+
"go/types"
9+
10+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/extractjsontags"
11+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/inspector"
12+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
13+
"github.com/JoelSpeed/kal/pkg/config"
14+
"golang.org/x/tools/go/analysis"
15+
)
16+
17+
const (
18+
name = "nomaps"
19+
)
20+
21+
var (
22+
errCouldNotGetInspector = errors.New("could not get inspector")
23+
)
24+
25+
type analyzer struct {
26+
policy config.NoMapsPolicy
27+
}
28+
29+
// newAnalyzer creates a new analyzer.
30+
func newAnalyzer(cfg config.NoMapsConfig) *analysis.Analyzer {
31+
defaultConfig(&cfg)
32+
33+
a := &analyzer{
34+
policy: cfg.Policy,
35+
}
36+
37+
return &analysis.Analyzer{
38+
Name: name,
39+
Doc: "Checks for usage of map types. Maps are discouraged apart from `map[string]string` which is used for labels and annotations. Use a list of named objects instead.",
40+
Run: a.run,
41+
Requires: []*analysis.Analyzer{inspector.Analyzer},
42+
}
43+
}
44+
45+
func (a *analyzer) run(pass *analysis.Pass) (any, error) {
46+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
47+
if !ok {
48+
return nil, errCouldNotGetInspector
49+
}
50+
51+
inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) {
52+
a.checkField(pass, field)
53+
})
54+
55+
return nil, nil //nolint:nilnil
56+
}
57+
58+
func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field) {
59+
stringToStringMapType := types.NewMap(types.Typ[types.String], types.Typ[types.String])
60+
61+
underlyingType := pass.TypesInfo.TypeOf(field.Type).Underlying()
62+
63+
if ptr, ok := underlyingType.(*types.Pointer); ok {
64+
underlyingType = ptr.Elem().Underlying()
65+
}
66+
67+
m, ok := underlyingType.(*types.Map)
68+
if !ok {
69+
return
70+
}
71+
72+
if a.policy == config.NoMapsEnforce {
73+
report(pass, field.Pos(), field.Names[0].Name)
74+
return
75+
}
76+
77+
if a.policy == config.NoMapsAllowStringToStringMaps {
78+
if types.Identical(m, stringToStringMapType) {
79+
return
80+
}
81+
82+
report(pass, field.Pos(), field.Names[0].Name)
83+
}
84+
85+
if a.policy == config.NoMapsIgnore {
86+
key := m.Key().Underlying()
87+
_, ok := key.(*types.Basic)
88+
89+
elm := m.Elem().Underlying()
90+
_, ok2 := elm.(*types.Basic)
91+
92+
if ok && ok2 {
93+
return
94+
}
95+
96+
report(pass, field.Pos(), field.Names[0].Name)
97+
}
98+
}
99+
100+
func report(pass *analysis.Pass, pos token.Pos, fieldName string) {
101+
pass.Report(analysis.Diagnostic{
102+
Pos: pos,
103+
Message: fmt.Sprintf("%s should not use a map type, use a list type with a unique name/identifier instead", fieldName),
104+
})
105+
}
106+
107+
func defaultConfig(cfg *config.NoMapsConfig) {
108+
if cfg.Policy == "" {
109+
cfg.Policy = config.NoMapsAllowStringToStringMaps
110+
}
111+
}

pkg/analysis/nomaps/analyzer_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package nomaps_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/JoelSpeed/kal/pkg/analysis/nomaps"
7+
"github.com/JoelSpeed/kal/pkg/config"
8+
"golang.org/x/tools/go/analysis/analysistest"
9+
)
10+
11+
func Test(t *testing.T) {
12+
testdata := analysistest.TestData()
13+
14+
a, err := nomaps.Initializer().Init(config.LintersConfig{})
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
19+
analysistest.Run(t, testdata, a, "a")
20+
}
21+
22+
func TestWithEnforce(t *testing.T) {
23+
testdata := analysistest.TestData()
24+
25+
a, err := nomaps.Initializer().Init(config.LintersConfig{
26+
NoMaps: config.NoMapsConfig{
27+
Policy: config.NoMapsEnforce,
28+
},
29+
})
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
analysistest.Run(t, testdata, a, "b")
35+
}
36+
37+
func TestWithAllowStringToStringMaps(t *testing.T) {
38+
testdata := analysistest.TestData()
39+
40+
a, err := nomaps.Initializer().Init(config.LintersConfig{
41+
NoMaps: config.NoMapsConfig{
42+
Policy: config.NoMapsAllowStringToStringMaps,
43+
},
44+
})
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
analysistest.Run(t, testdata, a, "c")
50+
}
51+
52+
func TestWithIgnore(t *testing.T) {
53+
testdata := analysistest.TestData()
54+
55+
a, err := nomaps.Initializer().Init(config.LintersConfig{
56+
NoMaps: config.NoMapsConfig{
57+
Policy: config.NoMapsIgnore,
58+
},
59+
})
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
64+
analysistest.Run(t, testdata, a, "d")
65+
}

pkg/analysis/nomaps/doc.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
nomaps provides a linter to ensure that fields do not use map types.
3+
4+
Maps are discouraged in Kubernetes APIs. It is hard to distinguish between structs and maps in JSON/YAML and as such, lists of named subobjects are preferred over plain map types.
5+
6+
Instead of
7+
8+
ports:
9+
www:
10+
containerPort: 80
11+
12+
use
13+
14+
ports:
15+
- name: www
16+
containerPort: 80
17+
18+
Lists should use the `+listType=map` and `+listMapKey=name` markers, or equivalent.
19+
*/
20+
21+
package nomaps

pkg/analysis/nomaps/initializer.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package nomaps
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 newAnalyzer(cfg.NoMaps), nil
25+
}
26+
27+
// Default determines whether this Analyzer is on by default, or not.
28+
func (initializer) Default() bool {
29+
return true
30+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package a
2+
3+
type (
4+
MapStringComponent map[string]Component
5+
PtrMapStringComponent *map[string]Component
6+
MapStringInt map[string]int
7+
MapIntString map[int]string
8+
)
9+
10+
type (
11+
MapStringComponentAlias = map[string]Component
12+
MapStringPtrComponentAlias = *map[string]Component
13+
MapStringIntAlias = map[string]int
14+
DefinedMapStringComponentAlias = MapStringComponent
15+
DefinedMapStringComponentPtrAlias = *MapStringComponent
16+
)
17+
18+
type (
19+
MapStringGenerics[V any] map[string]V
20+
MapIntGenerics[V any] map[int]V
21+
MapComparableKeyString[K comparable] map[K]string
22+
MapComparableKeyInt[K comparable] map[K]int
23+
)
24+
25+
type NoMapsTestStruct struct {
26+
Primitive int32 `json:"primitive"`
27+
Components []Component `json:"components"`
28+
MapComponents map[string]Component `json:"mapComponents"` // want "MapComponents should not use a map type, use a list type with a unique name/identifier instead"
29+
PtrMapComponents *map[string]Component `json:"mapComponents"` // want "MapComponents should not use a map type, use a list type with a unique name/identifier instead"
30+
MapStringInt map[string]int `json:"mapStringInt"` // want "MapStringInt should not use a map type, use a list type with a unique name/identifier instead"
31+
Labels map[string]string `json:"specialCase"`
32+
}
33+
34+
type NoMapsTestStructWithDefiningType struct {
35+
MapStringComponent MapStringComponent `json:"mapStringComponent"` // want "MapStringComponent should not use a map type, use a list type with a unique name/identifier instead"
36+
PtrMapStringComponent PtrMapStringComponent `json:"ptrMapStringComponent"` // want "PtrMapStringComponent should not use a map type, use a list type with a unique name/identifier instead"
37+
MapStringInt MapStringInt `json:"mapStringInt"` // want "MapStringInt should not use a map type, use a list type with a unique name/identifier instead"
38+
MapIntString MapIntString `json:"mapIntString"` // want "MapIntString should not use a map type, use a list type with a unique name/identifier instead"
39+
}
40+
41+
type NoMapsTestStructWithAlias struct {
42+
MapStringComponentAlias MapStringComponentAlias `json:"mapStringComponentAlias"` // want "MapStringComponentAlias should not use a map type, use a list type with a unique name/identifier instead"
43+
MapStringPtrComponentAlias MapStringPtrComponentAlias `json:"mapStringPtrComponentAlias"` // want "MapStringPtrComponentAlias should not use a map type, use a list type with a unique name/identifier instead"
44+
MapStringIntAlias MapStringIntAlias `json:"mapStringIntAlias"` // want "MapStringIntAlias should not use a map type, use a list type with a unique name/identifier instead"
45+
DefinedMapStringComponentAlias DefinedMapStringComponentAlias `json:"definedMapStringComponentAlias"` // want "DefinedMapStringComponentAlias should not use a map type, use a list type with a unique name/identifier instead"
46+
DefinedMapStringComponentPtrAlias DefinedMapStringComponentPtrAlias `json:"definedMapStringComponentPtrAlias"` // want "DefinedMapStringComponentPtrAlias should not use a map type, use a list type with a unique name/identifier instead"
47+
}
48+
49+
type NoMapsTestStructWithGenerics[K comparable, V any] struct {
50+
MapStringGenerics MapStringGenerics[V] `json:"mapStringGenerics"` // want "MapStringGenerics should not use a map type, use a list type with a unique name/identifier instead"
51+
MapIntGenerics MapIntGenerics[V] `json:"mapIntGenerics"` // want "MapIntGenerics should not use a map type, use a list type with a unique name/identifier instead"
52+
MapComparableKeyString MapComparableKeyString[K] `json:"mapComparableKeyString"` // want "MapComparableKeyString should not use a map type, use a list type with a unique name/identifier instead"
53+
MapComparableKeyInt MapComparableKeyInt[K] `json:"mapComparableKeyInt"` // want "MapComparableKeyInt should not use a map type, use a list type with a unique name/identifier instead"
54+
}
55+
56+
type NoMapsTestStructWithEmbedded struct {
57+
NoMapsTestStruct
58+
NoMapsTestStructWithDefiningType
59+
NoMapsTestStructWithGenerics[string, Component]
60+
NoMapsTestStructWithAlias
61+
}
62+
63+
type Component struct {
64+
Key string `json:"key"`
65+
Value int32 `json:"value"`
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package b
2+
3+
type (
4+
MapStringComponent map[string]Component
5+
PtrMapStringComponent *map[string]Component
6+
MapStringInt map[string]int
7+
MapIntString map[int]string
8+
)
9+
10+
type (
11+
MapStringComponentAlias = map[string]Component
12+
MapStringPtrComponentAlias = *map[string]Component
13+
MapStringIntAlias = map[string]int
14+
DefinedMapStringComponentAlias = MapStringComponent
15+
DefinedMapStringComponentPtrAlias = *MapStringComponent
16+
)
17+
18+
type (
19+
MapStringGenerics[V any] map[string]V
20+
MapIntGenerics[V any] map[int]V
21+
MapComparableKeyString[K comparable] map[K]string
22+
MapComparableKeyInt[K comparable] map[K]int
23+
)
24+
25+
type NoMapsTestStruct struct {
26+
Primitive int32 `json:"primitive"`
27+
Components []Component `json:"components"`
28+
MapComponents map[string]Component `json:"mapComponents"` // want "MapComponents should not use a map type, use a list type with a unique name/identifier instead"
29+
PtrMapComponents *map[string]Component `json:"mapComponents"` // want "MapComponents should not use a map type, use a list type with a unique name/identifier instead"
30+
MapStringInt map[string]int `json:"mapStringInt"` // want "MapStringInt should not use a map type, use a list type with a unique name/identifier instead"
31+
Labels map[string]string `json:"specialCase"` // want "Labels should not use a map type, use a list type with a unique name/identifier instead"
32+
}
33+
34+
type NoMapsTestStructWithDefiningType struct {
35+
MapStringComponent MapStringComponent `json:"mapStringComponent"` // want "MapStringComponent should not use a map type, use a list type with a unique name/identifier instead"
36+
PtrMapStringComponent PtrMapStringComponent `json:"ptrMapStringComponent"` // want "PtrMapStringComponent should not use a map type, use a list type with a unique name/identifier instead"
37+
MapStringInt MapStringInt `json:"mapStringInt"` // want "MapStringInt should not use a map type, use a list type with a unique name/identifier instead"
38+
MapIntString MapIntString `json:"mapIntString"` // want "MapIntString should not use a map type, use a list type with a unique name/identifier instead"
39+
}
40+
41+
type NoMapsTestStructWithAlias struct {
42+
MapStringComponentAlias MapStringComponentAlias `json:"mapStringComponentAlias"` // want "MapStringComponentAlias should not use a map type, use a list type with a unique name/identifier instead"
43+
MapStringPtrComponentAlias MapStringPtrComponentAlias `json:"mapStringPtrComponentAlias"` // want "MapStringPtrComponentAlias should not use a map type, use a list type with a unique name/identifier instead"
44+
MapStringIntAlias MapStringIntAlias `json:"mapStringIntAlias"` // want "MapStringIntAlias should not use a map type, use a list type with a unique name/identifier instead"
45+
DefinedMapStringComponentAlias DefinedMapStringComponentAlias `json:"definedMapStringComponentAlias"` // want "DefinedMapStringComponentAlias should not use a map type, use a list type with a unique name/identifier instead"
46+
DefinedMapStringComponentPtrAlias DefinedMapStringComponentPtrAlias `json:"definedMapStringComponentPtrAlias"` // want "DefinedMapStringComponentPtrAlias should not use a map type, use a list type with a unique name/identifier instead"
47+
}
48+
49+
type NoMapsTestStructWithGenerics[K comparable, V any] struct {
50+
MapStringGenerics MapStringGenerics[V] `json:"mapStringGenerics"` // want "MapStringGenerics should not use a map type, use a list type with a unique name/identifier instead"
51+
MapIntGenerics MapIntGenerics[V] `json:"mapIntGenerics"` // want "MapIntGenerics should not use a map type, use a list type with a unique name/identifier instead"
52+
MapComparableKeyString MapComparableKeyString[K] `json:"mapComparableKeyString"` // want "MapComparableKeyString should not use a map type, use a list type with a unique name/identifier instead"
53+
MapComparableKeyInt MapComparableKeyInt[K] `json:"mapComparableKeyInt"` // want "MapComparableKeyInt should not use a map type, use a list type with a unique name/identifier instead"
54+
}
55+
56+
type NoMapsTestStructWithEmbedded struct {
57+
NoMapsTestStruct
58+
NoMapsTestStructWithDefiningType
59+
NoMapsTestStructWithGenerics[string, Component]
60+
NoMapsTestStructWithAlias
61+
}
62+
63+
type Component struct {
64+
Key string `json:"key"`
65+
Value int32 `json:"value"`
66+
}

0 commit comments

Comments
 (0)