diff --git a/docs/linters.md b/docs/linters.md index 19b66384..4b05913e 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -9,6 +9,7 @@ - [ForbiddenMarkers](#forbiddenmarkers) - Checks that no forbidden markers are present on types/fields. - [Integers](#integers) - Validates usage of supported integer types - [JSONTags](#jsontags) - Ensures proper JSON tag formatting +- [MarkerTypos](#markertypos) - Detects and fixes common typos and syntax issues in marker comments - [MaxLength](#maxlength) - Checks for maximum length constraints on strings and arrays - [NamingConventions](#namingconventions) - Ensures field names adhere to user-defined naming conventions - [NoBools](#nobools) - Prevents usage of boolean types @@ -357,6 +358,71 @@ or `+kubebuilder:validation:items:MaxLenth` if the array is an element of the bu Adding maximum lengths to strings and arrays not only ensures that the API is not abused (used to store overly large data, reduces DDOS etc.), but also allows CEL validation cost estimations to be kept within reasonable bounds. +## MarkerTypos + +The `markertypos` linter detects and fixes common typos and syntax issues in marker comments used in Kubernetes API definitions. + +This linter validates three main categories of marker issues: + +### Spacing Issues + +The linter detects and fixes incorrect spacing in marker comments: + +- **Space after '+' symbol**: Markers should not have space after the `+` symbol +- **Missing space after '//' prefix**: Markers should have a space after the `//` comment prefix + +Examples of spacing issues: + +```go +// + kubebuilder:validation:MaxLength:=256 // Incorrect: space after + +type Example1 string + +//+required // Incorrect: missing space after // +type Example2 string +``` + +Fixed versions: + +```go +// +kubebuilder:validation:MaxLength:=256 // Correct: no space after + +type Example1 string + +// +required // Correct: space after // +type Example2 string +``` + +### Common Typos + +The linter detects and suggests corrections for frequently misspelled marker identifiers: + +- `kubebuidler` → `kubebuilder` +- `kubebuiler` → `kubebuilder` +- `kubebulider` → `kubebuilder` +- `kubbuilder` → `kubebuilder` +- `kubebulder` → `kubebuilder` +- `optinal` → `optional` +- `requied` → `required` +- `requird` → `required` +- `nullabel` → `nullable` +- `validaton` → `validation` +- `valdiation` → `validation` +- `defualt` → `default` +- `defult` → `default` +- `exampl` → `example` +- `examle` → `example` + +Examples of typo detection: + +```go +// +kubebuidler:validation:Required // Typo detected: kubebuidler +// +optinal // Typo detected: optinal +// +kubebuilder:validaton:MaxLength:=256 // Typo detected: validaton +``` + +### Fixes + +No automatic fixes are provided + ## NamingConventions The `namingconventions` linter ensures that field names adhere to a set of defined naming conventions. diff --git a/pkg/analysis/markertypos/analyzer.go b/pkg/analysis/markertypos/analyzer.go new file mode 100644 index 00000000..371c7ccd --- /dev/null +++ b/pkg/analysis/markertypos/analyzer.go @@ -0,0 +1,210 @@ +/* +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 markertypos + +import ( + "fmt" + "go/ast" + "regexp" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + + kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" + helper_inspector "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" +) + +const ( + name = "markertypos" +) + +// Analyzer is the analyzer for the markertypos package. +// It checks for common typos and syntax issues in marker comments. +var Analyzer = &analysis.Analyzer{ + Name: name, + Doc: "Check for common typos and syntax issues in marker comments.", + Run: run, + Requires: []*analysis.Analyzer{helper_inspector.Analyzer, inspect.Analyzer}, +} + +// Regular expressions for marker validation. +var ( + // Matches markers that start with + followed by optional space. + markerWithSpaceRegex = regexp.MustCompile(`^\s*//\s*\+\s+\w+`) + + // Matches markers missing space after // (e.g., //+marker instead of // +marker). + markerMissingSpaceAfterSlashRegex = regexp.MustCompile(`^\s*//\+\S+`) +) + +func run(pass *analysis.Pass) (any, error) { + helperInspect, ok := pass.ResultOf[helper_inspector.Analyzer].(helper_inspector.Inspector) + if !ok { + return nil, kalerrors.ErrCouldNotGetInspector + } + + // Regular marker analysis for well-formed markers + helperInspect.InspectFields(func(field *ast.Field, _ []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) { + checkFieldMarkers(pass, field, markersAccess) + }) + + helperInspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { + checkTypeSpecMarkers(pass, typeSpec, markersAccess) + }) + + // Additional analysis for malformed markers that aren't picked up by the marker parser + astInspector, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + if !ok { + return nil, kalerrors.ErrCouldNotGetInspector + } + + // Scan all comments for malformed markers + astInspector.Preorder([]ast.Node{(*ast.GenDecl)(nil), (*ast.Field)(nil)}, func(n ast.Node) { + switch node := n.(type) { + case *ast.GenDecl: + if node.Doc != nil { + for _, comment := range node.Doc.List { + checkMalformedMarker(pass, comment) + } + } + case *ast.Field: + if node.Doc != nil { + for _, comment := range node.Doc.List { + checkMalformedMarker(pass, comment) + } + } + } + }) + + return nil, nil //nolint:nilnil +} + +func checkFieldMarkers(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers) { + if field == nil || len(field.Names) == 0 { + return + } + + fieldMarkers := markersAccess.FieldMarkers(field) + for _, marker := range fieldMarkers.UnsortedList() { + checkMarkerSyntax(pass, marker) + } +} + +func checkTypeSpecMarkers(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markers.Markers) { + if typeSpec == nil { + return + } + + typeMarkers := markersAccess.TypeMarkers(typeSpec) + for _, marker := range typeMarkers.UnsortedList() { + checkMarkerSyntax(pass, marker) + } +} + +func checkMalformedMarker(pass *analysis.Pass, comment *ast.Comment) { + // Check for markers missing space after // + if markerMissingSpaceAfterSlashRegex.MatchString(comment.Text) { + // Create a pseudo-marker for reporting + marker := markers.Marker{ + RawComment: comment.Text, + Pos: comment.Pos(), + End: comment.End(), + } + reportMissingSpaceAfterSlashIssue(pass, marker) + // Also check for typos in malformed markers + checkCommonTypos(pass, marker) + } +} + +func checkMarkerSyntax(pass *analysis.Pass, marker markers.Marker) { + rawComment := marker.RawComment + + // Check for missing space after // + if markerMissingSpaceAfterSlashRegex.MatchString(rawComment) { + reportMissingSpaceAfterSlashIssue(pass, marker) + } + + // Check for space after + + if markerWithSpaceRegex.MatchString(rawComment) { + reportSpacingIssue(pass, marker) + } + + // Check for common typos + checkCommonTypos(pass, marker) +} + +func reportMissingSpaceAfterSlashIssue(pass *analysis.Pass, marker markers.Marker) { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + Message: "marker should have space after '//' comment prefix", + }) +} + +func reportSpacingIssue(pass *analysis.Pass, marker markers.Marker) { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + Message: "marker should not have space after '+' symbol", + }) +} + +func checkCommonTypos(pass *analysis.Pass, marker markers.Marker) { + rawComment := marker.RawComment + foundTypos := make(map[string]string) + + // Common marker typos. + commonTypos := map[string]string{ + "kubebuidler": "kubebuilder", + "kubebuiler": "kubebuilder", + "kubebulider": "kubebuilder", + "kubbuilder": "kubebuilder", + "kubebulder": "kubebuilder", + "optinal": "optional", + "requied": "required", + "requird": "required", + "nullabel": "nullable", + "validaton": "validation", + "valdiation": "validation", + "defualt": "default", //nolint:misspell + "defult": "default", + "exampl": "example", + "examle": "example", + } + + // Collect all typos found in this marker + for typo, correction := range commonTypos { + typoRegex := regexp.MustCompile(`\b` + regexp.QuoteMeta(typo) + `\b`) + if typoRegex.MatchString(rawComment) { + foundTypos[typo] = correction + } + } + + // Report each typo separately (but with combined fix if multiple typos exist) + if len(foundTypos) > 0 { + reportTypos(pass, marker, foundTypos) + } +} + +func reportTypos(pass *analysis.Pass, marker markers.Marker, foundTypos map[string]string) { + // Report each typo as a separate diagnostic + for typo, correction := range foundTypos { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + Message: fmt.Sprintf("possible typo: '%s' should be '%s'", typo, correction), + }) + } +} diff --git a/pkg/analysis/markertypos/analyzer_test.go b/pkg/analysis/markertypos/analyzer_test.go new file mode 100644 index 00000000..25360382 --- /dev/null +++ b/pkg/analysis/markertypos/analyzer_test.go @@ -0,0 +1,28 @@ +/* +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 markertypos_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "sigs.k8s.io/kube-api-linter/pkg/analysis/markertypos" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.RunWithSuggestedFixes(t, testdata, markertypos.Analyzer, "a") +} diff --git a/pkg/analysis/markertypos/doc.go b/pkg/analysis/markertypos/doc.go new file mode 100644 index 00000000..5d7c2273 --- /dev/null +++ b/pkg/analysis/markertypos/doc.go @@ -0,0 +1,56 @@ +/* +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. +*/ + +/* +markertypos is an analyzer that checks for common typos and syntax issues in marker comments. + +This linter validates three main categories of marker issues: + + 1. Spacing Issues: + Detects and fixes incorrect spacing after the '+' symbol in markers. + + For example, this would be reported: + // + kubebuilder:validation:MaxLength:=256 + type Foo string + + And should be fixed to: + // +kubebuilder:validation:MaxLength:=256 + type Foo string + + 2. Syntax Issues: + Validates that kubebuilder markers use ':=' syntax while non-kubebuilder markers use '=' syntax. + + Kubebuilder markers should use ':=' syntax: + // +kubebuilder:validation:MaxLength:=256 (correct) + // +kubebuilder:validation:MaxLength=256 (incorrect, will be reported) + + Non-kubebuilder markers should use '=' syntax: + // +default:value="test" (correct) + // +default:value:="test" (incorrect, will be reported) + + 3. Common Typos: + Detects and suggests corrections for frequently misspelled marker identifiers. + + Examples of typos that would be reported: + // +kubebuidler:validation:Required → should be 'kubebuilder' + // +optinal → should be 'optional' + // +requied → should be 'required' + // +kubebuilder:validaton:MaxLength → should be 'validation' + +This linter provides automatic fixes for all detected issues, making it easy to maintain +consistent and correct marker syntax across Kubernetes API definitions. +*/ +package markertypos diff --git a/pkg/analysis/markertypos/initializer.go b/pkg/analysis/markertypos/initializer.go new file mode 100644 index 00000000..d93ed35d --- /dev/null +++ b/pkg/analysis/markertypos/initializer.go @@ -0,0 +1,35 @@ +/* +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 markertypos + +import ( + "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.NewInitializer( + name, + Analyzer, + true, + ) +} diff --git a/pkg/analysis/markertypos/testdata/src/a/a.go b/pkg/analysis/markertypos/testdata/src/a/a.go new file mode 100644 index 00000000..bf745dce --- /dev/null +++ b/pkg/analysis/markertypos/testdata/src/a/a.go @@ -0,0 +1,144 @@ +package a + +// Valid markers that should not trigger any errors + +// +kubebuilder:validation:MaxLength:=256 +type ValidKubebuilderMarker string + +// +required +// +optional +type ValidNonKubebuilderMarkers string + +// +kubebuilder:object:root:=true +// +kubebuilder:subresource:status +type ValidKubebuilderObject struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength:=1 + ValidField string `json:"validField"` + + // +optional + // +default:value="test" + ValidOptionalField string `json:"validOptionalField"` +} + +// Invalid markers that should trigger errors + +// + kubebuilder:validation:MaxLength:=256 // want "marker should not have space after '\\+' symbol" +type SpacingIssueKubebuilder string + +// + required // want "marker should not have space after '\\+' symbol" +type SpacingIssueNonKubebuilder string + +// +kubebuilder:validation:MaxLength=256 +type KubebuilderWrongSyntax string + +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:Format=date-time +type MultipleKubebuilderWrongSyntax struct { + Field string `json:"field"` +} + +// +default:value:="test" +type NonKubebuilderWrongSyntax string + +// +required:=true +// +optional:=false +type MultipleNonKubebuilderWrongSyntax struct { + Field string `json:"field"` +} + +// Common typos + +// +kubebuidler:validation:MaxLength:=256 // want "possible typo: 'kubebuidler' should be 'kubebuilder'" +type KubebuilderTypo1 string + +// +kubebuiler:validation:Required // want "possible typo: 'kubebuiler' should be 'kubebuilder'" +type KubebuilderTypo2 string + +// +kubebulider:object:root:=true // want "possible typo: 'kubebulider' should be 'kubebuilder'" +type KubebuilderTypo3 string + +// +kubbuilder:validation:MinLength:=1 // want "possible typo: 'kubbuilder' should be 'kubebuilder'" +type KubebuilderTypo4 string + +// +kubebulder:subresource:status // want "possible typo: 'kubebulder' should be 'kubebuilder'" +type KubebuilderTypo5 string + +// +optinal // want "possible typo: 'optinal' should be 'optional'" +type OptionalTypo string + +// +requied // want "possible typo: 'requied' should be 'required'" +type RequiredTypo string + +// +requird // want "possible typo: 'requird' should be 'required'" +type RequiredTypo2 string + +// +nullabel // want "possible typo: 'nullabel' should be 'nullable'" +type NullableTypo string + +// +kubebuilder:validaton:MaxLength:=256 // want "possible typo: 'validaton' should be 'validation'" +type ValidationTypo string + +// +kubebuilder:valdiation:Required // want "possible typo: 'valdiation' should be 'validation'" +type ValidationTypo2 string + +// +defualt:value="test" // want "possible typo: 'defualt' should be 'default'" +type DefaultTypo string + +// +defult:value="test" // want "possible typo: 'defult' should be 'default'" +type DefaultTypo2 string + +// +kubebuilder:exampl:="test" // want "possible typo: 'exampl' should be 'example'" +type ExampleTypo string + +// +kubebuilder:examle:="test" // want "possible typo: 'examle' should be 'example'" +type ExampleTypo2 string + +// Missing space after // prefix + +// +kubebuilder:validation:MaxLength:=256 // want "marker should have space after '//' comment prefix" +type MissingSpaceKubebuilder string + +// +required // want "marker should have space after '//' comment prefix" +type MissingSpaceNonKubebuilder string + +// +optional // want "marker should have space after '//' comment prefix" +type MissingSpaceOptional string + +// +kubebuilder:object:root:=true // want "marker should have space after '//' comment prefix" +type MissingSpaceKubebuilderObject string + +// +default:value="test" // want "marker should have space after '//' comment prefix" +type MissingSpaceDefault string + +// Complex cases with multiple issues + +// + kubebuidler:validaton:MaxLength=256 // want "marker should not have space after '\\+' symbol" "possible typo: 'kubebuidler' should be 'kubebuilder'" "possible typo: 'validaton' should be 'validation'" +type MultipleIssues string + +// +kubebuidler:validaton:MaxLength=256 // want "marker should have space after '//' comment prefix" "possible typo: 'kubebuidler' should be 'kubebuilder'" "possible typo: 'validaton' should be 'validation'" +type MultipleIssuesWithMissingSpace string + +// +requied // want "marker should have space after '//' comment prefix" "possible typo: 'requied' should be 'required'" +type MissingSpaceAndTypo string + +// +defualt:value:="test" // want "marker should have space after '//' comment prefix" "possible typo: 'defualt' should be 'default'" +type MissingSpaceDefaultTypoWrongSyntax string + +// +kubebuilder:validation:MaxLength:=256 +type ComplexValidStruct struct { + // + requied // want "marker should not have space after '\\+' symbol" "possible typo: 'requied' should be 'required'" + InvalidField1 string `json:"invalidField1"` + + //+kubebuilder:validation:Required // want "marker should have space after '//' comment prefix" + FieldWithMissingSpace string `json:"fieldWithMissingSpace"` + + //+optional // want "marker should have space after '//' comment prefix" + AnotherFieldWithMissingSpace string `json:"anotherFieldWithMissingSpace"` + + // +kubebuilder:validation:Required + ValidField string `json:"validField"` +} + +type NoLintMarker //nolint + diff --git a/pkg/analysis/markertypos/testdata/src/a/a.go.golden b/pkg/analysis/markertypos/testdata/src/a/a.go.golden new file mode 100644 index 00000000..c3f8a2cf --- /dev/null +++ b/pkg/analysis/markertypos/testdata/src/a/a.go.golden @@ -0,0 +1,117 @@ +package a + +// Valid markers that should not trigger any errors + +// +kubebuilder:validation:MaxLength:=256 +type ValidKubebuilderMarker string + +// +required +// +optional +type ValidNonKubebuilderMarkers string + +// +kubebuilder:object:root:=true +// +kubebuilder:subresource:status +type ValidKubebuilderObject struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength:=1 + ValidField string `json:"validField"` + + // +optional + // +default:value="test" + ValidOptionalField string `json:"validOptionalField"` +} + +// Invalid markers that should trigger errors + +// +kubebuilder:validation:MaxLength:=256 +type SpacingIssueKubebuilder string + +// +required +type SpacingIssueNonKubebuilder string + +// +kubebuilder:validation:MaxLength:=256 +type KubebuilderWrongSyntax string + +// +kubebuilder:validation:MinLength:=1 +// +kubebuilder:validation:Format:=date-time +type MultipleKubebuilderWrongSyntax struct { + Field string `json:"field"` +} + +// +default:value="test" +type NonKubebuilderWrongSyntax string + +// +required=true +// +optional=false +type MultipleNonKubebuilderWrongSyntax struct { + Field string `json:"field"` +} + +// Common typos + +// +kubebuilder:validation:MaxLength:=256 +type KubebuilderTypo1 string + +// +kubebuilder:validation:Required +type KubebuilderTypo2 string + +// +kubebuilder:object:root:=true +type KubebuilderTypo3 string + +// +kubebuilder:validation:MinLength:=1 +type KubebuilderTypo4 string + +// +kubebuilder:subresource:status +type KubebuilderTypo5 string + +// +optional +type OptionalTypo string + +// +required +type RequiredTypo string + +// +required +type RequiredTypo2 string + +// +nullable +type NullableTypo string + +// +kubebuilder:validation:MaxLength:=256 +type ValidationTypo string + +// +kubebuilder:validation:Required +type ValidationTypo2 string + +// +default:value="test" +type DefaultTypo string + +// +default:value="test" +type DefaultTypo2 string + +// +kubebuilder:example:="test" +type ExampleTypo string + +// +kubebuilder:example:="test" +type ExampleTypo2 string + +// Complex cases with multiple issues + +// +kubebuilder:validation:MaxLength:=256 +type MultipleIssues string + +// +kubebuilder:validation:MaxLength:=256 +type ComplexValidStruct struct { + // +required + InvalidField1 string `json:"invalidField1"` + + // +kubebuilder:validation:MinLength:=1 + InvalidField2 string `json:"invalidField2"` + + // +default:value="test" + InvalidField3 string `json:"invalidField3"` + + // +kubebuilder:validation:Required + ValidField string `json:"validField"` +} + +type NoLintMarker //nolint