diff --git a/docs/linters.md b/docs/linters.md index 1e914939..47e44ae5 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -2,6 +2,7 @@ - [Conditions](#conditions) - Checks that `Conditions` fields are correctly formatted - [CommentStart](#commentstart) - Ensures comments start with the serialized form of the type +- [ConflictingMarkers](#conflictingmarkers) - Detects mutually exclusive markers on the same field - [DuplicateMarkers](#duplicatemarkers) - Checks for exact duplicates of markers - [Integers](#integers) - Validates usage of supported integer types - [JSONTags](#jsontags) - Ensures proper JSON tag formatting @@ -73,6 +74,49 @@ The `commentstart` linter can automatically fix comments that do not start with When the `json` tag is present, and matches the first word of the field comment in all but casing, the linter will suggest that the comment be updated to match the `json` tag. +## ConflictingMarkers + +The `conflictingmarkers` linter detects and reports when mutually exclusive markers are used on the same field. +This prevents common configuration errors and unexpected behavior in Kubernetes API types. + +The linter reports issues when markers from two or more sets of a conflict definition are present on the same field. +It does NOT report issues when multiple markers from the same set are present - only when markers from +different sets within the same conflict definition are found together. + +The linter is configurable and allows users to define sets of conflicting markers. +Each conflict set must specify: +- A unique name for the conflict +- Multiple sets of markers that are mutually exclusive with each other (at least 2 sets) +- A description explaining why the markers conflict + +### Configuration + +```yaml +lintersConfig: + conflictingmarkers: + conflicts: + - name: "optional_vs_required" + sets: + - ["optional", "+kubebuilder:validation:Optional", "+k8s:validation:optional"] + - ["required", "+kubebuilder:validation:Required", "+k8s:validation:required"] + description: "A field cannot be both optional and required" + - name: "default_vs_required" + sets: + - ["default", "+kubebuilder:default"] + - ["required", "+kubebuilder:validation:Required", "+k8s:validation:required"] + description: "A field with a default value cannot be required" + - name: "three_way_conflict" + sets: + - ["marker5", "marker6"] + - ["marker7", "marker8"] + - ["marker9", "marker10"] + description: "Three-way conflict between marker sets" +``` + +**Note**: This linter is not enabled by default and must be explicitly enabled in the configuration. + +The linter does not provide automatic fixes as it cannot determine which conflicting marker should be removed. + ## DuplicateMarkers The duplicatemarkers linter checks for exact duplicates of markers for types and fields. diff --git a/pkg/analysis/conflictingmarkers/analyzer.go b/pkg/analysis/conflictingmarkers/analyzer.go new file mode 100644 index 00000000..65a3b55e --- /dev/null +++ b/pkg/analysis/conflictingmarkers/analyzer.go @@ -0,0 +1,132 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package conflictingmarkers + +import ( + "fmt" + "go/ast" + "strings" + + "golang.org/x/tools/go/analysis" + "k8s.io/apimachinery/pkg/util/sets" + kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" +) + +const name = "conflictingmarkers" + +type analyzer struct { + conflictSets []ConflictSet +} + +func newAnalyzer(cfg *ConflictingMarkersConfig) *analysis.Analyzer { + if cfg == nil { + cfg = &ConflictingMarkersConfig{} + } + + // Register markers from configuration + for _, conflictSet := range cfg.Conflicts { + for _, set := range conflictSet.Sets { + for _, markerID := range set { + markers.DefaultRegistry().Register(markerID) + } + } + } + + a := &analyzer{ + conflictSets: cfg.Conflicts, + } + + return &analysis.Analyzer{ + Name: name, + Doc: "Check that fields do not have conflicting markers from mutually exclusive sets", + Run: a.run, + Requires: []*analysis.Analyzer{inspector.Analyzer}, + } +} + +func (a *analyzer) run(pass *analysis.Pass) (any, error) { + inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) + if !ok { + return nil, kalerrors.ErrCouldNotGetInspector + } + + inspect.InspectFields(func(field *ast.Field, stack []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) { + checkField(pass, field, markersAccess, a.conflictSets) + }) + + return nil, nil //nolint:nilnil +} + +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, conflictSets []ConflictSet) { + if field == nil || len(field.Names) == 0 { + return + } + + markers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field) + + for _, conflictSet := range conflictSets { + checkConflict(pass, field, markers, conflictSet) + } +} + +func checkConflict(pass *analysis.Pass, field *ast.Field, markers markers.MarkerSet, conflictSet ConflictSet) { + // Track which sets have markers present + conflictingMarkers := make([]sets.Set[string], 0) + + for _, set := range conflictSet.Sets { + foundMarkers := sets.New[string]() + + for _, markerID := range set { + if markers.Has(markerID) { + foundMarkers.Insert(markerID) + } + } + // Only add the set if it has at least one marker + if foundMarkers.Len() > 0 { + conflictingMarkers = append(conflictingMarkers, foundMarkers) + } + } + + // If two or more sets have markers, report the conflict + if len(conflictingMarkers) >= 2 { + reportConflict(pass, field, conflictSet, conflictingMarkers) + } +} + +func reportConflict(pass *analysis.Pass, field *ast.Field, conflictSet ConflictSet, conflictingMarkers []sets.Set[string]) { + // Build a descriptive message showing which sets conflict + setDescriptions := make([]string, 0, len(conflictingMarkers)) + + for _, set := range conflictingMarkers { + markersList := sets.List(set) + setDescriptions = append(setDescriptions, fmt.Sprintf("%v", markersList)) + } + + message := fmt.Sprintf("field %s has conflicting markers: %s: {%s}. %s", + field.Names[0].Name, + conflictSet.Name, + strings.Join(setDescriptions, ", "), + conflictSet.Description) + + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: message, + }) +} diff --git a/pkg/analysis/conflictingmarkers/analyzer_test.go b/pkg/analysis/conflictingmarkers/analyzer_test.go new file mode 100644 index 00000000..0380d898 --- /dev/null +++ b/pkg/analysis/conflictingmarkers/analyzer_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package conflictingmarkers_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers" +) + +func TestConflictingMarkersAnalyzer(t *testing.T) { + testdata := analysistest.TestData() + + config := &conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "test_conflict", + Sets: [][]string{{"marker1", "marker2"}, {"marker3", "marker4"}}, + Description: "Test markers conflict with each other", + }, + { + Name: "three_way_conflict", + Sets: [][]string{{"marker5", "marker6"}, {"marker7", "marker8"}, {"marker9", "marker10"}}, + Description: "Three-way conflict between marker sets", + }, + }, + } + + initializer := conflictingmarkers.Initializer() + + analyzer, err := initializer.Init(config) + if err != nil { + t.Fatal(err) + } + + analysistest.Run(t, testdata, analyzer, "a") +} diff --git a/pkg/analysis/conflictingmarkers/config.go b/pkg/analysis/conflictingmarkers/config.go new file mode 100644 index 00000000..5317cc64 --- /dev/null +++ b/pkg/analysis/conflictingmarkers/config.go @@ -0,0 +1,39 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package conflictingmarkers + +// ConflictingMarkersConfig contains the configuration for the conflictingmarkers linter. +type ConflictingMarkersConfig struct { + // Conflicts allows users to define sets of conflicting markers. + // Each entry defines a conflict between multiple sets of markers. + Conflicts []ConflictSet `json:"conflicts"` +} + +// ConflictSet represents a conflict between multiple sets of markers. +// Markers within each set are mutually exclusive with markers in all other sets. +// The linter will emit a diagnostic when a field has markers from two or more sets. +type ConflictSet struct { + // Name is a human-readable name for this conflict set. + // This name will appear in diagnostic messages to identify the type of conflict. + Name string `json:"name"` + // Sets contains the sets of markers that are mutually exclusive with each other. + // Each set is a slice of marker identifiers. + // The linter will emit a diagnostic when a field has markers from two or more sets. + Sets [][]string `json:"sets"` + // Description provides a description of why these markers conflict. + // The linter will include this description in the diagnostic message when a conflict is detected. + Description string `json:"description"` +} diff --git a/pkg/analysis/conflictingmarkers/conflictingmarkers_suite_test.go b/pkg/analysis/conflictingmarkers/conflictingmarkers_suite_test.go new file mode 100644 index 00000000..f762df73 --- /dev/null +++ b/pkg/analysis/conflictingmarkers/conflictingmarkers_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conflictingmarkers_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConflictingMarkers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "conflictingmarkers") +} diff --git a/pkg/analysis/conflictingmarkers/doc.go b/pkg/analysis/conflictingmarkers/doc.go new file mode 100644 index 00000000..765f60cf --- /dev/null +++ b/pkg/analysis/conflictingmarkers/doc.go @@ -0,0 +1,60 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +conflictingmarkers is a linter that detects and reports when mutually exclusive markers are used on the same field. +This prevents common configuration errors and unexpected behavior in Kubernetes API types. + +The linter reports issues when markers from two or more sets of a conflict definition are present on the same field. +It does NOT report issues when multiple markers from the same set are present - only when markers from +different sets within the same conflict definition are found together. + +The linter is fully configurable and requires users to define all conflict sets they want to check. +There are no built-in conflict sets - all conflicts must be explicitly configured. + +Each conflict set must specify: +- A unique name for the conflict +- Multiple sets of markers that are mutually exclusive with each other (at least 2 sets) +- A description explaining why the markers conflict + +Example configuration: +```yaml +lintersConfig: + + conflictingmarkers: + conflicts: + - name: "optional_vs_required" + sets: + - ["optional", "+kubebuilder:validation:Optional", "+k8s:validation:optional"] + - ["required", "+kubebuilder:validation:Required", "+k8s:validation:required"] + description: "A field cannot be both optional and required" + - name: "my_custom_conflict" + sets: + - ["custom:marker1", "custom:marker2"] + - ["custom:marker3", "custom:marker4"] + - ["custom:marker5", "custom:marker6"] + description: "These markers define different storage backends that cannot be used simultaneously" + +``` + +Configuration options: +- `conflicts`: Required list of conflict set definitions. + +Note: This linter is not enabled by default and must be explicitly enabled in the configuration. + +The linter does not provide automatic fixes as it cannot determine which conflicting marker should be removed. +*/ +package conflictingmarkers diff --git a/pkg/analysis/conflictingmarkers/initializer.go b/pkg/analysis/conflictingmarkers/initializer.go new file mode 100644 index 00000000..7f05424d --- /dev/null +++ b/pkg/analysis/conflictingmarkers/initializer.go @@ -0,0 +1,117 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package conflictingmarkers + +import ( + "fmt" + + "golang.org/x/tools/go/analysis" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/kube-api-linter/pkg/analysis/initializer" + "sigs.k8s.io/kube-api-linter/pkg/analysis/registry" +) + +func init() { + registry.DefaultRegistry().RegisterLinter(Initializer()) +} + +// Initializer returns the AnalyzerInitializer for this +// Analyzer so that it can be added to the registry. +func Initializer() initializer.AnalyzerInitializer { + return initializer.NewConfigurableInitializer( + name, + initAnalyzer, + false, + validateConfig, + ) +} + +// initAnalyzer returns the initialized Analyzer. +func initAnalyzer(cfg *ConflictingMarkersConfig) (*analysis.Analyzer, error) { + return newAnalyzer(cfg), nil +} + +// validateConfig validates the configuration in the config.ConflictingMarkersConfig struct. +func validateConfig(cfg *ConflictingMarkersConfig, fldPath *field.Path) field.ErrorList { + if cfg == nil { + return field.ErrorList{} + } + + fieldErrors := field.ErrorList{} + + // Validate that at least one conflict set is defined + if len(cfg.Conflicts) == 0 { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("conflicts"), "at least one conflict set is required")) + return fieldErrors + } + + nameSet := sets.New[string]() + + for i, conflictSet := range cfg.Conflicts { + if nameSet.Has(conflictSet.Name) { + fieldErrors = append(fieldErrors, field.Duplicate(fldPath.Child("conflicts").Index(i).Child("name"), conflictSet.Name)) + continue + } + + fieldErrors = append(fieldErrors, validateConflictSet(conflictSet, fldPath.Child("conflicts").Index(i))...) + + nameSet.Insert(conflictSet.Name) + } + + return fieldErrors +} + +func validateConflictSet(conflictSet ConflictSet, fldPath *field.Path) field.ErrorList { + fieldErrors := field.ErrorList{} + + if conflictSet.Name == "" { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("name"), "name is required")) + } + + if conflictSet.Description == "" { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("description"), "description is required")) + } + + if len(conflictSet.Sets) < 2 { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("sets"), "at least 2 sets are required")) + return fieldErrors + } + + // Validate each set is non-empty + for i, set := range conflictSet.Sets { + if len(set) == 0 { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("sets").Index(i), "set cannot be empty")) + } + } + + // Check for overlapping markers between any sets + for i := range conflictSet.Sets { + for j := i + 1; j < len(conflictSet.Sets); j++ { + setI := sets.New(conflictSet.Sets[i]...) + setJ := sets.New(conflictSet.Sets[j]...) + + if intersection := setI.Intersection(setJ); intersection.Len() > 0 { + fieldErrors = append(fieldErrors, field.Invalid( + fldPath.Child("sets"), + conflictSet, + fmt.Sprintf("sets %d and %d cannot contain overlapping markers: %v", i+1, j+1, sets.List(intersection)))) + } + } + } + + return fieldErrors +} diff --git a/pkg/analysis/conflictingmarkers/initializer_test.go b/pkg/analysis/conflictingmarkers/initializer_test.go new file mode 100644 index 00000000..d95afb5d --- /dev/null +++ b/pkg/analysis/conflictingmarkers/initializer_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conflictingmarkers_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/initializer" +) + +var _ = Describe("conflictingmarkers initializer", func() { + Context("config validation", func() { + type testCase struct { + config conflictingmarkers.ConflictingMarkersConfig + expectedErr string + } + + DescribeTable("should validate the provided config", func(in testCase) { + ci, ok := conflictingmarkers.Initializer().(initializer.ConfigurableAnalyzerInitializer) + Expect(ok).To(BeTrue()) + + errs := ci.ValidateConfig(&in.config, field.NewPath("conflictingmarkers")) + if len(in.expectedErr) > 0 { + Expect(errs.ToAggregate()).To(MatchError(in.expectedErr)) + } else { + Expect(errs).To(HaveLen(0), "No errors were expected") + } + }, + Entry("With a valid config with single conflict", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "test_conflict", + Sets: [][]string{{"marker1"}, {"marker2"}}, + Description: "Test conflict", + }, + }, + }, + expectedErr: "", + }), + Entry("With a valid config with multiple conflicts", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "test_conflict", + Sets: [][]string{{"marker1"}, {"marker2"}}, + Description: "Test conflict", + }, + { + Name: "another_conflict", + Sets: [][]string{{"marker3", "marker4"}, {"marker5"}}, + Description: "Another test conflict", + }, + }, + }, + expectedErr: "", + }), + Entry("With empty conflicts list", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{}, + }, + expectedErr: "conflictingmarkers.conflicts: Required value: at least one conflict set is required", + }), + Entry("With missing name", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "", + Sets: [][]string{{"marker1"}, {"marker2"}}, + Description: "Test conflict", + }, + }, + }, + expectedErr: "conflictingmarkers.conflicts[0].name: Required value: name is required", + }), + Entry("With missing description", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "test_conflict", + Sets: [][]string{{"marker1"}, {"marker2"}}, + }, + }, + }, + expectedErr: "conflictingmarkers.conflicts[0].description: Required value: description is required", + }), + Entry("With insufficient sets (only 1 set)", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "test_conflict", + Sets: [][]string{{"marker1"}}, + Description: "Test conflict", + }, + }, + }, + expectedErr: "conflictingmarkers.conflicts[0].sets: Required value: at least 2 sets are required", + }), + Entry("With empty set", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "test_conflict", + Sets: [][]string{{"marker1"}, {}}, + Description: "Test conflict", + }, + }, + }, + expectedErr: "conflictingmarkers.conflicts[0].sets[1]: Required value: set cannot be empty", + }), + Entry("With overlapping markers between sets", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "test_conflict", + Sets: [][]string{{"marker1", "marker2"}, {"marker2", "marker3"}}, + Description: "Test conflict", + }, + }, + }, + expectedErr: "conflictingmarkers.conflicts[0].sets: Invalid value: conflictingmarkers.ConflictSet{Name:\"test_conflict\", Sets:[][]string{[]string{\"marker1\", \"marker2\"}, []string{\"marker2\", \"marker3\"}}, Description:\"Test conflict\"}: sets 1 and 2 cannot contain overlapping markers: [marker2]", + }), + Entry("With duplicate conflict names", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "duplicate_name", + Sets: [][]string{{"marker1"}, {"marker2"}}, + Description: "First conflict", + }, + { + Name: "duplicate_name", + Sets: [][]string{{"marker3"}, {"marker4"}}, + Description: "Second conflict", + }, + }, + }, + expectedErr: "conflictingmarkers.conflicts[1].name: Duplicate value: \"duplicate_name\"", + }), + Entry("With three-way conflict", testCase{ + config: conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "three_way_conflict", + Sets: [][]string{{"marker1"}, {"marker2"}, {"marker3"}}, + Description: "Three-way conflict", + }, + }, + }, + expectedErr: "", + }), + ) + }) +}) diff --git a/pkg/analysis/conflictingmarkers/testdata/src/a/a.go b/pkg/analysis/conflictingmarkers/testdata/src/a/a.go new file mode 100644 index 00000000..aa7292be --- /dev/null +++ b/pkg/analysis/conflictingmarkers/testdata/src/a/a.go @@ -0,0 +1,64 @@ +package a + +type TestStruct struct { + // Valid field with only marker1 + // +marker1 + ValidMarker1Field string `json:"validMarker1Field"` + + // Valid field with marker1 that has a value + // +marker1:=someValue + ValidMarker1WithValueField string `json:"validMarker1WithValueField"` + + // Valid field with marker1 and marker2 (both in same conflict set) + // +marker1 + // +marker2 + ValidMarker1And2Field string `json:"validMarker1And2Field"` + + // Conflict: marker1 vs marker3 (both with values) + // +marker1:=value1 + // +marker3:=value2 + ConflictWithValuesField string `json:"conflictWithValuesField"` // want "field ConflictWithValuesField has conflicting markers: test_conflict: \\{\\[marker1\\], \\[marker3\\]\\}. Test markers conflict with each other" + + // Conflict: marker1 vs marker3 (mixed with and without values) + // +marker1 + // +marker3:=someValue + ConflictMixedField string `json:"conflictMixedField"` // want "field ConflictMixedField has conflicting markers: test_conflict: \\{\\[marker1\\], \\[marker3\\]\\}. Test markers conflict with each other" + + // Multiple conflicts with multiple markers in each set: + // +marker1 + // +marker2 + // +marker3 + // +marker4 + MultipleConflictsField string `json:"multipleConflictsField"` // want "field MultipleConflictsField has conflicting markers: test_conflict: \\{\\[marker1 marker2\\], \\[marker3 marker4\\]\\}. Test markers conflict with each other" + + // Three-way conflict: marker5 vs marker7 vs marker9 + // +marker5 + // +marker7 + // +marker9 + ThreeWayConflictField string `json:"threeWayConflictField"` // want "field ThreeWayConflictField has conflicting markers: three_way_conflict: \\{\\[marker5\\], \\[marker7\\], \\[marker9\\]\\}. Three-way conflict between marker sets" + + // Three-way conflict with values + // +marker6:=value1 + // +marker8:=value2 + // +marker10:=value3 + ThreeWayConflictWithValuesField string `json:"threeWayConflictWithValuesField"` // want "field ThreeWayConflictWithValuesField has conflicting markers: three_way_conflict: \\{\\[marker6\\], \\[marker8\\], \\[marker10\\]\\}. Three-way conflict between marker sets" + + // Valid field with markers from same set in three-way conflict + // +marker5 + // +marker6 + ValidThreeWaySameSetField string `json:"validThreeWaySameSetField"` + + // Three-way conflict with multiple markers from each set + // +marker5 + // +marker6 + // +marker7 + // +marker8 + // +marker9 + // +marker10 + ThreeWayMultipleMarkersField string `json:"threeWayMultipleMarkersField"` // want "field ThreeWayMultipleMarkersField has conflicting markers: three_way_conflict: \\{\\[marker5 marker6\\], \\[marker7 marker8\\], \\[marker10 marker9\\]\\}. Three-way conflict between marker sets" + + // Three-way conflict with only subset of sets triggered (sets 1 and 2 only) + // +marker5 + // +marker7 + SubsetThreeWayConflictField string `json:"subsetThreeWayConflictField"` // want "field SubsetThreeWayConflictField has conflicting markers: three_way_conflict: \\{\\[marker5\\], \\[marker7\\]\\}. Three-way conflict between marker sets" +} diff --git a/pkg/registration/doc.go b/pkg/registration/doc.go index 041a2730..0541cbc8 100644 --- a/pkg/registration/doc.go +++ b/pkg/registration/doc.go @@ -25,6 +25,7 @@ package registration import ( _ "sigs.k8s.io/kube-api-linter/pkg/analysis/commentstart" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/conditions" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/integers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags"