Skip to content

Commit 31fcd74

Browse files
committed
feat: add conflictingmarkers linter to detect mutually exclusive markers
1 parent cf6e504 commit 31fcd74

File tree

6 files changed

+409
-0
lines changed

6 files changed

+409
-0
lines changed

docs/linters.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [StatusOptional](#statusoptional) - Ensures status fields are marked as optional
1818
- [StatusSubresource](#statussubresource) - Validates status subresource configuration
1919
- [UniqueMarkers](#uniquemarkers) - Ensures unique marker definitions
20+
- [ConflictingMarkers](#conflictingmarkers) - Detects and reports when mutually exclusive markers are used on the same field
2021

2122
## Conditions
2223

@@ -345,3 +346,51 @@ Taking the example configuration from above:
345346
- Marker definitions of `custom:SomeCustomMarker:fruit=apple,color=red` and `custom:SomeCustomMarker:fruit=orange,color=red` would _not_ violate the uniqueness requirement.
346347

347348
Each entry in `customMarkers` must have a unique `identifier`.
349+
350+
## ConflictingMarkers
351+
352+
The `conflictingmarkers` linter detects and reports when mutually exclusive markers are used on the same field.
353+
This prevents common configuration errors and unexpected behavior in Kubernetes API types.
354+
355+
The linter reports issues when markers from two or more sets of a conflict definition are present on the same field.
356+
It does NOT report issues when multiple markers from the same set are present - only when markers from
357+
different sets within the same conflict definition are found together.
358+
359+
The linter is configurable and allows users to define sets of conflicting markers.
360+
Each conflict set must specify:
361+
- A unique name for the conflict
362+
- Multiple sets of markers that are mutually exclusive with each other (at least 2 sets)
363+
- A description explaining why the markers conflict
364+
365+
### Configuration
366+
367+
```yaml
368+
lintersConfig:
369+
conflictingmarkers:
370+
conflicts:
371+
- name: "optional_vs_required"
372+
sets:
373+
- ["optional", "+kubebuilder:validation:Optional", "+k8s:validation:optional"]
374+
- ["required", "+kubebuilder:validation:Required", "+k8s:validation:required"]
375+
description: "A field cannot be both optional and required"
376+
- name: "default_vs_required"
377+
sets:
378+
- ["default", "+kubebuilder:default"]
379+
- ["required", "+kubebuilder:validation:Required", "+k8s:validation:required"]
380+
description: "A field with a default value cannot be required"
381+
- name: "mutually_exclusive_validation"
382+
sets:
383+
- ["optional", "+kubebuilder:validation:Optional"]
384+
- ["required", "+kubebuilder:validation:Required"]
385+
- ["default", "+kubebuilder:default"]
386+
description: "A field cannot be optional, required, and have a default value"
387+
- name: "my_custom_conflict"
388+
sets:
389+
- ["custom:marker1", "custom:marker2"]
390+
- ["custom:marker3", "custom:marker4"]
391+
description: "These markers conflict because..."
392+
```
393+
394+
**Note**: This linter is not enabled by default and must be explicitly enabled in the configuration.
395+
396+
The linter does not provide automatic fixes as it cannot determine which conflicting marker should be removed.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 conflictingmarkers
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+
)
31+
32+
const name = "conflictingmarkers"
33+
34+
type analyzer struct {
35+
conflictSets []ConflictSet
36+
}
37+
38+
func newAnalyzer(cfg *ConflictingMarkersConfig) *analysis.Analyzer {
39+
if cfg == nil {
40+
cfg = &ConflictingMarkersConfig{}
41+
}
42+
43+
// Register markers from configuration
44+
for _, conflictSet := range cfg.Conflicts {
45+
for _, set := range conflictSet.Sets {
46+
for _, markerID := range set {
47+
markers.DefaultRegistry().Register(markerID)
48+
}
49+
}
50+
}
51+
52+
a := &analyzer{
53+
conflictSets: cfg.Conflicts,
54+
}
55+
56+
// Use configured documentation or fall back to default
57+
doc := cfg.Doc
58+
if doc == "" {
59+
doc = "Check that fields do not have conflicting markers from mutually exclusive sets"
60+
}
61+
62+
return &analysis.Analyzer{
63+
Name: name,
64+
Doc: doc,
65+
Run: a.run,
66+
Requires: []*analysis.Analyzer{inspector.Analyzer},
67+
}
68+
}
69+
70+
func (a *analyzer) run(pass *analysis.Pass) (any, error) {
71+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
72+
if !ok {
73+
return nil, kalerrors.ErrCouldNotGetInspector
74+
}
75+
76+
inspect.InspectFields(func(field *ast.Field, stack []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) {
77+
checkField(pass, field, markersAccess, a.conflictSets)
78+
})
79+
80+
return nil, nil //nolint:nilnil
81+
}
82+
83+
func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, conflictSets []ConflictSet) {
84+
if field == nil || len(field.Names) == 0 {
85+
return
86+
}
87+
88+
markers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field)
89+
90+
for _, conflictSet := range conflictSets {
91+
checkConflict(pass, field, markers, conflictSet)
92+
}
93+
}
94+
95+
func checkConflict(pass *analysis.Pass, field *ast.Field, markers markers.MarkerSet, conflictSet ConflictSet) {
96+
// Track which sets have markers present
97+
var conflictingMarkers []sets.Set[string]
98+
99+
// Check each set for markers
100+
for _, set := range conflictSet.Sets {
101+
foundMarkers := sets.New[string]()
102+
for _, markerID := range set {
103+
if markers.Has(markerID) {
104+
foundMarkers.Insert(markerID)
105+
}
106+
}
107+
// Only add the set if it has at least one marker
108+
if foundMarkers.Len() > 0 {
109+
conflictingMarkers = append(conflictingMarkers, foundMarkers)
110+
}
111+
}
112+
113+
// If two or more sets have markers, report the conflict
114+
if len(conflictingMarkers) >= 2 {
115+
reportConflict(pass, field, conflictSet, conflictingMarkers)
116+
}
117+
}
118+
119+
func reportConflict(pass *analysis.Pass, field *ast.Field, conflictSet ConflictSet, conflictingMarkers []sets.Set[string]) {
120+
// Build a descriptive message showing which sets conflict
121+
var setDescriptions []string
122+
for _, set := range conflictingMarkers {
123+
markersList := sets.List(set)
124+
setDescriptions = append(setDescriptions, fmt.Sprintf("%v", markersList))
125+
}
126+
127+
message := fmt.Sprintf("field %s has conflicting markers: %s: {%s}. %s",
128+
field.Names[0].Name,
129+
conflictSet.Name,
130+
strings.Join(setDescriptions, ", "),
131+
conflictSet.Description)
132+
133+
pass.Report(analysis.Diagnostic{
134+
Pos: field.Pos(),
135+
Message: message,
136+
})
137+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 conflictingmarkers
17+
18+
// ConflictingMarkersConfig contains the configuration for the conflictingmarkers linter.
19+
type ConflictingMarkersConfig struct {
20+
// Doc is the documentation string for the analyzer.
21+
// If not provided, a default description will be used.
22+
Doc string `json:"doc"`
23+
// Conflicts allows users to define sets of conflicting markers.
24+
// Each entry defines a conflict between multiple sets of markers.
25+
Conflicts []ConflictSet `json:"conflicts"`
26+
}
27+
28+
// ConflictSet represents a conflict between multiple sets of markers.
29+
// Markers within each set are mutually exclusive with markers in all other sets.
30+
// The linter will emit a diagnostic when a field has markers from two or more sets.
31+
type ConflictSet struct {
32+
// Name is a human-readable name for this conflict set.
33+
// This name will appear in diagnostic messages to identify the type of conflict.
34+
Name string `json:"name"`
35+
// Sets contains the sets of markers that are mutually exclusive with each other.
36+
// Each set is a slice of marker identifiers.
37+
// The linter will emit a diagnostic when a field has markers from two or more sets.
38+
Sets [][]string `json:"sets"`
39+
// Description provides a description of why these markers conflict.
40+
// The linter will include this description in the diagnostic message when a conflict is detected.
41+
Description string `json:"description"`
42+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
conflictingmarkers is a linter that detects and reports when mutually exclusive markers are used on the same field.
19+
This prevents common configuration errors and unexpected behavior in Kubernetes API types.
20+
21+
The linter reports issues when markers from two or more sets of a conflict definition are present on the same field.
22+
It does NOT report issues when multiple markers from the same set are present - only when markers from
23+
different sets within the same conflict definition are found together.
24+
25+
The linter is fully configurable and requires users to define all conflict sets they want to check.
26+
There are no built-in conflict sets - all conflicts must be explicitly configured.
27+
28+
Each conflict set must specify:
29+
- A unique name for the conflict
30+
- Multiple sets of markers that are mutually exclusive with each other (at least 2 sets)
31+
- A description explaining why the markers conflict
32+
33+
Example configuration:
34+
```yaml
35+
lintersConfig:
36+
37+
conflictingmarkers:
38+
# Optional: Custom documentation for the analyzer
39+
doc: "Custom analyzer description for conflicting markers"
40+
conflicts:
41+
- name: "optional_vs_required"
42+
sets:
43+
- ["optional", "+kubebuilder:validation:Optional", "+k8s:validation:optional"]
44+
- ["required", "+kubebuilder:validation:Required", "+k8s:validation:required"]
45+
description: "A field cannot be both optional and required"
46+
- name: "my_custom_conflict"
47+
sets:
48+
- ["custom:marker1", "custom:marker2"]
49+
- ["custom:marker3", "custom:marker4"]
50+
- ["custom:marker5", "custom:marker6"]
51+
description: "These markers define different storage backends that cannot be used simultaneously"
52+
53+
```
54+
55+
Configuration options:
56+
- `doc`: Optional custom documentation string for the analyzer. If not provided, a default description will be used.
57+
- `conflicts`: Required list of conflict set definitions.
58+
59+
Note: This linter is not enabled by default and must be explicitly enabled in the configuration.
60+
61+
The linter does not provide automatic fixes as it cannot determine which conflicting marker should be removed.
62+
*/
63+
package conflictingmarkers

0 commit comments

Comments
 (0)