Skip to content

Commit 72222be

Browse files
committed
Add automatic fixes to nonpointerstructs
1 parent 181ff36 commit 72222be

File tree

6 files changed

+255
-11
lines changed

6 files changed

+255
-11
lines changed

docs/linters.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,21 @@ If there are no required fields, the struct is implicitly optional and must be m
529529
To have an optional struct field that includes required fields, the struct must be a pointer.
530530
To have a required struct field that includes no required fields, the struct must be a pointer.
531531

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+
532547
## NoNullable
533548

534549
The `nonullable` linter ensures that types and fields do not have the `nullable` marker.

pkg/analysis/nonpointerstructs/analyzer.go

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,29 +30,45 @@ import (
3030

3131
const name = "nonpointerstructs"
3232

33-
func newAnalyzer() *analysis.Analyzer {
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+
3445
return &analysis.Analyzer{
3546
Name: name,
3647
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.",
37-
Run: run,
48+
Run: a.run,
3849
Requires: []*analysis.Analyzer{inspector.Analyzer},
3950
}
4051
}
4152

42-
func run(pass *analysis.Pass) (any, error) {
53+
type analyzer struct {
54+
preferredRequiredMarker string
55+
preferredOptionalMarker string
56+
}
57+
58+
func (a *analyzer) run(pass *analysis.Pass) (any, error) {
4359
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
4460
if !ok {
4561
return nil, kalerrors.ErrCouldNotGetInspector
4662
}
4763

4864
inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) {
49-
checkField(pass, field, markersAccess, jsonTagInfo, qualifiedFieldName)
65+
a.checkField(pass, field, markersAccess, jsonTagInfo, qualifiedFieldName)
5066
})
5167

5268
return nil, nil //nolint:nilnil
5369
}
5470

55-
func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) {
71+
func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) {
5672
if field.Type == nil {
5773
return
5874
}
@@ -74,9 +90,9 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelp
7490
case hasRequiredField && isRequired, !hasRequiredField && isOptional:
7591
// This is the desired case.
7692
case hasRequiredField:
77-
pass.Reportf(field.Pos(), "field %s is a non-pointer struct with required fields. It must be marked as required.", qualifiedFieldName)
93+
a.handleShouldBeRequired(pass, field, markersAccess, qualifiedFieldName)
7894
case !hasRequiredField:
79-
pass.Reportf(field.Pos(), "field %s is a non-pointer struct with no required fields. It must be marked as optional.", qualifiedFieldName)
95+
a.handleShouldBeOptional(pass, field, markersAccess, qualifiedFieldName)
8096
}
8197
}
8298

@@ -113,3 +129,80 @@ func hasRequiredField(structType *ast.StructType, markersAccess markershelper.Ma
113129

114130
return false
115131
}
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+
}

pkg/analysis/nonpointerstructs/analyzer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import (
2525
func Test(t *testing.T) {
2626
testdata := analysistest.TestData()
2727

28-
analyzer, err := nonpointerstructs.Initializer().Init(nil)
28+
analyzer, err := nonpointerstructs.Initializer().Init(&nonpointerstructs.Config{})
2929
if err != nil {
3030
t.Fatalf("initializing nonpointerstructs linter: %v", err)
3131
}
3232

33-
analysistest.Run(t, testdata, analyzer, "a")
33+
analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a")
3434
}
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+
}

pkg/analysis/nonpointerstructs/initializer.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ limitations under the License.
1616
package nonpointerstructs
1717

1818
import (
19+
"fmt"
20+
21+
"golang.org/x/tools/go/analysis"
22+
"k8s.io/apimachinery/pkg/util/validation/field"
1923
"sigs.k8s.io/kube-api-linter/pkg/analysis/initializer"
2024
"sigs.k8s.io/kube-api-linter/pkg/analysis/registry"
25+
"sigs.k8s.io/kube-api-linter/pkg/markers"
2126
)
2227

