Skip to content

Commit c7d211e

Browse files
committed
new linter: defaults
Signed-off-by: sivchari <shibuuuu5@gmail.com>
1 parent 91d1bdb commit c7d211e

File tree

12 files changed

+415
-3
lines changed

12 files changed

+415
-3
lines changed

docs/linters.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [CommentStart](#commentstart) - Ensures comments start with the serialized form of the type
66
- [ConflictingMarkers](#conflictingmarkers) - Detects mutually exclusive markers on the same field
77
- [DefaultOrRequired](#defaultorrequired) - Ensures fields marked as required do not have default values
8+
- [Defaults](#defaults) - Checks that fields with default markers are configured correctly
89
- [DuplicateMarkers](#duplicatemarkers) - Checks for exact duplicates of markers
910
- [DependentTags](#dependenttags) - Enforces dependencies between markers
1011
- [ForbiddenMarkers](#forbiddenmarkers) - Checks that no forbidden markers are present on types/fields.
@@ -212,6 +213,45 @@ The linter also detects conflicts with:
212213

213214
This linter is enabled by default and helps ensure that API designs are consistent and unambiguous about whether fields are truly required or have default values.
214215

216+
## Defaults
217+
218+
The `defaults` linter checks that fields with default markers are configured correctly.
219+
220+
Fields with default markers (`+default`, `+kubebuilder:default`, or `+k8s:default`) should also be marked as optional.
221+
Additionally, fields with default markers should have `omitempty` or `omitzero` in their json tags to ensure that
222+
the default values are applied correctly during serialization and deserialization.
223+
224+
### Example
225+
226+
A well-configured field with a default:
227+
228+
```go
229+
type MyStruct struct {
230+
// +optional
231+
// +default="default-value"
232+
Field string `json:"field,omitempty"`
233+
}
234+
```
235+
236+
The following issues will be flagged by the linter:
237+
238+
```go
239+
type MyStruct struct {
240+
// Missing optional marker
241+
// +default="value"
242+
Field1 string `json:"field1,omitempty"` // Error: has default but not marked as optional
243+
244+
// Missing omitempty tag
245+
// +optional
246+
// +default="value"
247+
Field2 string `json:"field2"` // Error: has default but no omitempty or omitzero
248+
}
249+
```
250+
251+
### Fixes
252+
253+
The `defaults` linter can automatically add `omitempty,omitzero` to the json tag when missing.
254+
215255
## DuplicateMarkers
216256

217257
The duplicatemarkers linter checks for exact duplicates of markers for types and fields.

pkg/analysis/defaults/analyzer.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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 defaults
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 (
32+
name = "defaults"
33+
)
34+
35+
func init() {
36+
markershelper.DefaultRegistry().Register(
37+
markers.DefaultMarker,
38+
markers.KubebuilderDefaultMarker,
39+
markers.K8sDefaultMarker,
40+
markers.OptionalMarker,
41+
markers.KubebuilderOptionalMarker,
42+
markers.K8sOptionalMarker,
43+
)
44+
}
45+
46+
// Analyzer is the analyzer for the defaults package.
47+
var Analyzer = &analysis.Analyzer{
48+
Name: name,
49+
Doc: `Checks that fields with default markers are configured correctly.
50+
Fields with default markers (+default, +kubebuilder:default, or +k8s:default) should also be marked as optional.
51+
Additionally, fields with default markers should have "omitempty" or "omitzero" in their json tags to ensure that the default values are applied correctly during serialization and deserialization.
52+
`,
53+
Run: run,
54+
Requires: []*analysis.Analyzer{inspector.Analyzer},
55+
}
56+
57+
func run(pass *analysis.Pass) (any, error) {
58+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
59+
if !ok {
60+
return nil, kalerrors.ErrCouldNotGetInspector
61+
}
62+
63+
inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) {
64+
checkField(pass, field, jsonTagInfo, markersAccess, qualifiedFieldName)
65+
})
66+
67+
return nil, nil //nolint:nilnil
68+
}
69+
70+
func checkField(pass *analysis.Pass, field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) {
71+
if field == nil || len(field.Names) == 0 {
72+
return
73+
}
74+
75+
fieldMarkers := markersAccess.FieldMarkers(field)
76+
77+
// Check for any default marker (+default, +kubebuilder:default, or +k8s:default)
78+
hasDefault := fieldMarkers.Has(markers.DefaultMarker)
79+
hasKubebuilderDefault := fieldMarkers.Has(markers.KubebuilderDefaultMarker)
80+
hasK8sDefault := fieldMarkers.Has(markers.K8sDefaultMarker)
81+
82+
if !hasDefault && !hasKubebuilderDefault && !hasK8sDefault {
83+
return
84+
}
85+
86+
if hasKubebuilderDefault {
87+
checkKubebuilderDefault(pass, field, fieldMarkers, qualifiedFieldName)
88+
}
89+
90+
checkDefaultOptional(pass, field, markersAccess, qualifiedFieldName)
91+
92+
checkDefaultOmitEmptyOrOmitZero(pass, field, jsonTagInfo, qualifiedFieldName)
93+
}
94+
95+
func checkKubebuilderDefault(pass *analysis.Pass, field *ast.Field, fieldMarkers markershelper.MarkerSet, qualifiedFieldName string) {
96+
kubebuilderDefaultMarkers := fieldMarkers.Get(markers.KubebuilderDefaultMarker)
97+
for _, marker := range kubebuilderDefaultMarkers {
98+
payloadValue := marker.Payload.Value
99+
pass.Report(analysis.Diagnostic{
100+
Pos: field.Pos(),
101+
Message: fmt.Sprintf("field %s should use +default or +k8s:default marker instead of +kubebuilder:default", qualifiedFieldName),
102+
SuggestedFixes: []analysis.SuggestedFix{
103+
{
104+
Message: fmt.Sprintf("replace +kubebuilder:default with +default=%s", payloadValue),
105+
TextEdits: []analysis.TextEdit{
106+
{
107+
Pos: marker.Pos,
108+
End: marker.End,
109+
NewText: fmt.Appendf(nil, "// +default=%s", payloadValue),
110+
},
111+
},
112+
},
113+
},
114+
})
115+
}
116+
}
117+
118+
func checkDefaultOptional(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) {
119+
if !utils.IsFieldOptional(field, markersAccess) {
120+
pass.Report(analysis.Diagnostic{
121+
Pos: field.Pos(),
122+
Message: fmt.Sprintf("field %s has a default value but is not marked as optional", qualifiedFieldName),
123+
})
124+
}
125+
}
126+
127+
func checkDefaultOmitEmptyOrOmitZero(pass *analysis.Pass, field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) {
128+
if !jsonTagInfo.OmitEmpty && !jsonTagInfo.OmitZero && !jsonTagInfo.Inline && !jsonTagInfo.Ignored {
129+
pass.Report(analysis.Diagnostic{
130+
Pos: field.Pos(),
131+
Message: fmt.Sprintf("field %s has a default value but does not have omitempty or omitzero in its json tag", qualifiedFieldName),
132+
SuggestedFixes: []analysis.SuggestedFix{
133+
{
134+
Message: fmt.Sprintf("add omitempty to the json tag of field %s", qualifiedFieldName),
135+
TextEdits: []analysis.TextEdit{
136+
{
137+
Pos: jsonTagInfo.Pos,
138+
End: jsonTagInfo.End,
139+
NewText: fmt.Appendf([]byte{}, "%s,omitempty,omitzero", jsonTagInfo.RawValue),
140+
},
141+
},
142+
},
143+
},
144+
})
145+
}
146+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 defaults_test
17+
18+
import (
19+
"testing"
20+
21+
"golang.org/x/tools/go/analysis/analysistest"
22+
"sigs.k8s.io/kube-api-linter/pkg/analysis/defaults"
23+
)
24+
25+
func TestDefaultsAnalyzer(t *testing.T) {
26+
testdata := analysistest.TestData()
27+
analysistest.RunWithSuggestedFixes(t, testdata, defaults.Analyzer, "a")
28+
}

pkg/analysis/defaults/doc.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
defaults is a linter to check that fields with default markers are configured correctly.
19+
20+
Fields with default markers (+default, +kubebuilder:default, or +k8s:default) should also be marked as optional.
21+
Additionally, fields with default markers should have "omitempty" or "omitzero" in their json tags
22+
to ensure that the default values are applied correctly during serialization and deserialization.
23+
24+
Example of a well-configured field with a default:
25+
26+
// +optional
27+
// +default="default-value"
28+
Field string `json:"field,omitempty"`
29+
30+
Example of issues this linter will catch:
31+
32+
// Missing optional marker
33+
// +default="value"
34+
Field string `json:"field,omitempty"`
35+
36+
// Missing omitempty tag
37+
// +optional
38+
// +default="value"
39+
Field string `json:"field"`
40+
*/
41+
package defaults
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 defaults
17+
18+
import (
19+
"sigs.k8s.io/kube-api-linter/pkg/analysis/initializer"
20+
"sigs.k8s.io/kube-api-linter/pkg/analysis/registry"
21+
)
22+
23+
func init() {
24+
registry.DefaultRegistry().RegisterLinter(Initializer())
25+
}
26+
27+
// Initializer returns the AnalyzerInitializer for this
28+
// Analyzer so that it can be added to the registry.
29+
func Initializer() initializer.AnalyzerInitializer {
30+
return initializer.NewInitializer(name, Analyzer, true)
31+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package a
2+
3+
type A struct {
4+
// GoodDefaultField is correctly configured with +default, +optional, and omitempty.
5+
// +optional
6+
// +default="default-value"
7+
GoodDefaultField string `json:"goodDefaultField,omitempty"`
8+
9+
// GoodK8sDefaultField uses +k8s:default which is also acceptable.
10+
// +optional
11+
// +k8s:default="default-value"
12+
GoodK8sDefaultField string `json:"goodK8sDefaultField,omitempty"`
13+
14+
// GoodDefaultFieldWithOmitZero is correctly configured with +default, +optional, and omitzero.
15+
// +optional
16+
// +default=0
17+
GoodDefaultFieldWithOmitZero int `json:"goodDefaultFieldWithOmitZero,omitzero"`
18+
19+
// KubebuilderDefaultField uses +kubebuilder:default which should be replaced.
20+
// +optional
21+
// +kubebuilder:default="default-value"
22+
KubebuilderDefaultField string `json:"kubebuilderDefaultField,omitempty"` // want "field A.KubebuilderDefaultField should use \\+default or \\+k8s:default marker instead of \\+kubebuilder:default"
23+
24+
// MissingOptionalField has a default but is not marked optional.
25+
// +default="value"
26+
MissingOptionalField string `json:"missingOptionalField,omitempty"` // want "field A.MissingOptionalField has a default value but is not marked as optional"
27+
28+
// MissingOmitEmptyField has a default and optional but no omitempty.
29+
// +optional
30+
// +default="value"
31+
MissingOmitEmptyField string `json:"missingOmitEmptyField"` // want "field A.MissingOmitEmptyField has a default value but does not have omitempty or omitzero in its json tag"
32+
33+
// BothIssuesField has both issues: not optional and no omitempty.
34+
// +default="value"
35+
BothIssuesField string `json:"bothIssuesField"` // want "field A.BothIssuesField has a default value but is not marked as optional" "field A.BothIssuesField has a default value but does not have omitempty or omitzero in its json tag"
36+
37+
// NoDefaultField is a regular field without default - should not be flagged.
38+
// +optional
39+
NoDefaultField string `json:"noDefaultField,omitempty"`
40+
41+
// RequiredFieldWithDefault should still be flagged as not optional.
42+
// +required
43+
// +default="value"
44+
RequiredFieldWithDefault string `json:"requiredFieldWithDefault,omitempty"` // want "field A.RequiredFieldWithDefault has a default value but is not marked as optional"
45+
46+
// InlineField with default and inline tag should not require omitempty.
47+
// +optional
48+
// +default={}
49+
InlineField B `json:",inline"`
50+
51+
// IgnoredField with json ignore tag should not require omitempty.
52+
// +optional
53+
// +default="ignored"
54+
IgnoredField string `json:"-"`
55+
}
56+
57+
type B struct {
58+
// NestedField is a nested field.
59+
// +optional
60+
NestedField string `json:"nestedField,omitempty"`
61+
}

0 commit comments

Comments
 (0)