Skip to content

Commit 2d06435

Browse files
authored
Merge pull request #189 from JoelSpeed/nonpointerstructs
New Linter: NonPointerStructs
2 parents 8e86c46 + 72222be commit 2d06435

File tree

14 files changed

+563
-14
lines changed

14 files changed

+563
-14
lines changed

docs/linters.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [NoDurations](#nodurations) - Prevents usage of duration types
1717
- [NoFloats](#nofloats) - Prevents usage of floating-point types
1818
- [Nomaps](#nomaps) - Restricts usage of map types
19+
- [NonPointerStructs](#nonpointerstructs) - Ensures non-pointer structs are marked correctly with required/optional markers
1920
- [NoNullable](#nonullable) - Prevents usage of the nullable marker
2021
- [Nophase](#nophase) - Prevents usage of 'Phase' fields
2122
- [Notimestamp](#notimestamp) - Prevents usage of 'TimeStamp' fields
@@ -514,6 +515,35 @@ lintersConfig:
514515
policy: Enforce | AllowStringToStringMaps | Ignore # Determines how the linter should handle maps of simple types. Defaults to AllowStringToStringMaps.
515516
```
516517

518+
## NonPointerStructs
519+
520+
The `nonpointerstructs` linter checks that non-pointer structs that contain required fields are marked as required.
521+
Non-pointer structs that contain no required fields are marked as optional.
522+
523+
This linter is important for types validated in Go as there is no way to validate the optionality of the fields at runtime,
524+
aside from checking the fields within them.
525+
526+
If a struct is marked required, this can only be validated by having a required field within it.
527+
If there are no required fields, the struct is implicitly optional and must be marked as so.
528+
529+
To have an optional struct field that includes required fields, the struct must be a pointer.
530+
To have a required struct field that includes no required fields, the struct must be a pointer.
531+
532+
### Configuration
533+
534+
```yaml
535+
lintersConfig:
536+
nonpointerstructs:
537+
preferredRequiredMarker: required | kubebuilder:validation:Required | k8s:required # The preferred required marker to use for required fields when providing fixes. Defaults to `required`.
538+
preferredOptionalMarker: optional | kubebuilder:validation:Optional | k8s:optional # The preferred optional marker to use for optional fields when providing fixes. Defaults to `optional`.
539+
```
540+
541+
### Fixes
542+
543+
The `nonpointerstructs` linter can automatically fix non-pointer struct fields that are not marked as required or optional.
544+
545+
It will suggest to mark the field as required or optional, depending on the fields within the non-pointer struct.
546+
517547
## NoNullable
518548

519549
The `nonullable` linter ensures that types and fields do not have the `nullable` marker.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package nonpointerstructs
17+
18+
import (
19+
"fmt"
20+
"go/ast"
21+
22+
"golang.org/x/tools/go/analysis"
23+
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
24+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
25+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
26+
markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
27+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
28+
"sigs.k8s.io/kube-api-linter/pkg/markers"
29+
)
30+
31+
const name = "nonpointerstructs"
32+
33+
func newAnalyzer(cfg *Config) *analysis.Analyzer {
34+
if cfg == nil {
35+
cfg = &Config{}
36+
}
37+
38+
defaultConfig(cfg)
39+
40+
a := &analyzer{
41+
preferredRequiredMarker: cfg.PreferredRequiredMarker,
42+
preferredOptionalMarker: cfg.PreferredOptionalMarker,
43+
}
44+
45+
return &analysis.Analyzer{
46+
Name: name,
47+
Doc: "Checks that non-pointer structs that contain required fields are marked as required. Non-pointer structs that contain no required fields are marked as optional.",
48+
Run: a.run,
49+
Requires: []*analysis.Analyzer{inspector.Analyzer},
50+
}
51+
}
52+
53+
type analyzer struct {
54+
preferredRequiredMarker string
55+
preferredOptionalMarker string
56+
}
57+
58+
func (a *analyzer) run(pass *analysis.Pass) (any, error) {
59+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
60+
if !ok {
61+
return nil, kalerrors.ErrCouldNotGetInspector
62+
}
63+
64+
inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) {
65+
a.checkField(pass, field, markersAccess, jsonTagInfo, qualifiedFieldName)
66+
})
67+
68+
return nil, nil //nolint:nilnil
69+
}
70+
71+
func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) {
72+
if field.Type == nil {
73+
return
74+
}
75+
76+
if jsonTagInfo.Inline {
77+
return
78+
}
79+
80+
structType, ok := asNonPointerStruct(pass, field.Type)
81+
if !ok {
82+
return
83+
}
84+
85+
hasRequiredField := hasRequiredField(structType, markersAccess)
86+
isOptional := utils.IsFieldOptional(field, markersAccess)
87+
isRequired := utils.IsFieldRequired(field, markersAccess)
88+
89+
switch {
90+
case hasRequiredField && isRequired, !hasRequiredField && isOptional:
91+
// This is the desired case.
92+
case hasRequiredField:
93+
a.handleShouldBeRequired(pass, field, markersAccess, qualifiedFieldName)
94+
case !hasRequiredField:
95+
a.handleShouldBeOptional(pass, field, markersAccess, qualifiedFieldName)
96+
}
97+
}
98+
99+
func asNonPointerStruct(pass *analysis.Pass, field ast.Expr) (*ast.StructType, bool) {
100+
switch typ := field.(type) {
101+
case *ast.StructType:
102+
return typ, true
103+
case *ast.Ident:
104+
typeSpec, ok := utils.LookupTypeSpec(pass, typ)
105+
if !ok {
106+
return nil, false
107+
}
108+
109+
return asNonPointerStruct(pass, typeSpec.Type)
110+
default:
111+
return nil, false
112+
}
113+
}
114+
115+
func hasRequiredField(structType *ast.StructType, markersAccess markershelper.Markers) bool {
116+
for _, field := range structType.Fields.List {
117+
if utils.IsFieldRequired(field, markersAccess) {
118+
return true
119+
}
120+
}
121+
122+
structMarkers := markersAccess.StructMarkers(structType)
123+
124+
if structMarkers.Has(markers.KubebuilderMinPropertiesMarker) && !structMarkers.HasWithValue(fmt.Sprintf("%s=0", markers.KubebuilderMinPropertiesMarker)) {
125+
// A non-zero min properties marker means that the struct is validated to have at least one field.
126+
// This means it can be treated the same as having a required field.
127+
return true
128+
}
129+
130+
return false
131+
}
132+
133+
func defaultConfig(cfg *Config) {
134+
if cfg.PreferredRequiredMarker == "" {
135+
cfg.PreferredRequiredMarker = markers.RequiredMarker
136+
}
137+
138+
if cfg.PreferredOptionalMarker == "" {
139+
cfg.PreferredOptionalMarker = markers.OptionalMarker
140+
}
141+
}
142+
143+
func (a *analyzer) handleShouldBeRequired(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) {
144+
fieldMarkers := markersAccess.FieldMarkers(field)
145+
146+
textEdits := []analysis.TextEdit{}
147+
148+
for _, marker := range []string{markers.OptionalMarker, markers.KubebuilderOptionalMarker, markers.K8sOptionalMarker} {
149+
for _, m := range fieldMarkers.Get(marker) {
150+
textEdits = append(textEdits, analysis.TextEdit{
151+
Pos: m.Pos,
152+
End: m.End + 1, // Add 1 to include the newline character
153+
NewText: nil,
154+
})
155+
}
156+
}
157+
158+
textEdits = append(textEdits, analysis.TextEdit{
159+
Pos: field.Pos(),
160+
End: field.Pos(),
161+
NewText: fmt.Appendf(nil, "// +%s\n", a.preferredRequiredMarker),
162+
})
163+
164+
pass.Report(analysis.Diagnostic{
165+
Pos: field.Pos(),
166+
End: field.Pos(),
167+
Message: fmt.Sprintf("field %s is a non-pointer struct with required fields. It must be marked as required.", qualifiedFieldName),
168+
SuggestedFixes: []analysis.SuggestedFix{
169+
{
170+
Message: "should mark the field as required",
171+
TextEdits: textEdits,
172+
},
173+
},
174+
})
175+
}
176+
177+
func (a *analyzer) handleShouldBeOptional(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) {
178+
fieldMarkers := markersAccess.FieldMarkers(field)
179+
180+
textEdits := []analysis.TextEdit{}
181+
182+
for _, marker := range []string{markers.RequiredMarker, markers.KubebuilderRequiredMarker, markers.K8sRequiredMarker} {
183+
for _, m := range fieldMarkers.Get(marker) {
184+
textEdits = append(textEdits, analysis.TextEdit{
185+
Pos: m.Pos,
186+
End: m.End + 1, // Add 1 to include the newline character
187+
NewText: nil,
188+
})
189+
}
190+
}
191+
192+
textEdits = append(textEdits, analysis.TextEdit{
193+
Pos: field.Pos(),
194+
End: field.Pos(),
195+
NewText: fmt.Appendf(nil, "// +%s\n", a.preferredOptionalMarker),
196+
})
197+
198+
pass.Report(analysis.Diagnostic{
199+
Pos: field.Pos(),
200+
Message: fmt.Sprintf("field %s is a non-pointer struct with no required fields. It must be marked as optional.", qualifiedFieldName),
201+
SuggestedFixes: []analysis.SuggestedFix{
202+
{
203+
Message: "should mark the field as optional",
204+
TextEdits: textEdits,
205+
},
206+
},
207+
})
208+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package nonpointerstructs_test
17+
18+
import (
19+
"testing"
20+
21+
"golang.org/x/tools/go/analysis/analysistest"
22+
"sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs"
23+
)
24+
25+
func Test(t *testing.T) {
26+
testdata := analysistest.TestData()
27+
28+
analyzer, err := nonpointerstructs.Initializer().Init(&nonpointerstructs.Config{})
29+
if err != nil {
30+
t.Fatalf("initializing nonpointerstructs linter: %v", err)
31+
}
32+
33+
analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a")
34+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package nonpointerstructs
17+
18+
// Config is the configuration for the nonpointerstructs linter.
19+
type Config struct {
20+
// preferredRequiredMarker is the preferred marker to use for required fields when providing fixes.
21+
// If this field is not set, the default value is "required".
22+
// Valid values are "required" and "kubebuilder:validation:Required" and "k8s:required".
23+
PreferredRequiredMarker string `json:"preferredRequiredMarker"`
24+
25+
// preferredOptionalMarker is the preferred marker to use for optional fields when providing fixes.
26+
// If this field is not set, the default value is "optional".
27+
// Valid values are "optional" and "kubebuilder:validation:Optional" and "k8s:optional".
28+
PreferredOptionalMarker string `json:"preferredOptionalMarker"`
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/*
18+
nonpointerstructs is a linter that checks that non-pointer structs that contain required fields are marked as required.
19+
Non-pointer structs that contain no required fields are marked as optional.
20+
21+
This linter is important for types validated in Go as there is no way to validate the optionality of the fields at runtime,
22+
aside from checking the fields within them.
23+
24+
If a struct is marked required, this can only be validated by having a required field within it.
25+
If there are no required fields, the struct is implicitly optional and must be marked as so.
26+
27+
To have an optional struct field that includes required fields, the struct must be a pointer.
28+
To have a required struct field that includes no required fields, the struct must be a pointer.
29+
*/
30+
package nonpointerstructs

0 commit comments

Comments
 (0)