2328
func init() {
@@ -27,9 +32,36 @@ func init() {
2732
// Initializer returns the AnalyzerInitializer for this
2833
// Analyzer so that it can be added to the registry.
2934
func Initializer() initializer.AnalyzerInitializer {
30-
return initializer.NewInitializer(
35+
return initializer.NewConfigurableInitializer(
3136
name,
32-
newAnalyzer(),
37+
initAnalyzer,
3338
true,
39+
validateConfig,
3440
)
3541
}
42+
43+
func initAnalyzer(cfg *Config) (*analysis.Analyzer, error) {
44+
return newAnalyzer(cfg), nil
45+
}
46+
47+
func validateConfig(cfg *Config, fldPath *field.Path) field.ErrorList {
48+
if cfg == nil {
49+
return field.ErrorList{}
50+
}
51+
52+
fieldErrors := field.ErrorList{}
53+
54+
switch cfg.PreferredRequiredMarker {
55+
case "", markers.RequiredMarker, markers.KubebuilderRequiredMarker, markers.K8sRequiredMarker:
56+
default:
57+
fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("preferredRequiredMarker"), cfg.PreferredRequiredMarker, fmt.Sprintf("invalid value, must be one of %q, %q, %q or omitted", markers.RequiredMarker, markers.KubebuilderRequiredMarker, markers.K8sRequiredMarker)))
58+
}
59+
60+
switch cfg.PreferredOptionalMarker {
61+
case "", markers.OptionalMarker, markers.KubebuilderOptionalMarker, markers.K8sOptionalMarker:
62+
default:
63+
fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("preferredOptionalMarker"), cfg.PreferredOptionalMarker, fmt.Sprintf("invalid value, must be one of %q, %q, %q or omitted", markers.OptionalMarker, markers.KubebuilderOptionalMarker, markers.K8sOptionalMarker)))
64+
}
65+
66+
return fieldErrors
67+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package a
2+
3+
type A struct {
4+
// +required
5+
WithRequired WithRequiredField `json:"withRequired"` // want "field A.WithRequired is a non-pointer struct with required fields. It must be marked as required."
6+
// +optional
7+
WithOptional WithOptionalField `json:"withOptional"` // want "field A.WithOptional is a non-pointer struct with no required fields. It must be marked as optional."
8+
// +required
9+
WithRequiredAndOptional WithRequiredAndOptionalField `json:"withRequiredAndOptional"` // want "field A.WithRequiredAndOptional is a non-pointer struct with required fields. It must be marked as required."
10+
// +required
11+
WithOptionalAndMinProperties WithOptionalFieldsAndMinProperties `json:"withOptionalAndMinProperties"` // want "field A.WithOptionalAndMinProperties is a non-pointer struct with required fields. It must be marked as required."
12+
13+
// No diagnostic with correct markers
14+
15+
// +required
16+
WithRequiredFieldCorrect WithRequiredField `json:"withRequiredFieldCorrect"`
17+
18+
// +optional
19+
WithOptionalFieldCorrect WithOptionalField `json:"withOptionalFieldCorrect"`
20+
21+
// +kubebuilder:validation:Required
22+
WithRequiredAndOptionalFieldCorrect WithRequiredAndOptionalField `json:"withRequiredAndOptionalFieldCorrect"`
23+
24+
// +k8s:required
25+
WithOptionalFieldsAndMinPropertiesCorrect WithOptionalFieldsAndMinProperties `json:"withOptionalFieldsAndMinPropertiesCorrect"`
26+
27+
// Diagnostic with incorrect markers
28+
29+
// +required
30+
WithRequiredFieldIncorrect WithRequiredField `json:"withRequiredFieldIncorrect"` // want "field A.WithRequiredFieldIncorrect is a non-pointer struct with required fields. It must be marked as required."
31+
32+
// +optional
33+
WithOptionalFieldIncorrect WithOptionalField `json:"withOptionalFieldIncorrect"` // want "field A.WithOptionalFieldIncorrect is a non-pointer struct with no required fields. It must be marked as optional."
34+
35+
// +required
36+
WithRequiredAndOptionalFieldIncorrect WithRequiredAndOptionalField `json:"withRequiredAndOptionalFieldIncorrect"` // want "field A.WithRequiredAndOptionalFieldIncorrect is a non-pointer struct with required fields. It must be marked as required."
37+
38+
// +required
39+
WithOptionalFieldsAndMinPropertiesIncorrect WithOptionalFieldsAndMinProperties `json:"withOptionalFieldsAndMinPropertiesIncorrect"` // want "field A.WithOptionalFieldsAndMinPropertiesIncorrect is a non-pointer struct with required fields. It must be marked as required."
40+
41+
// Pointers are ignored in this linter.
42+
WithRequiredFieldAndPointer *WithRequiredField `json:"withRequiredFieldAndPointer"`
43+
WithOptionalFieldAndPointer *WithOptionalField `json:"withOptionalFieldAndPointer"`
44+
WithRequiredAndOptionalFieldAndPointer *WithRequiredAndOptionalField `json:"withRequiredAndOptionalFieldAndPointer"`
45+
WithOptionalFieldsAndMinPropertiesAndPointer *WithOptionalFieldsAndMinProperties `json:"withOptionalFieldsAndMinPropertiesAndPointer"`
46+
47+
// Inline structs are ignored in this linter.
48+
WithRequiredField `json:",inline"`
49+
WithOptionalField `json:",inline"`
50+
WithRequiredAndOptionalField `json:",inline"`
51+
WithOptionalFieldsAndMinProperties `json:",inline"`
52+
}
53+
54+
type WithRequiredField struct {
55+
// +required
56+
RequiredField string `json:"requiredField"`
57+
}
58+
59+
type WithOptionalField struct {
60+
// +optional
61+
OptionalField string `json:"optionalField"`
62+
}
63+
64+
type WithRequiredAndOptionalField struct {
65+
// +k8s:required
66+
RequiredField string `json:"requiredField"`
67+
// +k8s:optional
68+
OptionalField string `json:"optionalField"`
69+
}
70+
71+
// +kubebuilder:validation:MinProperties=1
72+
type WithOptionalFieldsAndMinProperties struct {
73+
// +k8s:optional
74+
OptionalField string `json:"optionalField"`
75+
}

0 commit comments

Comments
 (0)