Skip to content

Commit 10b6e09

Browse files
Merge branch 'main' into enums
2 parents 55a0a29 + 70246ec commit 10b6e09

File tree

12 files changed

+614
-93
lines changed

12 files changed

+614
-93
lines changed

docs/linters.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [DefaultOrRequired](#defaultorrequired) - Ensures fields marked as required do not have default values
88
- [DuplicateMarkers](#duplicatemarkers) - Checks for exact duplicates of markers
99
- [Enums](#enums) - Enforces proper usage of enumerated fields with type aliases and +enum marker
10+
- [DependentTags](#dependenttags) - Enforces dependencies between markers
1011
- [ForbiddenMarkers](#forbiddenmarkers) - Checks that no forbidden markers are present on types/fields.
1112
- [Integers](#integers) - Validates usage of supported integer types
1213
- [JSONTags](#jsontags) - Ensures proper JSON tag formatting
@@ -93,6 +94,43 @@ If any of the 5 markers in the example above are missing, the linter will sugges
9394
When `usePatchStrategy` is set to `Ignore`, the linter will not suggest to add the `patchStrategy` and `patchMergeKey` tags to the `Conditions` field markers.
9495
When `usePatchStrategy` is set to `Forbid`, the linter will suggest to remove the `patchStrategy` and `patchMergeKey` tags from the `Conditions` field markers.
9596

97+
## DependentTags
98+
99+
The `dependenttags` linter enforces dependencies between markers. This prevents API inconsistencies where one marker requires the presence of another.
100+
101+
The linter is configured with a main tag and a list of required dependent tags. If the main tag is present on a field, the linter checks for the presence of the dependent tags based on the `type` field:
102+
- `All`: Ensures that **all** of the dependent tags are present.
103+
- `Any`: Ensures that **at least one** of the dependent tags is present.
104+
105+
### Configuration
106+
107+
```yaml
108+
lintersConfig:
109+
dependenttags:
110+
rules:
111+
- identifier: "k8s:unionMember"
112+
type: "All"
113+
dependents:
114+
- "k8s:optional"
115+
- identifier: "listType"
116+
type: "All"
117+
dependents:
118+
- "k8s:listType"
119+
- identifier: "example:any"
120+
type: "Any"
121+
dependents:
122+
- "dep1"
123+
- "dep2"
124+
```
125+
126+
### Behavior
127+
128+
This linter only checks for the presence or absence of markers; it does not inspect or enforce specific values within those markers. Therefore:
129+
130+
- **Values:** The linter does not care about the values of the `identifier` or `dependent` markers. It only verifies if the markers themselves are present.
131+
- **Fixes:** This linter does not provide automatic fixes. It only reports violations.
132+
- **Same/Different Values:** Whether you want the same or different values between dependent markers is outside the scope of this linter. You would need other validation mechanisms (e.g., CEL validation) to enforce value-based dependencies.
133+
96134
## CommentStart
97135

98136
The `commentstart` linter checks that all comments in the API types start with the serialized form of the type they are commenting on.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
package dependenttags
18+
19+
import (
20+
"fmt"
21+
"go/ast"
22+
"strings"
23+
24+
"golang.org/x/tools/go/analysis"
25+
26+
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
27+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
28+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
29+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
30+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
31+
)
32+
33+
// analyzer implements the dependenttags linter.
34+
type analyzer struct {
35+
cfg Config
36+
}
37+
38+
// newAnalyzer creates a new analyzer.
39+
func newAnalyzer(cfg Config) *analysis.Analyzer {
40+
// Register markers from configuration
41+
for _, rule := range cfg.Rules {
42+
markers.DefaultRegistry().Register(rule.Identifier)
43+
44+
for _, dep := range rule.Dependents {
45+
markers.DefaultRegistry().Register(dep)
46+
}
47+
}
48+
49+
a := &analyzer{
50+
cfg: cfg,
51+
}
52+
53+
return &analysis.Analyzer{
54+
Name: name,
55+
Doc: "Enforces dependencies between markers.",
56+
Run: a.run,
57+
Requires: []*analysis.Analyzer{inspector.Analyzer, markers.Analyzer},
58+
}
59+
}
60+
61+
// run is the main function for the analyzer.
62+
func (a *analyzer) run(pass *analysis.Pass) (any, error) {
63+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
64+
if !ok {
65+
return nil, kalerrors.ErrCouldNotGetInspector
66+
}
67+
68+
inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) {
69+
if field.Doc == nil {
70+
return
71+
}
72+
73+
fieldMarkers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field)
74+
75+
for _, rule := range a.cfg.Rules {
76+
if _, ok := fieldMarkers[rule.Identifier]; ok {
77+
switch rule.Type {
78+
case DependencyTypeAny:
79+
handleAny(pass, field, rule, fieldMarkers, qualifiedFieldName)
80+
case DependencyTypeAll:
81+
handleAll(pass, field, rule, fieldMarkers, qualifiedFieldName)
82+
default:
83+
panic(fmt.Sprintf("unknown dependency type %s", rule.Type))
84+
}
85+
}
86+
}
87+
})
88+
89+
return nil, nil //nolint:nilnil
90+
}
91+
func handleAll(pass *analysis.Pass, field *ast.Field, rule Rule, fieldMarkers markers.MarkerSet, qualifiedFieldName string) {
92+
missing := make([]string, 0, len(rule.Dependents))
93+
94+
for _, dependent := range rule.Dependents {
95+
if _, depOk := fieldMarkers[dependent]; !depOk {
96+
missing = append(missing, fmt.Sprintf("+%s", dependent))
97+
}
98+
}
99+
100+
if len(missing) > 0 {
101+
pass.Reportf(field.Pos(), "field %s with marker +%s is missing required marker(s): %s", qualifiedFieldName, rule.Identifier, strings.Join(missing, ", "))
102+
}
103+
}
104+
105+
func handleAny(pass *analysis.Pass, field *ast.Field, rule Rule, fieldMarkers markers.MarkerSet, qualifiedFieldName string) {
106+
found := false
107+
108+
for _, dependent := range rule.Dependents {
109+
if _, depOk := fieldMarkers[dependent]; depOk {
110+
found = true
111+
break
112+
}
113+
}
114+
115+
if !found {
116+
dependents := make([]string, len(rule.Dependents))
117+
for i, d := range rule.Dependents {
118+
dependents[i] = fmt.Sprintf("+%s", d)
119+
}
120+
121+
pass.Reportf(field.Pos(), "field %s with marker +%s requires at least one of the following markers, but none were found: %s", qualifiedFieldName, rule.Identifier, strings.Join(dependents, ", "))
122+
}
123+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
package dependenttags_test
18+
19+
import (
20+
"testing"
21+
22+
"golang.org/x/tools/go/analysis/analysistest"
23+
"sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags"
24+
)
25+
26+
func TestAnalyzer(t *testing.T) {
27+
testdata := analysistest.TestData()
28+
cfg := dependenttags.Config{
29+
Rules: []dependenttags.Rule{
30+
{
31+
Identifier: "k8s:unionMember",
32+
Type: dependenttags.DependencyTypeAll,
33+
Dependents: []string{"k8s:optional"},
34+
},
35+
{
36+
Identifier: "listType",
37+
Type: dependenttags.DependencyTypeAll,
38+
Dependents: []string{"k8s:listType"},
39+
},
40+
{
41+
Identifier: "example:any",
42+
Type: dependenttags.DependencyTypeAny,
43+
Dependents: []string{"dep1", "dep2"},
44+
},
45+
{
46+
Identifier: "listType=map",
47+
Type: dependenttags.DependencyTypeAll,
48+
Dependents: []string{"listMapKey"},
49+
},
50+
},
51+
}
52+
analyzer, err := dependenttags.Initializer().Init(&cfg)
53+
54+
if err != nil {
55+
t.Fatalf("failed to initialize analyzer: %v", err)
56+
}
57+
58+
analysistest.Run(t, testdata, analyzer, "a")
59+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
package dependenttags
18+
19+
// DependencyType defines the type of dependency rule.
20+
type DependencyType string
21+
22+
const (
23+
// DependencyTypeAll indicates that all dependent markers are required.
24+
DependencyTypeAll DependencyType = "All"
25+
// DependencyTypeAny indicates that at least one of the dependent markers is required.
26+
DependencyTypeAny DependencyType = "Any"
27+
)
28+
29+
// Config defines the configuration for the dependenttags linter.
30+
type Config struct {
31+
// Rules defines the dependency rules between markers.
32+
Rules []Rule `mapstructure:"rules"`
33+
}
34+
35+
// Rule defines a dependency rule where a main marker requires a set of dependent markers.
36+
type Rule struct {
37+
// Identifier is the marker that requires other markers.
38+
Identifier string `mapstructure:"identifier"`
39+
// Dependents are the markers that are required by Main.
40+
Dependents []string `mapstructure:"dependents"`
41+
// Type defines how to interpret the dependents list.
42+
// When set to All, every dependent in the list must be present when the identifier is present on a field or type.
43+
// When set to Any, at least one of the listed dependents must be present when the identifier is present on a field or type.
44+
Type DependencyType `mapstructure:"type,omitempty"`
45+
}
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+
17+
package dependenttags_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestDependentTags(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "DependentTags Suite")
29+
}

pkg/analysis/dependenttags/doc.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 dependenttags enforces dependencies between markers.
17+
//
18+
// # Analyzer dependenttags
19+
//
20+
// The dependenttags analyzer validates that if a specific marker (identifier) is present on a field,
21+
// a set of other markers (dependent tags) are also present. This is useful for enforcing API
22+
// contracts where certain markers imply the presence of others.
23+
//
24+
// For example, a field marked with `+k8s:unionMember` must also be marked with `+k8s:optional`.
25+
//
26+
// # Configuration
27+
//
28+
// The linter is configured with a list of rules. Each rule specifies an identifier marker and a list of
29+
// dependent markers. The `type` field is required and specifies how to interpret the dependents list:
30+
// - `All`: all dependent markers are required.
31+
// - `Any`: at least one of the dependent markers is required.
32+
//
33+
// This linter only checks for the presence or absence of markers; it does not inspect or enforce specific values within those markers. It also does not provide automatic fixes.
34+
//
35+
// linters:
36+
// dependenttags:
37+
// rules:
38+
// - identifier: "k8s:unionMember"
39+
// type: "All"
40+
// dependents:
41+
// - "k8s:optional"
42+
// - identifier: "listType"
43+
// type: "All"
44+
// dependents:
45+
// - "k8s:listType"
46+
// - identifier: "example:any"
47+
// type: "Any"
48+
// dependents:
49+
// - "dep1"
50+
// - "dep2"
51+
package dependenttags

0 commit comments

Comments
 (0)