Skip to content

Commit c725d8f

Browse files
authored
Merge pull request #94 from everettraven/feature/uniquemarkers
add uniquemarkers linter
2 parents 74075c8 + b47f20a commit c725d8f

File tree

13 files changed

+1382
-38
lines changed

13 files changed

+1382
-38
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,36 @@ This linter is not enabled by default as it is only applicable to CustomResource
415415
In the case where there is a status field present but no `kubebuilder:subresource:status` marker, the
416416
linter will suggest adding the comment `// +kubebuilder:subresource:status` above the struct.
417417

418+
## UniqueMarkers
419+
420+
The `uniquemarkers` linter ensures that types and fields do not contain more than a single definition of a marker that should only be present once.
421+
422+
Because this linter has no way of determining which marker definition was intended it does not suggest any fixes
423+
424+
### Configuration
425+
It can configured to include a set of custom markers in the analysis by setting:
426+
```yaml
427+
lintersConfig:
428+
uniqueMarkers:
429+
customMarkers:
430+
- identifier: custom:SomeCustomMarker
431+
attributes:
432+
- fruit
433+
```
434+
435+
For each custom marker, it must specify an `identifier` and optionally some `attributes`.
436+
As an example, take the marker definition `kubebuilder:validation:XValidation:rule='has(self.foo)',message='should have foo',fieldPath='.foo'`.
437+
The identifier for the marker is `kubebuilder:validation:XValidation` and its attributes are `rule`, `message`, and `fieldPath`.
438+
439+
When specifying `attributes`, those attributes are included in the uniqueness identification of a marker definition.
440+
441+
Taking the example configuration from above:
442+
443+
- Marker definitions of `custom:SomeCustomMarker:fruit=apple,color=red` and `custom:SomeCustomMarker:fruit=apple,color=green` would violate the uniqueness requirement and be flagged.
444+
- Marker definitions of `custom:SomeCustomMarker:fruit=apple,color=red` and `custom:SomeCustomMarker:fruit=orange,color=red` would _not_ violate the uniqueness requirement.
445+
446+
Each entry in `customMarkers` must have a unique `identifier`.
447+
418448
# Contributing
419449

420450
New linters can be added by following the [New Linter][new-linter] guide.

pkg/analysis/duplicatemarkers/analyzer.go

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"fmt"
2020
"go/ast"
2121
"go/token"
22-
"go/types"
2322

2423
"golang.org/x/tools/go/analysis"
2524

@@ -65,7 +64,7 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Mar
6564
return
6665
}
6766

68-
markerSet := collectMarkers(pass, markersAccess, field)
67+
markerSet := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field)
6968

7069
seen := markers.NewMarkerSet()
7170

@@ -79,32 +78,6 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Mar
7978
}
8079
}
8180

82-
func collectMarkers(pass *analysis.Pass, markersAccess markers.Markers, field *ast.Field) markers.MarkerSet {
83-
markers := markersAccess.FieldMarkers(field)
84-
85-
if _, ok := pass.TypesInfo.TypeOf(field.Type).(*types.Basic); ok {
86-
return markers
87-
}
88-
89-
ident, ok := field.Type.(*ast.Ident)
90-
if !ok {
91-
return markers
92-
}
93-
94-
typeSpec, ok := utils.LookupTypeSpec(pass, ident)
95-
if !ok {
96-
return markers
97-
}
98-
99-
typeMarkers := markersAccess.TypeMarkers(typeSpec)
100-
101-
// duplicatemarkers removes duplicate markers without the first line.
102-
// By inserting the field markers after the type markers, it allows the markers to be removed from the field.
103-
typeMarkers.Insert(markers.UnsortedList()...)
104-
105-
return typeMarkers
106-
}
107-
10881
func checkTypeSpec(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markers.Markers) {
10982
if typeSpec == nil {
11083
return

pkg/analysis/optionalfields/util.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func isStructZeroValueValid(pass *analysis.Pass, field *ast.Field, structType *a
193193

194194
zeroValueValid, nonOmittedFields := areStructFieldZeroValuesValid(pass, structType, markersAccess, jsonTagInfo)
195195

196-
markerSet := combinedMarkers(markersAccess, field, structType)
196+
markerSet := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field)
197197

198198
minProperties, err := getMarkerNumericValueByName[int](markerSet, markers.KubebuilderMinPropertiesMarker)
199199
if err != nil && !errors.Is(err, errMarkerMissingValue) {
@@ -332,15 +332,6 @@ func enumFieldAllowsEmpty(fieldMarkers markershelper.MarkerSet) bool {
332332
return false
333333
}
334334

335-
func combinedMarkers(markersAccess markershelper.Markers, field *ast.Field, structType *ast.StructType) markershelper.MarkerSet {
336-
markers := markersAccess.FieldMarkers(field)
337-
structMarkers := markersAccess.StructMarkers(structType)
338-
339-
markers.Insert(structMarkers.UnsortedList()...)
340-
341-
return markers
342-
}
343-
344335
// number is a type constraint for numeric types.
345336
// It allows us to create a generic extraction function for numeric values from markers.
346337
type number interface {
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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 uniquemarkers
17+
18+
import (
19+
"fmt"
20+
"go/ast"
21+
"strings"
22+
23+
"golang.org/x/tools/go/analysis"
24+
"k8s.io/apimachinery/pkg/util/sets"
25+
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
26+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
27+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
28+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
29+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
30+
"sigs.k8s.io/kube-api-linter/pkg/config"
31+
markersconsts "sigs.k8s.io/kube-api-linter/pkg/markers"
32+
)
33+
34+
const name = "uniquemarkers"
35+
36+
func init() {
37+
for _, uniqueMarker := range defaultUniqueMarkers() {
38+
markers.DefaultRegistry().Register(uniqueMarker.Identifier)
39+
}
40+
}
41+
42+
type analyzer struct {
43+
uniqueMarkers []config.UniqueMarker
44+
}
45+
46+
func newAnalyzer(cfg config.UniqueMarkersConfig) *analysis.Analyzer {
47+
a := &analyzer{
48+
uniqueMarkers: append(defaultUniqueMarkers(), cfg.CustomMarkers...),
49+
}
50+
51+
return &analysis.Analyzer{
52+
Name: name,
53+
Doc: "Check that all markers that should be unique on a field/type are only present once",
54+
Run: a.run,
55+
Requires: []*analysis.Analyzer{inspector.Analyzer},
56+
}
57+
}
58+
59+
func (a *analyzer) run(pass *analysis.Pass) (any, error) {
60+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
61+
if !ok {
62+
return nil, kalerrors.ErrCouldNotGetInspector
63+
}
64+
65+
inspect.InspectFields(func(field *ast.Field, stack []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) {
66+
checkField(pass, field, markersAccess, a.uniqueMarkers)
67+
})
68+
69+
inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) {
70+
checkType(pass, typeSpec, markersAccess, a.uniqueMarkers)
71+
})
72+
73+
return nil, nil //nolint:nilnil
74+
}
75+
76+
func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, uniqueMarkers []config.UniqueMarker) {
77+
if field == nil || len(field.Names) == 0 {
78+
return
79+
}
80+
81+
markers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field)
82+
check(markers, uniqueMarkers, reportField(pass, field))
83+
}
84+
85+
func checkType(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markers.Markers, uniqueMarkers []config.UniqueMarker) {
86+
if typeSpec == nil {
87+
return
88+
}
89+
90+
markers := markersAccess.TypeMarkers(typeSpec)
91+
check(markers, uniqueMarkers, reportType(pass, typeSpec))
92+
}
93+
94+
func check(markerSet markers.MarkerSet, uniqueMarkers []config.UniqueMarker, reportFunc func(id string)) {
95+
for _, marker := range uniqueMarkers {
96+
marks := markerSet.Get(marker.Identifier)
97+
markSet := sets.New[string]()
98+
99+
for _, mark := range marks {
100+
id := constructIdentifier(mark, marker.Attributes...)
101+
102+
if markSet.Has(id) {
103+
reportFunc(id)
104+
continue
105+
}
106+
107+
markSet.Insert(id)
108+
}
109+
}
110+
}
111+
112+
// constructIdentifier returns a string that serves as a unique identifier for a
113+
// marker based on the provided attributes that should be unique for the marker.
114+
func constructIdentifier(marker markers.Marker, attributes ...string) string {
115+
// if there are no unique attributes, the unique identifier is just
116+
// the base marker identifier
117+
if len(attributes) == 0 {
118+
return marker.Identifier
119+
}
120+
121+
// If a marker doesn't specify the attribute, we should assume it is equivalent
122+
// to the empty string ("") so that we can still key on uniqueness of other attributes
123+
// effectively.
124+
id := fmt.Sprintf("%s:", marker.Identifier)
125+
for _, attr := range attributes {
126+
id += fmt.Sprintf("%s=%s,", attr, marker.Expressions[attr])
127+
}
128+
129+
id = strings.TrimSuffix(id, ",")
130+
131+
return id
132+
}
133+
134+
func reportField(pass *analysis.Pass, field *ast.Field) func(id string) {
135+
return func(id string) {
136+
pass.Report(analysis.Diagnostic{
137+
Pos: field.Pos(),
138+
Message: fmt.Sprintf("field %s has multiple definitions of marker %s when only a single definition should exist", field.Names[0].Name, id),
139+
})
140+
}
141+
}
142+
143+
func reportType(pass *analysis.Pass, typeSpec *ast.TypeSpec) func(id string) {
144+
return func(id string) {
145+
pass.Report(analysis.Diagnostic{
146+
Pos: typeSpec.Pos(),
147+
Message: fmt.Sprintf("type %s has multiple definitions of marker %s when only a single definition should exist", typeSpec.Name, id),
148+
})
149+
}
150+
}
151+
152+
//nolint:funlen
153+
func defaultUniqueMarkers() []config.UniqueMarker {
154+
return []config.UniqueMarker{
155+
// Basic unique markers
156+
// ------
157+
{
158+
Identifier: markersconsts.DefaultMarker,
159+
},
160+
// ------
161+
162+
// Kubebuilder-specific unique markers
163+
// ------
164+
{
165+
Identifier: markersconsts.KubebuilderDefaultMarker,
166+
},
167+
{
168+
Identifier: markersconsts.KubebuilderExampleMarker,
169+
},
170+
{
171+
Identifier: markersconsts.KubebuilderEnumMarker,
172+
},
173+
{
174+
Identifier: markersconsts.KubebuilderExclusiveMaximumMarker,
175+
},
176+
{
177+
Identifier: markersconsts.KubebuilderExclusiveMinimumMarker,
178+
},
179+
{
180+
Identifier: markersconsts.KubebuilderFormatMarker,
181+
},
182+
{
183+
Identifier: markersconsts.KubebuilderMaxItemsMarker,
184+
},
185+
{
186+
Identifier: markersconsts.KubebuilderMaxLengthMarker,
187+
},
188+
{
189+
Identifier: markersconsts.KubebuilderMaxPropertiesMarker,
190+
},
191+
{
192+
Identifier: markersconsts.KubebuilderMaximumMarker,
193+
},
194+
{
195+
Identifier: markersconsts.KubebuilderMinItemsMarker,
196+
},
197+
{
198+
Identifier: markersconsts.KubebuilderMinLengthMarker,
199+
},
200+
{
201+
Identifier: markersconsts.KubebuilderMinPropertiesMarker,
202+
},
203+
{
204+
Identifier: markersconsts.KubebuilderMinimumMarker,
205+
},
206+
{
207+
Identifier: markersconsts.KubebuilderMultipleOfMarker,
208+
},
209+
{
210+
Identifier: markersconsts.KubebuilderPatternMarker,
211+
},
212+
{
213+
Identifier: markersconsts.KubebuilderTypeMarker,
214+
},
215+
{
216+
Identifier: markersconsts.KubebuilderUniqueItemsMarker,
217+
},
218+
219+
{
220+
Identifier: markersconsts.KubebuilderItemsEnumMarker,
221+
},
222+
{
223+
Identifier: markersconsts.KubebuilderItemsFormatMarker,
224+
},
225+
{
226+
Identifier: markersconsts.KubebuilderItemsMaxLengthMarker,
227+
},
228+
{
229+
Identifier: markersconsts.KubebuilderItemsMaxItemsMarker,
230+
},
231+
{
232+
Identifier: markersconsts.KubebuilderItemsMaxPropertiesMarker,
233+
},
234+
{
235+
Identifier: markersconsts.KubebuilderItemsMaximumMarker,
236+
},
237+
{
238+
Identifier: markersconsts.KubebuilderItemsMinLengthMarker,
239+
},
240+
{
241+
Identifier: markersconsts.KubebuilderItemsMinItemsMarker,
242+
},
243+
{
244+
Identifier: markersconsts.KubebuilderItemsMinPropertiesMarker,
245+
},
246+
{
247+
Identifier: markersconsts.KubebuilderItemsMinimumMarker,
248+
},
249+
{
250+
Identifier: markersconsts.KubebuilderItemsExclusiveMaximumMarker,
251+
},
252+
{
253+
Identifier: markersconsts.KubebuilderItemsExclusiveMinimumMarker,
254+
},
255+
{
256+
Identifier: markersconsts.KubebuilderItemsMultipleOfMarker,
257+
},
258+
{
259+
Identifier: markersconsts.KubebuilderItemsPatternMarker,
260+
},
261+
{
262+
Identifier: markersconsts.KubebuilderItemsTypeMarker,
263+
},
264+
{
265+
Identifier: markersconsts.KubebuilderItemsUniqueItemsMarker,
266+
},
267+
{
268+
Identifier: markersconsts.KubebuilderXValidationMarker,
269+
Attributes: []string{
270+
"rule",
271+
},
272+
},
273+
{
274+
Identifier: markersconsts.KubebuilderItemsXValidationMarker,
275+
Attributes: []string{
276+
"rule",
277+
},
278+
},
279+
// ------
280+
}
281+
}

0 commit comments

Comments
 (0)