From 689c55b00d6ce66a6a702d5c00882f630377e4ef Mon Sep 17 00:00:00 2001 From: Jeremy Poulin Date: Thu, 15 Jan 2026 21:49:36 -0500 Subject: [PATCH] chore(tools): bump kube-api-linter to latest Update kube-api-linter from v0.0.0-20251028144537 to v0.0.0-20260114104534. This also bumps k8s.io/gengo/v2 to v2.0.0-20250922181213 as a transitive dependency. Created with support from Claude Opus 4 (Anthropic) --- tools/go.mod | 4 +- tools/go.sum | 8 +- tools/vendor/k8s.io/gengo/v2/Makefile | 2 +- .../k8s.io/gengo/v2/generator/execute.go | 39 +- .../gengo/v2/generator/import_tracker.go | 4 +- tools/vendor/k8s.io/gengo/v2/namer/namer.go | 9 +- tools/vendor/k8s.io/gengo/v2/parser/parse.go | 123 +++- .../k8s.io/gengo/v2/parser/parse_122.go | 5 + .../k8s.io/gengo/v2/parser/parse_pre_122.go | 4 + tools/vendor/modules.txt | 13 +- .../pkg/analysis/arrayofstruct/analyzer.go | 60 +- .../pkg/analysis/commentstart/analyzer.go | 19 +- .../pkg/analysis/conditions/doc.go | 2 +- .../analysis/conflictingmarkers/analyzer.go | 23 +- .../analysis/defaultorrequired/analyzer.go | 71 ++ .../pkg/analysis/defaultorrequired/doc.go | 32 + .../analysis/defaultorrequired/initializer.go | 35 + .../pkg/analysis/defaults/analyzer.go | 397 +++++++++++ .../pkg/analysis/defaults/config.go | 93 +++ .../pkg/analysis/defaults/doc.go | 41 ++ .../pkg/analysis/defaults/initializer.go | 91 +++ .../pkg/analysis/dependenttags/analyzer.go | 123 ++++ .../pkg/analysis/dependenttags/config.go | 45 ++ .../pkg/analysis/dependenttags/doc.go | 51 ++ .../pkg/analysis/dependenttags/initializer.go | 90 +++ .../pkg/analysis/duplicatemarkers/analyzer.go | 8 +- .../pkg/analysis/forbiddenmarkers/analyzer.go | 14 +- .../analysis/helpers/inspector/inspector.go | 85 ++- .../pkg/analysis/helpers/markers/analyzer.go | 643 ++++++++++++++++-- .../pkg/analysis/initializer/initializer.go | 6 +- .../pkg/analysis/integers/analyzer.go | 36 +- .../pkg/analysis/integers/doc.go | 2 +- .../pkg/analysis/jsontags/analyzer.go | 12 +- .../pkg/analysis/maxlength/analyzer.go | 13 +- .../pkg/analysis/minlength/analyzer.go | 258 +++++++ .../pkg/analysis/minlength/doc.go | 49 ++ .../pkg/analysis/minlength/initializer.go | 35 + .../analysis/namingconventions/analyzer.go | 30 +- .../pkg/analysis/namingconventions/doc.go | 4 +- .../pkg/analysis/nobools/analyzer.go | 36 +- .../pkg/analysis/nobools/doc.go | 2 +- .../pkg/analysis/nodurations/analyzer.go | 81 +-- .../pkg/analysis/nofloats/analyzer.go | 36 +- .../pkg/analysis/nomaps/analyzer.go | 62 +- .../pkg/analysis/nomaps/initializer.go | 2 +- .../analysis/nonpointerstructs/analyzer.go | 208 ++++++ .../pkg/analysis/nonpointerstructs/config.go | 29 + .../pkg/analysis/nonpointerstructs/doc.go | 34 + .../analysis/nonpointerstructs/initializer.go | 67 ++ .../pkg/analysis/noreferences/analyzer.go | 109 +++ .../pkg/analysis/noreferences/config.go | 36 + .../pkg/analysis/noreferences/doc.go | 39 ++ .../pkg/analysis/noreferences/initializer.go | 64 ++ .../pkg/analysis/optionalfields/analyzer.go | 17 +- .../pkg/analysis/optionalfields/config.go | 2 +- .../pkg/analysis/optionalfields/doc.go | 2 +- .../analysis/optionalfields/initializer.go | 2 +- .../analysis/optionalorrequired/analyzer.go | 9 +- .../pkg/analysis/preferredmarkers/analyzer.go | 315 +++++++++ .../pkg/analysis/preferredmarkers/config.go | 49 ++ .../pkg/analysis/preferredmarkers/doc.go | 96 +++ .../analysis/preferredmarkers/initializer.go | 132 ++++ .../pkg/analysis/requiredfields/analyzer.go | 17 +- .../pkg/analysis/requiredfields/config.go | 2 +- .../pkg/analysis/ssatags/analyzer.go | 49 +- .../pkg/analysis/ssatags/initializer.go | 2 +- .../pkg/analysis/statusoptional/analyzer.go | 2 +- .../analysis/statusoptional/initializer.go | 2 +- .../analysis/statussubresource/analyzer.go | 62 +- .../pkg/analysis/uniquemarkers/analyzer.go | 55 +- .../pkg/analysis/uniquemarkers/initializer.go | 2 +- .../analysis/utils/serialization/config.go | 2 +- .../serialization/serialization_check.go | 158 +++-- .../pkg/analysis/utils/serialization/util.go | 20 +- .../pkg/analysis/utils/type_check.go | 23 +- .../pkg/analysis/utils/utils.go | 228 ++++++- .../pkg/analysis/utils/zero_value.go | 93 ++- .../kube-api-linter/pkg/markers/markers.go | 9 + .../kube-api-linter/pkg/plugin/base/base.go | 2 +- .../kube-api-linter/pkg/registration/doc.go | 9 + 80 files changed, 4159 insertions(+), 586 deletions(-) create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/analyzer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/doc.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/initializer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/analyzer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/config.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/doc.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/initializer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/analyzer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/config.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/doc.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/initializer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/analyzer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/doc.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/initializer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/analyzer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/config.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/doc.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/initializer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/analyzer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/config.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/doc.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/initializer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/analyzer.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/config.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/doc.go create mode 100644 tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/initializer.go diff --git a/tools/go.mod b/tools/go.mod index c282c1c3fc3..c8c2489decb 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -25,13 +25,13 @@ require ( k8s.io/apiextensions-apiserver v0.34.1 k8s.io/apimachinery v0.34.1 k8s.io/code-generator v0.34.1 - k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f + k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/controller-tools v0.18.0 sigs.k8s.io/crdify v0.5.0 - sigs.k8s.io/kube-api-linter v0.0.0-20251028144537-077f0d3af196 + sigs.k8s.io/kube-api-linter v0.0.0-20260114104534-18147eee9c49 sigs.k8s.io/yaml v1.6.0 ) diff --git a/tools/go.sum b/tools/go.sum index 99b651f5fa5..7d2aeca1135 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -912,8 +912,8 @@ k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc= k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg= k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= -k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= -k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= @@ -930,8 +930,8 @@ sigs.k8s.io/crdify v0.5.0 h1:mrMH9CgXQPTZUpTU6Klqfnlys8bggv/7uvLT2lXSP7A= sigs.k8s.io/crdify v0.5.0/go.mod h1:ZIFxaYNgKYmFtZCLPysncXQ8oqwnNlHQbRUfxJHZwzU= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kube-api-linter v0.0.0-20251028144537-077f0d3af196 h1:5owwUeJIKzc9a/FHAhD0R8quPPHLRdd33VBTSmaUvOQ= -sigs.k8s.io/kube-api-linter v0.0.0-20251028144537-077f0d3af196/go.mod h1:TwxOEmBIjl8POuwDF2VfmrHZ5a4o2SIPjDyqtTX7T3Q= +sigs.k8s.io/kube-api-linter v0.0.0-20260114104534-18147eee9c49 h1:Dlr79S/bnHg+g86cBxul/lbctdVfMTLN1c5XeN6/0JM= +sigs.k8s.io/kube-api-linter v0.0.0-20260114104534-18147eee9c49/go.mod h1:5mP60UakkCye+eOcZ5p98VnV2O49qreW1gq9TdsUf7Q= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= diff --git a/tools/vendor/k8s.io/gengo/v2/Makefile b/tools/vendor/k8s.io/gengo/v2/Makefile index 8d0fbdaa8a8..5cb81a2fdb9 100644 --- a/tools/vendor/k8s.io/gengo/v2/Makefile +++ b/tools/vendor/k8s.io/gengo/v2/Makefile @@ -11,4 +11,4 @@ test: verify: GODEBUG=gotypesalias=0 ./hack/verify-examples.sh GODEBUG=gotypesalias=1 ./hack/verify-examples.sh - ./hack/verify-go-directive.sh 1.20 + ./hack/verify-go-directive.sh 1.23 diff --git a/tools/vendor/k8s.io/gengo/v2/generator/execute.go b/tools/vendor/k8s.io/gengo/v2/generator/execute.go index a1e052f5cc6..964a70e91c2 100644 --- a/tools/vendor/k8s.io/gengo/v2/generator/execute.go +++ b/tools/vendor/k8s.io/gengo/v2/generator/execute.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "strings" @@ -114,19 +115,53 @@ func assembleGoFile(w io.Writer, f *File) { w.Write(f.Body.Bytes()) } +func formatCode(src []byte) ([]byte, error) { + // We call goimports because it formats imports better than gofmt, but also + // call gofmt because it has the "simplify" logic. If a gofmt binary is + // not found, we will skip it. + src, err := importsWrapper(src) + if err != nil { + return nil, err + } + return gofmtWrapper(src) +} + func importsWrapper(src []byte) ([]byte, error) { opt := imports.Options{ Comments: true, TabIndent: true, TabWidth: 8, - FormatOnly: true, // Disable the insertion and deletion of imports + FormatOnly: true, // Disable the insertion and deletion of imports (slow!) } return imports.Process("", src, &opt) } +func gofmtWrapper(src []byte) ([]byte, error) { + const gofmt = "gofmt" + + if _, err := exec.LookPath(gofmt); err != nil { + klog.Errorf("WARNING: skipping output simplification: %v", err) + return src, nil + } + + cmd := exec.Command(gofmt, "-s") + cmd.Stdin = bytes.NewReader(src) + stdout := &bytes.Buffer{} + cmd.Stdout = stdout + stderr := &bytes.Buffer{} + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + if stderr.Len() > 0 { + return nil, fmt.Errorf("%s failed: %v: %s", gofmt, err, strings.TrimSpace(stderr.String())) + } + return nil, fmt.Errorf("%s failed: %v", gofmt, err) + } + return stdout.Bytes(), nil +} + func NewGoFile() *DefaultFileType { return &DefaultFileType{ - Format: importsWrapper, + Format: formatCode, Assemble: assembleGoFile, } } diff --git a/tools/vendor/k8s.io/gengo/v2/generator/import_tracker.go b/tools/vendor/k8s.io/gengo/v2/generator/import_tracker.go index 22393e4d493..f4b0f7b5f7f 100644 --- a/tools/vendor/k8s.io/gengo/v2/generator/import_tracker.go +++ b/tools/vendor/k8s.io/gengo/v2/generator/import_tracker.go @@ -61,13 +61,13 @@ func goTrackerLocalName(tracker namer.ImportTracker, localPkg string, t types.Na path := t.Package // Using backslashes in package names causes gengo to produce Go code which - // will not compile with the gc compiler. See the comment on GoSeperator. + // will not compile with the gc compiler. See the comment on GoSeparator. if strings.ContainsRune(path, '\\') { klog.Warningf("Warning: backslash used in import path '%v', this is unsupported.\n", path) } localLeaf := filepath.Base(localPkg) - dirs := strings.Split(path, namer.GoSeperator) + dirs := strings.Split(path, namer.GoSeparator) for n := len(dirs) - 1; n >= 0; n-- { // follow kube convention of not having anything between directory names name := strings.Join(dirs[n:], "") diff --git a/tools/vendor/k8s.io/gengo/v2/namer/namer.go b/tools/vendor/k8s.io/gengo/v2/namer/namer.go index bae2ee9b5b4..2202f8e70e9 100644 --- a/tools/vendor/k8s.io/gengo/v2/namer/namer.go +++ b/tools/vendor/k8s.io/gengo/v2/namer/namer.go @@ -26,14 +26,17 @@ import ( ) const ( - // GoSeperator is used to split go import paths. + // GoSeparator is used to split go import paths. // Forward slash is used instead of filepath.Seperator because it is the // only universally-accepted path delimiter and the only delimiter not // potentially forbidden by Go compilers. (In particular gc does not allow // the use of backslashes in import paths.) // See https://golang.org/ref/spec#Import_declarations. // See also https://github.com/kubernetes/gengo/issues/83#issuecomment-367040772. - GoSeperator = "/" + GoSeparator = "/" + // GoSeperator is a typo for GoSeparator. + // Deprecated: use GoSeparator instead. + GoSeperator = GoSeparator ) // Returns whether a name is a private Go name. @@ -200,7 +203,7 @@ var ( // filters out unwanted directory names and sanitizes remaining names. func (ns *NameStrategy) filterDirs(path string) []string { - allDirs := strings.Split(path, GoSeperator) + allDirs := strings.Split(path, GoSeparator) dirs := make([]string, 0, len(allDirs)) for _, p := range allDirs { if ns.IgnoreWords == nil || !ns.IgnoreWords[p] { diff --git a/tools/vendor/k8s.io/gengo/v2/parser/parse.go b/tools/vendor/k8s.io/gengo/v2/parser/parse.go index 4c1efa00104..f3760983d38 100644 --- a/tools/vendor/k8s.io/gengo/v2/parser/parse.go +++ b/tools/vendor/k8s.io/gengo/v2/parser/parse.go @@ -23,7 +23,10 @@ import ( "go/constant" "go/token" gotypes "go/types" + "maps" "path/filepath" + "reflect" + "slices" "sort" "strings" "time" @@ -382,11 +385,117 @@ func (p *Parser) NewUniverse() (types.Universe, error) { return u, nil } +// minimize returns a copy of lines with "irrelevant" lines removed. This +// includes blank lines and paragraphs starting with "Deprecated:". +func minimize(lines []string) []string { + out := make([]string, 0, len(lines)) + inDeprecated := false // paragraph tracking + prevWasBlank := false + for _, line := range lines { + if len(strings.TrimSpace(line)) == 0 { + prevWasBlank = true + inDeprecated = false + continue + } + if inDeprecated { + continue + } + if prevWasBlank && strings.HasPrefix(strings.TrimSpace(line), "Deprecated:") { + prevWasBlank = false + inDeprecated = true + continue + } + prevWasBlank = false + out = append(out, line) + } + return out +} + // addCommentsToType takes any accumulated comment lines prior to obj and // attaches them to the type t. func (p *Parser) addCommentsToType(obj gotypes.Object, t *types.Type) { - t.CommentLines = p.docComment(obj.Pos()) - t.SecondClosestCommentLines = p.priorDetachedComment(obj.Pos()) + if newLines, oldLines := p.docComment(obj.Pos()), t.CommentLines; len(newLines) > 0 { + switch { + case reflect.DeepEqual(oldLines, newLines): + // nothing needed + + case len(oldLines) == 0: + // no comments associated, or comments match exactly + t.CommentLines = newLines + + case isTypeAlias(obj.Type()): + // Ignore mismatched comments from obj because it's an alias. + // This can only be hit if gotypesalias is enabled. + if !reflect.DeepEqual(minimize(oldLines), minimize(newLines)) { + klog.Warningf( + "Mismatched comments on type %v.\n Using comments:\n%s\n Ignoring comments from type alias:\n%s\n", + t.GoType, + formatCommentBlock(oldLines), + formatCommentBlock(newLines), + ) + } + + case !isTypeAlias(obj.Type()): + // Overwrite existing comments with ones from obj because obj is not an alias. + // If gotypesalias is enabled, this should mean we found the "real" + // type, not an alias. If gotypesalias is disabled, we can end up + // overwriting the "real" comments with an alias's comments, but + // it is not clear if we can assume which one is the "real" one. + t.CommentLines = newLines + if !reflect.DeepEqual(minimize(oldLines), minimize(newLines)) { + klog.Warningf( + "Mismatched comments on type %v.\n Using comments:\n%s\n Ignoring comments from possible type alias:\n%s\n", + t.GoType, + formatCommentBlock(newLines), + formatCommentBlock(oldLines), + ) + } + } + } + + if newLines, oldLines := p.priorDetachedComment(obj.Pos()), t.SecondClosestCommentLines; len(newLines) > 0 { + switch { + case reflect.DeepEqual(oldLines, newLines): + // nothing needed + + case len(oldLines) == 0: + // no comments associated, or comments match exactly + t.SecondClosestCommentLines = newLines + + case isTypeAlias(obj.Type()): + // Ignore mismatched comments from obj because it's an alias. + // This can only be hit if gotypesalias is enabled. + if !reflect.DeepEqual(minimize(oldLines), minimize(newLines)) { + // ignore mismatched comments from obj because it's an alias + klog.Warningf( + "Mismatched secondClosestCommentLines on type %v.\n Using comments:\n%s\n Ignoring comments from type alias:\n%s\n", + t.GoType, + formatCommentBlock(oldLines), + formatCommentBlock(newLines), + ) + } + + case !isTypeAlias(obj.Type()): + // Overwrite existing comments with ones from obj because obj is not an alias. + // If gotypesalias is enabled, this should mean we found the "real" + // type, not an alias. If gotypesalias is disabled, we can end up + // overwriting the "real" comments with an alias's comments, but + // it is not clear if we can assume which one is the "real" one. + t.SecondClosestCommentLines = newLines + if !reflect.DeepEqual(minimize(oldLines), minimize(newLines)) { + klog.Warningf( + "Mismatched secondClosestCommentLines on type %v.\n Using comments:\n%s\n Ignoring comments from possible type alias:\n%s\n", + t.GoType, + formatCommentBlock(newLines), + formatCommentBlock(oldLines), + ) + } + } + } +} + +func formatCommentBlock(lines []string) string { + return " ```\n " + strings.Join(lines, "\n ") + "\n ```" } // packageDir tries to figure out the directory of the specified package. @@ -510,7 +619,9 @@ func (p *Parser) addPkgToUniverse(pkg *packages.Package, u *types.Universe) erro // Add all of this package's imports. importedPkgs := []string{} - for _, imp := range pkg.Imports { + // Iterate imports in a predictable order + for _, key := range slices.Sorted(maps.Keys(pkg.Imports)) { + imp := pkg.Imports[key] if err := p.addPkgToUniverse(imp, u); err != nil { return err } @@ -557,7 +668,11 @@ func (p *Parser) priorCommentLines(pos token.Pos, lines int) *ast.CommentGroup { } func splitLines(str string) []string { - return strings.Split(strings.TrimRight(str, "\n"), "\n") + lines := strings.Split(strings.TrimRight(str, "\n"), "\n") + if len(lines) == 1 && lines[0] == "" { + return nil + } + return lines } func goFuncNameToName(in string) types.Name { diff --git a/tools/vendor/k8s.io/gengo/v2/parser/parse_122.go b/tools/vendor/k8s.io/gengo/v2/parser/parse_122.go index ec2064958a9..de378eedd73 100644 --- a/tools/vendor/k8s.io/gengo/v2/parser/parse_122.go +++ b/tools/vendor/k8s.io/gengo/v2/parser/parse_122.go @@ -31,3 +31,8 @@ func (p *Parser) walkAliasType(u types.Universe, in gotypes.Type) *types.Type { } return nil } + +func isTypeAlias(in gotypes.Type) bool { + _, isAlias := in.(*gotypes.Alias) + return isAlias +} diff --git a/tools/vendor/k8s.io/gengo/v2/parser/parse_pre_122.go b/tools/vendor/k8s.io/gengo/v2/parser/parse_pre_122.go index 6f62100c0a7..535d6c9db68 100644 --- a/tools/vendor/k8s.io/gengo/v2/parser/parse_pre_122.go +++ b/tools/vendor/k8s.io/gengo/v2/parser/parse_pre_122.go @@ -28,3 +28,7 @@ import ( func (p *Parser) walkAliasType(u types.Universe, in gotypes.Type) *types.Type { return nil } + +func isTypeAlias(in gotypes.Type) bool { + return false +} diff --git a/tools/vendor/modules.txt b/tools/vendor/modules.txt index af7c1666a50..7249ac6b2cc 100644 --- a/tools/vendor/modules.txt +++ b/tools/vendor/modules.txt @@ -2376,8 +2376,8 @@ k8s.io/component-base/tracing k8s.io/component-base/tracing/api/v1 k8s.io/component-base/version k8s.io/component-base/zpages/features -# k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f -## explicit; go 1.20 +# k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b +## explicit; go 1.23 k8s.io/gengo/v2 k8s.io/gengo/v2/codetags k8s.io/gengo/v2/generator @@ -2480,12 +2480,15 @@ sigs.k8s.io/crdify/pkg/validators/version/served ## explicit; go 1.23 sigs.k8s.io/json sigs.k8s.io/json/internal/golang/encoding/json -# sigs.k8s.io/kube-api-linter v0.0.0-20251028144537-077f0d3af196 +# sigs.k8s.io/kube-api-linter v0.0.0-20260114104534-18147eee9c49 ## explicit; go 1.24.0 sigs.k8s.io/kube-api-linter/pkg/analysis/arrayofstruct 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/defaultorrequired +sigs.k8s.io/kube-api-linter/pkg/analysis/defaults +sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers sigs.k8s.io/kube-api-linter/pkg/analysis/errors sigs.k8s.io/kube-api-linter/pkg/analysis/forbiddenmarkers @@ -2496,16 +2499,20 @@ sigs.k8s.io/kube-api-linter/pkg/analysis/initializer sigs.k8s.io/kube-api-linter/pkg/analysis/integers sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags sigs.k8s.io/kube-api-linter/pkg/analysis/maxlength +sigs.k8s.io/kube-api-linter/pkg/analysis/minlength sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions sigs.k8s.io/kube-api-linter/pkg/analysis/nobools sigs.k8s.io/kube-api-linter/pkg/analysis/nodurations sigs.k8s.io/kube-api-linter/pkg/analysis/nofloats sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps +sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs sigs.k8s.io/kube-api-linter/pkg/analysis/nonullable sigs.k8s.io/kube-api-linter/pkg/analysis/nophase +sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences sigs.k8s.io/kube-api-linter/pkg/analysis/notimestamp sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired +sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers sigs.k8s.io/kube-api-linter/pkg/analysis/registry sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/arrayofstruct/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/arrayofstruct/analyzer.go index 21bbe93c849..f6c4526cd9c 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/arrayofstruct/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/arrayofstruct/analyzer.go @@ -20,6 +20,7 @@ import ( "go/ast" "golang.org/x/tools/go/analysis" + 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" @@ -39,20 +40,24 @@ var Analyzer = &analysis.Analyzer{ Requires: []*analysis.Analyzer{inspector.Analyzer}, } +func init() { + markershelper.DefaultRegistry().Register(markers.KubebuilderExactlyOneOf) +} + func 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, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) { - checkField(pass, field, markersAccess) + inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, qualifiedFieldName) }) return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) { +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) { // Get the element type of the array elementType := getArrayElementType(pass, field) if elementType == nil { @@ -75,12 +80,19 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelp return } + // Check if the struct has union markers that satisfy the required constraint + if hasExactlyOneOfMarker(structType, markersAccess) { + // ExactlyOneOf marker enforces that exactly one field is set, + // so we don't need to report an error + return + } + // Check if at least one field in the struct has a required marker if hasRequiredField(structType, markersAccess) { return } - reportArrayOfStructIssue(pass, field) + reportArrayOfStructIssue(pass, field, qualifiedFieldName) } // getArrayElementType extracts the element type from an array field. @@ -108,19 +120,8 @@ func getArrayElementType(pass *analysis.Pass, field *ast.Field) ast.Expr { } // reportArrayOfStructIssue reports a diagnostic for an array of structs without required fields. -func reportArrayOfStructIssue(pass *analysis.Pass, field *ast.Field) { - fieldName := utils.FieldName(field) - structName := utils.GetStructNameForField(pass, field) - - var prefix string - if structName != "" { - prefix = fmt.Sprintf("%s.%s", structName, fieldName) - } else { - prefix = fieldName - } - - message := fmt.Sprintf("%s is an array of structs, but the struct has no required fields. At least one field should be marked as %s to prevent ambiguous YAML configurations", prefix, markers.RequiredMarker) - +func reportArrayOfStructIssue(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string) { + message := fmt.Sprintf("%s is an array of structs, but the struct has no required fields. At least one field should be marked as required to prevent ambiguous YAML configurations", qualifiedFieldName) pass.Report(analysis.Diagnostic{ Pos: field.Pos(), Message: message, @@ -166,6 +167,11 @@ func getStructType(pass *analysis.Pass, expr ast.Expr) *ast.StructType { // Inline struct definition return et case *ast.Ident: + // Check if it's a basic type - exit condition for recursion + if utils.IsBasicType(pass, et) { + return nil + } + // Named struct type or type alias typeSpec, ok := utils.LookupTypeSpec(pass, et) if !ok { @@ -197,15 +203,23 @@ func hasRequiredField(structType *ast.StructType, markersAccess markershelper.Ma } for _, field := range structType.Fields.List { - fieldMarkers := markersAccess.FieldMarkers(field) - - // Check for any of the required markers - if fieldMarkers.Has(markers.RequiredMarker) || - fieldMarkers.Has(markers.KubebuilderRequiredMarker) || - fieldMarkers.Has(markers.K8sRequiredMarker) { + if utils.IsFieldRequired(field, markersAccess) { return true } } return false } + +// hasExactlyOneOfMarker checks if the struct has an ExactlyOneOf marker, +// which satisfies the required field constraint by ensuring exactly one field is set. +func hasExactlyOneOfMarker(structType *ast.StructType, markersAccess markershelper.Markers) bool { + if structType == nil { + return false + } + + // Use StructMarkers to get the set of markers on the struct + markerSet := markersAccess.StructMarkers(structType) + + return markerSet.Has(markers.KubebuilderExactlyOneOf) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/commentstart/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/commentstart/analyzer.go index b6744f13413..8e71a52fafd 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/commentstart/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/commentstart/analyzer.go @@ -19,7 +19,6 @@ import ( "fmt" "go/ast" "go/token" - "go/types" "strings" "golang.org/x/tools/go/analysis" @@ -27,7 +26,6 @@ import ( "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 = "commentstart" @@ -47,25 +45,20 @@ func run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - checkField(pass, field, jsonTagInfo) + inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, _ markers.Markers, qualifiedFieldName string) { + checkField(pass, field, jsonTagInfo, qualifiedFieldName) }) return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo) { +func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) { if tagInfo.Name == "" { return } - fieldName := utils.FieldName(field) - if fieldName == "" { - fieldName = types.ExprString(field.Type) - } - if field.Doc == nil { - pass.Reportf(field.Pos(), "field %s is missing godoc comment", fieldName) + pass.Reportf(field.Pos(), "field %s is missing godoc comment", qualifiedFieldName) return } @@ -75,7 +68,7 @@ func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.F // The comment start is correct, apart from the casing, we can fix that. pass.Report(analysis.Diagnostic{ Pos: firstLine.Pos(), - Message: fmt.Sprintf("godoc for field %s should start with '%s ...'", fieldName, tagInfo.Name), + Message: fmt.Sprintf("godoc for field %s should start with '%s ...'", qualifiedFieldName, tagInfo.Name), SuggestedFixes: []analysis.SuggestedFix{ { Message: fmt.Sprintf("should replace first word with `%s`", tagInfo.Name), @@ -90,7 +83,7 @@ func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.F }, }) } else { - pass.Reportf(field.Doc.List[0].Pos(), "godoc for field %s should start with '%s ...'", fieldName, tagInfo.Name) + pass.Reportf(field.Doc.List[0].Pos(), "godoc for field %s should start with '%s ...'", qualifiedFieldName, tagInfo.Name) } } } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conditions/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conditions/doc.go index a87587cc050..bf83db36296 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conditions/doc.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conditions/doc.go @@ -17,7 +17,7 @@ limitations under the License. /* conditions is a linter that verifies that the conditions field within the struct is correctly defined. -conditions fields in Kuberenetes API types are expected to be a slice of metav1.Condition. +conditions fields in Kubernetes API types are expected to be a slice of metav1.Condition. This linter verifies that the field is a slice of metav1.Condition and that it is correctly annotated with the required markers, and tags. diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/analyzer.go index f77f896cc66..38bacb6dea2 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/analyzer.go @@ -67,14 +67,14 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, _ []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - checkField(pass, field, markersAccess, a.conflictSets) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, a.conflictSets, qualifiedFieldName) }) return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, conflictSets []ConflictSet) { +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, conflictSets []ConflictSet, qualifiedFieldName string) { if field == nil || len(field.Names) == 0 { return } @@ -82,11 +82,11 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Mar markers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field) for _, conflictSet := range conflictSets { - checkConflict(pass, field, markers, conflictSet) + checkConflict(pass, field, markers, conflictSet, qualifiedFieldName) } } -func checkConflict(pass *analysis.Pass, field *ast.Field, markers markers.MarkerSet, conflictSet ConflictSet) { +func checkConflict(pass *analysis.Pass, field *ast.Field, markers markers.MarkerSet, conflictSet ConflictSet, qualifiedFieldName string) { // Track which sets have markers present conflictingMarkers := make([]sets.Set[string], 0) @@ -106,11 +106,11 @@ func checkConflict(pass *analysis.Pass, field *ast.Field, markers markers.Marker // If two or more sets have markers, report the conflict if len(conflictingMarkers) >= 2 { - reportConflict(pass, field, conflictSet, conflictingMarkers) + reportConflict(pass, field, conflictSet, conflictingMarkers, qualifiedFieldName) } } -func reportConflict(pass *analysis.Pass, field *ast.Field, conflictSet ConflictSet, conflictingMarkers []sets.Set[string]) { +func reportConflict(pass *analysis.Pass, field *ast.Field, conflictSet ConflictSet, conflictingMarkers []sets.Set[string], qualifiedFieldName string) { // Build a descriptive message showing which sets conflict setDescriptions := make([]string, 0, len(conflictingMarkers)) @@ -119,15 +119,8 @@ func reportConflict(pass *analysis.Pass, field *ast.Field, conflictSet ConflictS setDescriptions = append(setDescriptions, fmt.Sprintf("%v", markersList)) } - fieldName := field.Names[0].Name - structName := utils.GetStructNameForField(pass, field) - - if structName != "" { - fieldName = structName + "." + fieldName - } - message := fmt.Sprintf("field %s has conflicting markers: %s: {%s}. %s", - fieldName, + qualifiedFieldName, conflictSet.Name, strings.Join(setDescriptions, ", "), conflictSet.Description) diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/analyzer.go new file mode 100644 index 00000000000..0027f65089d --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/analyzer.go @@ -0,0 +1,71 @@ +/* +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 defaultorrequired + +import ( + "errors" + "fmt" + + "golang.org/x/tools/go/analysis" + "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" + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +const ( + name = "defaultorrequired" + doc = "Checks that fields marked as required do not have default values applied" +) + +var errUnexpectedInitializerType = errors.New("expected conflictingmarkers.Initializer() to be of type initializer.ConfigurableAnalyzerInitializer, but was not") + +// newAnalyzer creates a new analyzer that wraps conflictingmarkers with a predefined configuration +// for checking default and required marker conflicts. +func newAnalyzer() *analysis.Analyzer { + cfg := &conflictingmarkers.ConflictingMarkersConfig{ + Conflicts: []conflictingmarkers.ConflictSet{ + { + Name: "default_or_required", + Sets: [][]string{ + {markers.DefaultMarker, markers.KubebuilderDefaultMarker}, + {markers.RequiredMarker, markers.KubebuilderRequiredMarker, markers.K8sRequiredMarker}, + }, + Description: "A field with a default value cannot be required. A required field must be provided by the user, so a default value is not meaningful.", + }, + }, + } + + configInit, ok := conflictingmarkers.Initializer().(initializer.ConfigurableAnalyzerInitializer) + if !ok { + panic(fmt.Errorf("getting initializer: %w", errUnexpectedInitializerType)) + } + + errs := configInit.ValidateConfig(cfg, field.NewPath("defaultorrequired")) + if err := errs.ToAggregate(); err != nil { + panic(fmt.Errorf("defaultorrequired linter has an invalid conflictingmarkers configuration: %w", err)) + } + + analyzer, err := configInit.Init(cfg) + if err != nil { + panic(fmt.Errorf("defaultorrequired linter encountered an error initializing wrapped conflictingmarkers analyzer: %w", err)) + } + + analyzer.Name = name + analyzer.Doc = doc + + return analyzer +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/doc.go new file mode 100644 index 00000000000..f0c4fdfc5e4 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/doc.go @@ -0,0 +1,32 @@ +/* +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 defaultorrequired provides the defaultorrequired analyzer. + +The defaultorrequired analyzer checks that fields marked as required do not have default values applied. + +A field cannot be both required and have a default value, as these are conflicting concepts: +- A required field must be provided by the user and cannot be omitted +- A default value is used when a field is not provided + +For example, the following would be flagged: + + // +kubebuilder:validation:Required + // +kubebuilder:default:=value + Field string `json:"field"` +*/ +package defaultorrequired diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/initializer.go new file mode 100644 index 00000000000..9c1aeabb275 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaultorrequired/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 defaultorrequired + +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, + newAnalyzer(), + true, + ) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/analyzer.go new file mode 100644 index 00000000000..9dc5c6db5e7 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/analyzer.go @@ -0,0 +1,397 @@ +/* +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 defaults + +import ( + "fmt" + "go/ast" + "strings" + + "golang.org/x/tools/go/analysis" + 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" + markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +const ( + name = "defaults" +) + +func init() { + markershelper.DefaultRegistry().Register( + markers.DefaultMarker, + markers.KubebuilderDefaultMarker, + markers.K8sDefaultMarker, + markers.OptionalMarker, + markers.KubebuilderOptionalMarker, + markers.K8sOptionalMarker, + markers.RequiredMarker, + markers.KubebuilderRequiredMarker, + markers.K8sRequiredMarker, + ) +} + +type analyzer struct { + preferredDefaultMarker string + secondaryDefaultMarker string + omitEmptyPolicy OmitEmptyPolicy + omitZeroPolicy OmitZeroPolicy +} + +// newAnalyzer creates a new analyzer with the given configuration. +func newAnalyzer(cfg *DefaultsConfig) *analysis.Analyzer { + if cfg == nil { + cfg = &DefaultsConfig{} + } + + defaultConfig(cfg) + + a := &analyzer{ + omitEmptyPolicy: cfg.OmitEmpty.Policy, + omitZeroPolicy: cfg.OmitZero.Policy, + } + + switch cfg.PreferredDefaultMarker { + case markers.DefaultMarker: + a.preferredDefaultMarker = markers.DefaultMarker + a.secondaryDefaultMarker = markers.KubebuilderDefaultMarker + case markers.KubebuilderDefaultMarker: + a.preferredDefaultMarker = markers.KubebuilderDefaultMarker + a.secondaryDefaultMarker = markers.DefaultMarker + } + + return &analysis.Analyzer{ + Name: name, + Doc: `Checks that fields with default markers are configured correctly. +Fields with default markers (+default, +kubebuilder:default, or +k8s:default) should also be marked as optional and not required. +Additionally, fields with default markers should have "omitempty" or "omitzero" in their json tags to ensure that the default values are applied correctly during serialization and deserialization. +`, + Run: a.run, + Requires: []*analysis.Analyzer{inspector.Analyzer}, + } +} + +func defaultConfig(cfg *DefaultsConfig) { + if cfg.PreferredDefaultMarker == "" { + cfg.PreferredDefaultMarker = markers.DefaultMarker + } + + if cfg.OmitEmpty.Policy == "" { + cfg.OmitEmpty.Policy = OmitEmptyPolicySuggestFix + } + + if cfg.OmitZero.Policy == "" { + cfg.OmitZero.Policy = OmitZeroPolicySuggestFix + } +} + +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, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + a.checkField(pass, field, jsonTagInfo, markersAccess, qualifiedFieldName) + }) + + return nil, nil //nolint:nilnil +} + +func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + if field == nil || len(field.Names) == 0 { + return + } + + fieldMarkers := markersAccess.FieldMarkers(field) + + // Check for any default marker (+default, +kubebuilder:default, or +k8s:default) + hasPreferredDefault := fieldMarkers.Has(a.preferredDefaultMarker) + hasSecondaryDefault := fieldMarkers.Has(a.secondaryDefaultMarker) + hasK8sDefault := fieldMarkers.Has(markers.K8sDefaultMarker) + + hasAnyDefault := hasPreferredDefault || hasSecondaryDefault || hasK8sDefault + + if !hasAnyDefault { + return + } + + // Check +k8s:default marker (for declarative validation, separate from preferred/secondary) + // If +k8s:default is present but neither +default nor +kubebuilder:default is present, suggest adding the preferred one + a.checkK8sDefault(pass, field, fieldMarkers, qualifiedFieldName, hasPreferredDefault || hasSecondaryDefault) + + // Check secondary marker usage + // If both preferred and secondary exist, suggest removing secondary + // If only secondary exists, suggest replacing with preferred + if hasSecondaryDefault { + hasBothDefaults := hasPreferredDefault && hasSecondaryDefault + a.checkSecondaryDefault(pass, field, fieldMarkers, qualifiedFieldName, hasBothDefaults) + } + + a.checkDefaultNotRequired(pass, field, markersAccess, qualifiedFieldName) + + a.checkDefaultOptional(pass, field, markersAccess, qualifiedFieldName) + + a.checkDefaultOmitEmptyOrOmitZero(pass, field, jsonTagInfo, qualifiedFieldName) +} + +// checkK8sDefault checks for +k8s:default marker usage. +// +k8s:default is for declarative validation and is separate from preferred/secondary default markers. +// If the field has +k8s:default but doesn't have +default or +kubebuilder:default, we suggest adding the preferred one. +func (a *analyzer) checkK8sDefault(pass *analysis.Pass, field *ast.Field, fieldMarkers markershelper.MarkerSet, qualifiedFieldName string, hasOtherDefault bool) { + if !fieldMarkers.Has(markers.K8sDefaultMarker) { + return + } + + // If the field already has +default or +kubebuilder:default, +k8s:default is acceptable alongside them + // (e.g., in K/K where both are needed during transition period) + if hasOtherDefault { + return + } + + // If only +k8s:default is present, suggest adding the preferred default marker + k8sDefaultMarkers := fieldMarkers.Get(markers.K8sDefaultMarker) + for _, marker := range k8sDefaultMarkers { + payloadValue := marker.Payload.Value + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s has +%s but should also have +%s marker", qualifiedFieldName, markers.K8sDefaultMarker, a.preferredDefaultMarker), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("add +%s=%s", a.preferredDefaultMarker, payloadValue), + TextEdits: []analysis.TextEdit{ + { + Pos: marker.Pos, + End: marker.Pos, + NewText: fmt.Appendf(nil, "// +%s=%s\n\t", a.preferredDefaultMarker, payloadValue), + }, + }, + }, + }, + }) + } +} + +func (a *analyzer) checkSecondaryDefault(pass *analysis.Pass, field *ast.Field, fieldMarkers markershelper.MarkerSet, qualifiedFieldName string, hasBothDefaults bool) { + secondaryDefaultMarkers := fieldMarkers.Get(a.secondaryDefaultMarker) + + if hasBothDefaults { + // Both preferred and secondary markers exist - suggest removing secondary + pass.Report(reportShouldRemoveSecondaryMarker(field, secondaryDefaultMarkers, a.preferredDefaultMarker, a.secondaryDefaultMarker, qualifiedFieldName)) + return + } + // Only secondary marker exists - suggest replacing with preferred + pass.Report(reportShouldReplaceSecondaryMarker(field, secondaryDefaultMarkers, a.preferredDefaultMarker, a.secondaryDefaultMarker, qualifiedFieldName)) +} + +func reportShouldReplaceSecondaryMarker(field *ast.Field, markers []markershelper.Marker, preferredMarker, secondaryMarker, qualifiedFieldName string) analysis.Diagnostic { + textEdits := make([]analysis.TextEdit, len(markers)) + + for i, marker := range markers { + if i == 0 { + textEdits[i] = analysis.TextEdit{ + Pos: marker.Pos, + End: marker.End, + NewText: fmt.Appendf(nil, "// +%s=%s", preferredMarker, marker.Payload.Value), + } + + continue + } + + textEdits[i] = analysis.TextEdit{ + Pos: marker.Pos, + End: marker.End + 1, // Add 1 to position to include the new line + NewText: nil, + } + } + + return analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s should use +%s marker instead of +%s", qualifiedFieldName, preferredMarker, secondaryMarker), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("replace +%s with +%s", secondaryMarker, preferredMarker), + TextEdits: textEdits, + }, + }, + } +} + +func reportShouldRemoveSecondaryMarker(field *ast.Field, markers []markershelper.Marker, preferredMarker, secondaryMarker, qualifiedFieldName string) analysis.Diagnostic { + textEdits := make([]analysis.TextEdit, len(markers)) + + for i, marker := range markers { + textEdits[i] = analysis.TextEdit{ + Pos: marker.Pos, + End: marker.End + 1, // Add 1 to position to include the new line + NewText: nil, + } + } + + return analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s should use only the marker +%s, +%s is not required", qualifiedFieldName, preferredMarker, secondaryMarker), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("remove +%s", secondaryMarker), + TextEdits: textEdits, + }, + }, + } +} + +func (a *analyzer) checkDefaultNotRequired(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) { + if utils.IsFieldRequired(field, markersAccess) { + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s has a default value but is marked as required, which is contradictory", qualifiedFieldName), + }) + } +} + +func (a *analyzer) checkDefaultOptional(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) { + // If the field is required, we've already reported that issue in checkDefaultNotRequired + if utils.IsFieldRequired(field, markersAccess) { + return + } + + if !utils.IsFieldOptional(field, markersAccess) { + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s has a default value but is not marked as optional", qualifiedFieldName), + }) + } +} + +func (a *analyzer) checkDefaultOmitEmptyOrOmitZero(pass *analysis.Pass, field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) { + if jsonTagInfo.Inline || jsonTagInfo.Ignored { + return + } + + hasOmitEmpty := jsonTagInfo.OmitEmpty + hasOmitZero := jsonTagInfo.OmitZero + + // Check if the field is a pointer type - pointers don't need omitzero because nil is their zero value + isPointer, _ := utils.IsStarExpr(field.Type) + isStruct := !isPointer && utils.IsStructType(pass, field.Type) + + // For struct types (but not pointers), we prefer omitzero over omitempty. + // When omitzero is present, omitempty is not needed (modernize linter would complain). + if isStruct && a.omitZeroPolicy != OmitZeroPolicyForbid { + if !hasOmitZero { + a.reportMissingOmitZero(pass, field, jsonTagInfo, qualifiedFieldName, hasOmitEmpty) + } + + return + } + + // Check omitempty for non-struct types (only if policy is not Ignore) + if a.omitEmptyPolicy != OmitEmptyPolicyIgnore && !hasOmitEmpty { + a.reportMissingOmitEmpty(pass, field, jsonTagInfo, qualifiedFieldName) + } +} + +func (a *analyzer) reportMissingOmitEmpty(pass *analysis.Pass, field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) { + suggestedTag := fmt.Sprintf("%s,omitempty", jsonTagInfo.RawValue) + message := fmt.Sprintf("add omitempty to the json tag of field %s", qualifiedFieldName) + + switch a.omitEmptyPolicy { + case OmitEmptyPolicySuggestFix: + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s has a default value but does not have omitempty in its json tag", qualifiedFieldName), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: message, + TextEdits: []analysis.TextEdit{ + { + Pos: jsonTagInfo.Pos, + End: jsonTagInfo.End, + NewText: []byte(suggestedTag), + }, + }, + }, + }, + }) + case OmitEmptyPolicyWarn: + pass.Reportf(field.Pos(), "field %s has a default value but does not have omitempty in its json tag", qualifiedFieldName) + case OmitEmptyPolicyIgnore: + // Unreachable: this function is only called when the policy is not Ignore. + return + } +} + +func (a *analyzer) reportMissingOmitZero(pass *analysis.Pass, field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string, hasOmitEmpty bool) { + // For struct types, we prefer omitzero over omitempty. + // If the field has omitempty, we replace it with omitzero. + // If the field doesn't have omitempty, we just add omitzero. + // We never add both omitempty and omitzero together (modernize linter would complain). + var suggestedTag string + + var message string + + if hasOmitEmpty { + // Replace omitempty with omitzero + suggestedTag = replaceOmitEmptyWithOmitZero(jsonTagInfo.RawValue) + message = fmt.Sprintf("replace omitempty with omitzero in the json tag of field %s", qualifiedFieldName) + } else { + // Just add omitzero + suggestedTag = fmt.Sprintf("%s,omitzero", jsonTagInfo.RawValue) + message = fmt.Sprintf("add omitzero to the json tag of field %s", qualifiedFieldName) + } + + switch a.omitZeroPolicy { + case OmitZeroPolicySuggestFix: + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s has a default value but does not have omitzero in its json tag", qualifiedFieldName), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: message, + TextEdits: []analysis.TextEdit{ + { + Pos: jsonTagInfo.Pos, + End: jsonTagInfo.End, + NewText: []byte(suggestedTag), + }, + }, + }, + }, + }) + case OmitZeroPolicyWarn: + pass.Reportf(field.Pos(), "field %s has a default value but does not have omitzero in its json tag", qualifiedFieldName) + case OmitZeroPolicyForbid: + // Unreachable: this function is only called when the policy is not Forbid. + return + } +} + +// replaceOmitEmptyWithOmitZero replaces omitempty with omitzero in the json tag value. +func replaceOmitEmptyWithOmitZero(rawValue string) string { + // rawValue is like "fieldName,omitempty" or "fieldName,omitempty,inline" + // We need to replace "omitempty" with "omitzero" + parts := strings.Split(rawValue, ",") + for i, part := range parts { + if part == "omitempty" { + parts[i] = "omitzero" + } + } + + return strings.Join(parts, ",") +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/config.go new file mode 100644 index 00000000000..5277e8da97a --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/config.go @@ -0,0 +1,93 @@ +/* +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 defaults + +// OmitEmptyPolicy is the policy for omitempty. +// SuggestFix will suggest a fix for the field to add omitempty. +// Warn will warn about the field to add omitempty. +// Ignore will ignore the absence of omitempty. +type OmitEmptyPolicy string + +const ( + // OmitEmptyPolicySuggestFix will suggest a fix for the field. + OmitEmptyPolicySuggestFix OmitEmptyPolicy = "SuggestFix" + + // OmitEmptyPolicyWarn will warn about the field. + OmitEmptyPolicyWarn OmitEmptyPolicy = "Warn" + + // OmitEmptyPolicyIgnore will ignore the field. + OmitEmptyPolicyIgnore OmitEmptyPolicy = "Ignore" +) + +// OmitZeroPolicy is the policy for omitzero. +// SuggestFix will suggest a fix for the field to add omitzero. +// Warn will warn about the field to add omitzero. +// Forbid will forbid the field to have omitzero. +type OmitZeroPolicy string + +const ( + // OmitZeroPolicySuggestFix will suggest a fix for the field. + OmitZeroPolicySuggestFix OmitZeroPolicy = "SuggestFix" + + // OmitZeroPolicyWarn will warn about the field. + OmitZeroPolicyWarn OmitZeroPolicy = "Warn" + + // OmitZeroPolicyForbid will forbid the field. + OmitZeroPolicyForbid OmitZeroPolicy = "Forbid" +) + +// DefaultsConfig contains configuration for the defaults linter. +type DefaultsConfig struct { + // PreferredDefaultMarker is the preferred marker to use for default values. + // If this field is not set, the default value is "default". + // Valid values are "default" and "kubebuilder:default". + PreferredDefaultMarker string `json:"preferredDefaultMarker"` + + // OmitEmpty is the configuration for the `omitempty` tag within the json tag for fields with defaults. + // This defines how the linter should handle fields with defaults, and whether they should have the omitempty tag or not. + // By default, all fields with defaults will be expected to have the `omitempty` tag. + OmitEmpty DefaultsOmitEmpty `json:"omitempty"` + + // OmitZero is the configuration for the `omitzero` tag within the json tag for fields with defaults. + // This defines how the linter should handle fields with defaults, and whether they should have the omitzero tag or not. + // By default, struct fields with defaults will be expected to have the `omitzero` tag. + OmitZero DefaultsOmitZero `json:"omitzero"` +} + +// DefaultsOmitEmpty is the configuration for the `omitempty` tag on fields with defaults. +type DefaultsOmitEmpty struct { + // Policy determines whether the linter should require omitempty for fields with defaults. + // Valid values are "SuggestFix", "Warn" and "Ignore". + // When set to "SuggestFix", the linter will suggest adding the `omitempty` tag when a field with default does not have it. + // When set to "Warn", the linter will emit a warning if the field does not have the `omitempty` tag. + // When set to "Ignore", a field with default missing the `omitempty` tag will be ignored. + // When otherwise not specified, the default value is "SuggestFix". + Policy OmitEmptyPolicy `json:"policy"` +} + +// DefaultsOmitZero is the configuration for the `omitzero` tag on fields with defaults. +type DefaultsOmitZero struct { + // Policy determines whether the linter should require omitzero for struct fields with defaults. + // Valid values are "SuggestFix", "Warn" and "Forbid". + // When set to "SuggestFix", the linter will suggest adding the `omitzero` tag when a struct field with default does not have it. + // When set to "Warn", the linter will emit a warning if the field does not have the `omitzero` tag. + // When set to "Forbid", 'omitzero' tags will not be considered. + // Note, when set to "Forbid", and a field have the `omitzero` tag, the linter will not suggest adding it. + // Note, `omitzero` tag is supported in go version starting from go 1.24. + // Note, Configure omitzero policy to 'Forbid', if using with go version less than go 1.24. + // When otherwise not specified, the default value is "SuggestFix". + Policy OmitZeroPolicy `json:"policy"` +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/doc.go new file mode 100644 index 00000000000..5ccb0bea1f9 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/doc.go @@ -0,0 +1,41 @@ +/* +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. +*/ + +/* +defaults is a linter to check that fields with default markers are configured correctly. + +Fields with default markers (+default, +kubebuilder:default, or +k8s:default) should also be marked as optional. +Additionally, fields with default markers should have "omitempty" or "omitzero" in their json tags +to ensure that the default values are applied correctly during serialization and deserialization. + +Example of a well-configured field with a default: + + // +optional + // +default="default-value" + Field string `json:"field,omitempty"` + +Example of issues this linter will catch: + + // Missing optional marker + // +default="value" + Field string `json:"field,omitempty"` + + // Missing omitempty tag + // +optional + // +default="value" + Field string `json:"field"` +*/ +package defaults diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/initializer.go new file mode 100644 index 00000000000..6a16d284b9a --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/defaults/initializer.go @@ -0,0 +1,91 @@ +/* +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 defaults + +import ( + "fmt" + + "golang.org/x/tools/go/analysis" + "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" + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +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, + true, + validateConfig, + ) +} + +func initAnalyzer(cfg *DefaultsConfig) (*analysis.Analyzer, error) { + return newAnalyzer(cfg), nil +} + +// validateConfig is used to validate the configuration in the DefaultsConfig struct. +func validateConfig(cfg *DefaultsConfig, fldPath *field.Path) field.ErrorList { + if cfg == nil { + return field.ErrorList{} + } + + fieldErrors := field.ErrorList{} + + switch cfg.PreferredDefaultMarker { + case "", markers.DefaultMarker, markers.KubebuilderDefaultMarker: + default: + fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("preferredDefaultMarker"), cfg.PreferredDefaultMarker, fmt.Sprintf("invalid value, must be one of %q, %q or omitted", markers.DefaultMarker, markers.KubebuilderDefaultMarker))) + } + + fieldErrors = append(fieldErrors, validateOmitEmpty(cfg.OmitEmpty, fldPath.Child("omitempty"))...) + fieldErrors = append(fieldErrors, validateOmitZero(cfg.OmitZero, fldPath.Child("omitzero"))...) + + return fieldErrors +} + +// validateOmitEmpty is used to validate the configuration in the DefaultsOmitEmpty struct. +func validateOmitEmpty(oec DefaultsOmitEmpty, fldPath *field.Path) field.ErrorList { + fieldErrors := field.ErrorList{} + + switch oec.Policy { + case "", OmitEmptyPolicyIgnore, OmitEmptyPolicyWarn, OmitEmptyPolicySuggestFix: + default: + fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("policy"), oec.Policy, fmt.Sprintf("invalid value, must be one of %q, %q, %q or omitted", OmitEmptyPolicyIgnore, OmitEmptyPolicyWarn, OmitEmptyPolicySuggestFix))) + } + + return fieldErrors +} + +// validateOmitZero is used to validate the configuration in the DefaultsOmitZero struct. +func validateOmitZero(ozc DefaultsOmitZero, fldPath *field.Path) field.ErrorList { + fieldErrors := field.ErrorList{} + + switch ozc.Policy { + case "", OmitZeroPolicyForbid, OmitZeroPolicyWarn, OmitZeroPolicySuggestFix: + default: + fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("policy"), ozc.Policy, fmt.Sprintf("invalid value, must be one of %q, %q, %q or omitted", OmitZeroPolicyForbid, OmitZeroPolicyWarn, OmitZeroPolicySuggestFix))) + } + + return fieldErrors +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/analyzer.go new file mode 100644 index 00000000000..5286f77e276 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/analyzer.go @@ -0,0 +1,123 @@ +/* +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 dependenttags + +import ( + "fmt" + "go/ast" + "strings" + + "golang.org/x/tools/go/analysis" + + 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" +) + +// analyzer implements the dependenttags linter. +type analyzer struct { + cfg Config +} + +// newAnalyzer creates a new analyzer. +func newAnalyzer(cfg Config) *analysis.Analyzer { + // Register markers from configuration + for _, rule := range cfg.Rules { + markers.DefaultRegistry().Register(rule.Identifier) + + for _, dep := range rule.DependsOn { + markers.DefaultRegistry().Register(dep) + } + } + + a := &analyzer{ + cfg: cfg, + } + + return &analysis.Analyzer{ + Name: name, + Doc: "Enforces dependencies between markers.", + Run: a.run, + Requires: []*analysis.Analyzer{inspector.Analyzer, markers.Analyzer}, + } +} + +// run is the main function for the 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, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) { + if field.Doc == nil { + return + } + + fieldMarkers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field) + + for _, rule := range a.cfg.Rules { + if _, ok := fieldMarkers[rule.Identifier]; ok { + switch rule.Type { + case DependencyTypeAny: + handleAny(pass, field, rule, fieldMarkers, qualifiedFieldName) + case DependencyTypeAll: + handleAll(pass, field, rule, fieldMarkers, qualifiedFieldName) + default: + panic(fmt.Sprintf("unknown dependency type %s", rule.Type)) + } + } + } + }) + + return nil, nil //nolint:nilnil +} +func handleAll(pass *analysis.Pass, field *ast.Field, rule Rule, fieldMarkers markers.MarkerSet, qualifiedFieldName string) { + missing := make([]string, 0, len(rule.DependsOn)) + + for _, dependent := range rule.DependsOn { + if _, depOk := fieldMarkers[dependent]; !depOk { + missing = append(missing, fmt.Sprintf("+%s", dependent)) + } + } + + if len(missing) > 0 { + pass.Reportf(field.Pos(), "field %s with marker +%s is missing required marker(s): %s", qualifiedFieldName, rule.Identifier, strings.Join(missing, ", ")) + } +} + +func handleAny(pass *analysis.Pass, field *ast.Field, rule Rule, fieldMarkers markers.MarkerSet, qualifiedFieldName string) { + found := false + + for _, dependent := range rule.DependsOn { + if _, depOk := fieldMarkers[dependent]; depOk { + found = true + break + } + } + + if !found { + dependsOn := make([]string, len(rule.DependsOn)) + for i, d := range rule.DependsOn { + dependsOn[i] = fmt.Sprintf("+%s", d) + } + + 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(dependsOn, ", ")) + } +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/config.go new file mode 100644 index 00000000000..3bc95d93803 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/config.go @@ -0,0 +1,45 @@ +/* +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 dependenttags + +// DependencyType defines the type of dependency rule. +type DependencyType string + +const ( + // DependencyTypeAll indicates that all dependent markers are required. + DependencyTypeAll DependencyType = "All" + // DependencyTypeAny indicates that at least one of the dependent markers is required. + DependencyTypeAny DependencyType = "Any" +) + +// Config defines the configuration for the dependenttags linter. +type Config struct { + // Rules defines the dependency rules between markers. + Rules []Rule `mapstructure:"rules"` +} + +// Rule defines a dependency rule where a specific marker requires a set of other markers. +type Rule struct { + // Identifier is the marker that requires other markers. + Identifier string `mapstructure:"identifier"` + // DependsOn are the markers that are required when the identifier is present. + DependsOn []string `mapstructure:"dependsOn"` + // Type defines how to interpret the dependsOn list. + // When set to All, every dependent in the list must be present when the identifier is present on a field or type. + // When set to Any, at least one of the listed dependsOn must be present when the identifier is present on a field or type. + Type DependencyType `mapstructure:"type,omitempty"` +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/doc.go new file mode 100644 index 00000000000..4178f7d791a --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/doc.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 dependenttags enforces dependencies between markers. +// +// # Analyzer dependenttags +// +// The dependenttags analyzer validates that if a specific marker (identifier) is present on a field, +// a set of other markers (dependent tags) are also present. This is useful for enforcing API +// contracts where certain markers imply the presence of others. +// +// For example, a field marked with `+k8s:unionMember` must also be marked with `+k8s:optional`. +// +// # Configuration +// +// The linter is configured with a list of rules. Each rule specifies an identifier marker and a list of +// dependent markers. The `type` field is required and specifies how to interpret the dependsOn list: +// - `All`: all dependent markers are required. +// - `Any`: at least one of the dependent markers is required. +// +// 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. +// +// linters: +// dependenttags: +// rules: +// - identifier: "k8s:unionMember" +// type: "All" +// dependsOn: +// - "k8s:optional" +// - identifier: "listType" +// type: "All" +// dependsOn: +// - "k8s:listType" +// - identifier: "example:any" +// type: "Any" +// dependsOn: +// - "dep1" +// - "dep2" +package dependenttags diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/initializer.go new file mode 100644 index 00000000000..9541ca79fe8 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags/initializer.go @@ -0,0 +1,90 @@ +/* +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 dependenttags + +import ( + "fmt" + + "golang.org/x/tools/go/analysis" + "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" +) + +const ( + name = "dependenttags" +) + +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.ConfigurableAnalyzerInitializer { + return initializer.NewConfigurableInitializer( + name, + initAnalyzer, + false, + validateConfig, + ) +} + +// initAnalyzer returns the initialized Analyzer. +func initAnalyzer(cfg *Config) (*analysis.Analyzer, error) { + if cfg == nil { + cfg = &Config{} + } + + return newAnalyzer(*cfg), nil +} + +// validateConfig validates the linter configuration. +func validateConfig(cfg *Config, fldPath *field.Path) field.ErrorList { + var errs field.ErrorList + if cfg == nil { + return errs + } + + rulesPath := fldPath.Child("rules") + + if len(cfg.Rules) == 0 { + errs = append(errs, field.Invalid(rulesPath, cfg.Rules, "rules cannot be empty")) + } + + for i, rule := range cfg.Rules { + if rule.Identifier == "" { + errs = append(errs, field.Invalid(rulesPath.Index(i).Child("identifier"), rule.Identifier, "identifier marker cannot be empty")) + } + + if len(rule.DependsOn) == 0 { + errs = append(errs, field.Invalid(rulesPath.Index(i).Child("dependsOn"), rule.DependsOn, "dependsOn list cannot be empty")) + } + + if rule.Type == "" { + errs = append(errs, field.Required(rulesPath.Index(i).Child("type"), fmt.Sprintf("type must be explicitly set to '%s' or '%s'", DependencyTypeAll, DependencyTypeAny))) + } else { + switch rule.Type { + case DependencyTypeAll, DependencyTypeAny: + // valid + default: + errs = append(errs, field.Invalid(rulesPath.Index(i).Child("type"), rule.Type, fmt.Sprintf("type must be '%s' or '%s'", DependencyTypeAll, DependencyTypeAny))) + } + } + } + + return errs +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers/analyzer.go index 141a36d423c..32043385a28 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers/analyzer.go @@ -48,8 +48,8 @@ func run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, _ []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - checkField(pass, field, markersAccess) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, qualifiedFieldName) }) inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { @@ -59,7 +59,7 @@ func run(pass *analysis.Pass) (any, error) { return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers) { +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, qualifiedFieldName string) { if field == nil || len(field.Names) == 0 { return } @@ -74,7 +74,7 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Mar continue } - report(pass, field.Pos(), field.Names[0].Name, marker) + report(pass, field.Pos(), qualifiedFieldName, marker) } } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/forbiddenmarkers/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/forbiddenmarkers/analyzer.go index e60d1290d8f..beb3d7af081 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/forbiddenmarkers/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/forbiddenmarkers/analyzer.go @@ -61,8 +61,8 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - checkField(pass, field, markersAccess, a.forbiddenMarkers) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, a.forbiddenMarkers, qualifiedFieldName) }) inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { @@ -72,13 +72,13 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, forbiddenMarkers []Marker) { +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, forbiddenMarkers []Marker, qualifiedFieldName string) { if field == nil || len(field.Names) == 0 { return } markers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field) - check(markers, forbiddenMarkers, reportField(pass, field)) + check(markers, forbiddenMarkers, reportField(pass, field, qualifiedFieldName)) } func checkType(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markers.Markers, forbiddenMarkers []Marker) { @@ -112,7 +112,7 @@ func markerMatchesAttributeRules(marker markers.Marker, attrRules ...MarkerAttri for _, attrRule := range attrRules { // if the marker doesn't contain the attribute for a specified rule it fails the AND // operation. - val, ok := marker.Expressions[attrRule.Name] + val, ok := marker.Arguments[attrRule.Name] if !ok { return false } @@ -126,11 +126,11 @@ func markerMatchesAttributeRules(marker markers.Marker, attrRules ...MarkerAttri return true } -func reportField(pass *analysis.Pass, field *ast.Field) func(marker markers.Marker) { +func reportField(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string) func(marker markers.Marker) { return func(marker markers.Marker) { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("field %s has forbidden marker %q", field.Names[0].Name, marker.String()), + Message: fmt.Sprintf("field %s has forbidden marker %q", qualifiedFieldName, marker.String()), SuggestedFixes: []analysis.SuggestedFix{ { Message: fmt.Sprintf("remove forbidden marker %q", marker.String()), diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector/inspector.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector/inspector.go index 634893d9b18..ac87404f81a 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector/inspector.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector/inspector.go @@ -19,6 +19,7 @@ import ( "fmt" "go/ast" "go/token" + "go/types" astinspector "golang.org/x/tools/go/ast/inspector" "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" @@ -30,7 +31,10 @@ import ( // Inspector is an interface that allows for the inspection of fields in structs. type Inspector interface { // InspectFields is a function that iterates over fields in structs. - InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers)) + InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string)) + + // InspectFieldsIncludingListTypes is a function that iterates over fields in structs, including list types. + InspectFieldsIncludingListTypes(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string)) // InspectTypeSpec is a function that inspects the type spec and calls the provided inspectTypeSpec function. InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers)) @@ -55,7 +59,19 @@ func newInspector(astinspector *astinspector.Inspector, jsonTags extractjsontags // InspectFields iterates over fields in structs, ignoring any struct that is not a type declaration, and any field that is ignored and // therefore would not be included in the CRD spec. // For the remaining fields, it calls the provided inspectField function to apply analysis logic. -func (i *inspector) InspectFields(inspectField func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers)) { +func (i *inspector) InspectFields(inspectField func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string)) { + i.inspectFields(inspectField, true) +} + +// InspectFieldsIncludingListTypes iterates over fields in structs, including list types. +// Unlike InspectFields, this method does not skip fields in list type structs. +func (i *inspector) InspectFieldsIncludingListTypes(inspectField func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string)) { + i.inspectFields(inspectField, false) +} + +// inspectFields is a shared implementation for field iteration. +// The skipListTypes parameter controls whether list type structs should be skipped. +func (i *inspector) inspectFields(inspectField func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string), skipListTypes bool) { // Filter to fields so that we can iterate over fields in a struct. nodeFilter := []ast.Node{ (*ast.Field)(nil), @@ -67,7 +83,7 @@ func (i *inspector) InspectFields(inspectField func(field *ast.Field, stack []as } field, ok := n.(*ast.Field) - if !ok || !i.shouldProcessField(stack) { + if !ok || !i.shouldProcessField(stack, skipListTypes) { return ok } @@ -75,14 +91,32 @@ func (i *inspector) InspectFields(inspectField func(field *ast.Field, stack []as return false } - i.processFieldWithRecovery(field, stack, inspectField) + var structName string + + qualifiedFieldName := utils.FieldName(field) + if qualifiedFieldName == "" { + qualifiedFieldName = types.ExprString(field.Type) + } + + // The 0th node in the stack is the *ast.File. + file, ok := stack[0].(*ast.File) + if ok { + structName = utils.GetStructNameFromFile(file, field) + } + + if structName != "" { + qualifiedFieldName = fmt.Sprintf("%s.%s", structName, qualifiedFieldName) + } + + i.processFieldWithRecovery(field, qualifiedFieldName, inspectField) return true }) } // shouldProcessField checks if the field should be processed. -func (i *inspector) shouldProcessField(stack []ast.Node) bool { +// The skipListTypes parameter controls whether list type structs should be skipped. +func (i *inspector) shouldProcessField(stack []ast.Node, skipListTypes bool) bool { if len(stack) < 3 { return false } @@ -96,8 +130,13 @@ func (i *inspector) shouldProcessField(stack []ast.Node) bool { } structType, ok := stack[len(stack)-3].(*ast.StructType) - if !ok || isItemsType(structType) { - // Not in a struct or belongs to an items type. + if !ok { + // Not in a struct. + return false + } + + if skipListTypes && utils.IsKubernetesListType(structType, "") { + // Skip list types if requested. return false } @@ -117,7 +156,7 @@ func (i *inspector) shouldSkipField(field *ast.Field) bool { } // processFieldWithRecovery processes a field with panic recovery. -func (i *inspector) processFieldWithRecovery(field *ast.Field, stack []ast.Node, inspectField func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers)) { +func (i *inspector) processFieldWithRecovery(field *ast.Field, qualifiedFieldName string, inspectField func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string)) { tagInfo := i.jsonTags.FieldTags(field) defer func() { @@ -128,7 +167,7 @@ func (i *inspector) processFieldWithRecovery(field *ast.Field, stack []ast.Node, } }() - inspectField(field, stack, tagInfo, i.markers) + inspectField(field, tagInfo, i.markers, qualifiedFieldName) } // InspectTypeSpec inspects the type spec and calls the provided inspectTypeSpec function. @@ -147,34 +186,6 @@ func (i *inspector) InspectTypeSpec(inspectTypeSpec func(typeSpec *ast.TypeSpec, }) } -func isItemsType(structType *ast.StructType) bool { - // An items type is a struct with TypeMeta, ListMeta and Items fields. - if len(structType.Fields.List) != 3 { - return false - } - - // Check if the first field is TypeMeta. - // This should be a selector (e.g. metav1.TypeMeta) - // Check the TypeMeta part as the package name may vary. - if typeMeta, ok := structType.Fields.List[0].Type.(*ast.SelectorExpr); !ok || typeMeta.Sel.Name != "TypeMeta" { - return false - } - - // Check if the second field is ListMeta. - if listMeta, ok := structType.Fields.List[1].Type.(*ast.SelectorExpr); !ok || listMeta.Sel.Name != "ListMeta" { - return false - } - - // Check if the third field is Items. - // It should be an array, and be called Items. - itemsField := structType.Fields.List[2] - if _, ok := itemsField.Type.(*ast.ArrayType); !ok || len(itemsField.Names) == 0 || itemsField.Names[0].Name != "Items" { - return false - } - - return true -} - func isSchemalessType(markerSet markers.MarkerSet) bool { // Check if the field is marked as schemaless. schemalessMarker := markerSet.Get(markersconsts.KubebuilderSchemaLessMarker) diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers/analyzer.go index f75f7264daa..6db10995045 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers/analyzer.go @@ -26,19 +26,32 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "k8s.io/gengo/v2/codetags" + kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" ) -// UnnamedExpression is the expression key used +// UnnamedArgument is the argument key used // when parsing markers that don't have a specific -// named expression. +// named argument. +// +// This is specific to declarative validation markers only. +// Kubebuilder-style markers either have named arguments or a payload. // -// An example of a marker without a named expression -// is "kubebuilder:default:=foo". +// An example of a Declarative Validation marker with an unnamed argument +// is "k8s:ifEnabled(\"my-feature\")=...". // -// An example of a marker with named expressions -// is "kubebuilder:validation:XValidation:rule='...',message='...'". -const UnnamedExpression = "" +// An example of a Declarative Validation marker with named arguments +// is "k8s:item(one: "value", two: "value")=...". +const UnnamedArgument = "" + +// maxMarkerSeparationLines is the maximum number of lines that can separate +// a marker comment group from the godoc comment for it to still be considered +// associated with the type declaration. +const maxMarkerSeparationLines = 3 + +// markerPrefix is the prefix that identifies a comment line as a marker. +const markerPrefix = "// +" // Markers allows access to markers extracted from the // go types. @@ -46,7 +59,7 @@ type Markers interface { // FieldMarkers returns markers associated to the field. FieldMarkers(*ast.Field) MarkerSet - // StructMarkers returns markers associated to the given sturct. + // StructMarkers returns markers associated to the given struct. StructMarkers(*ast.StructType) MarkerSet // TypeMarkers returns markers associated to the given type. @@ -146,27 +159,64 @@ func run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotCreateMarkers } + // Pre-compute field Doc comment ownership map to avoid O(n²) complexity. + // This maps each field's Doc comment to the field itself, allowing O(1) + // lookups instead of full AST traversals in isDocCommentForField. + fieldDocComments := make(map[*ast.CommentGroup]*ast.Field) + + for _, file := range pass.Files { + ast.Inspect(file, func(n ast.Node) bool { + if field, ok := n.(*ast.Field); ok { + if field.Doc != nil { + fieldDocComments[field.Doc] = field + } + } + + return true + }) + } + inspect.Preorder(nodeFilter, func(n ast.Node) { switch typ := n.(type) { case *ast.GenDecl: - extractGenDeclMarkers(typ, results) + file := findFileForNode(typ, pass.Files) + extractGenDeclMarkers(typ, file, pass.Fset, results) case *ast.Field: - extractFieldMarkers(typ, results) + file := findFileForNode(typ, pass.Files) + extractFieldMarkers(typ, file, pass.Fset, results, fieldDocComments) } }) return results, nil } -func extractGenDeclMarkers(typ *ast.GenDecl, results *markers) { +// findFileForNode finds the file that contains the given AST node. +// For most packages, there are only a few files (typically 1-10), +// so a simple linear search is efficient and clear. +func findFileForNode(node ast.Node, files []*ast.File) *ast.File { + for _, f := range files { + if f.Pos() <= node.Pos() && node.End() <= f.End() { + return f + } + } + + return nil +} + +func extractGenDeclMarkers(typ *ast.GenDecl, file *ast.File, fset *token.FileSet, results *markers) { declMarkers := NewMarkerSet() + // Collect markers from the GenDecl's Doc field (comments directly attached to the declaration) if typ.Doc != nil { for _, comment := range typ.Doc.List { if marker := extractMarker(comment); marker.Identifier != "" { declMarkers.Insert(marker) } } + + // Also collect markers from the comment group immediately before the godoc comment + // if separated by a blank line. + extractOrphanedMarkers(typ.Doc, file, fset, declMarkers) } if len(typ.Specs) == 0 { @@ -185,56 +235,494 @@ func extractGenDeclMarkers(typ *ast.GenDecl, results *markers) { } } -func extractFieldMarkers(field *ast.Field, results *markers) { - if field == nil || field.Doc == nil { +// extractOrphanedMarkers finds markers in the comment group immediately before the godoc comment +// that are separated by a blank line. Only the immediately preceding comment group is checked, +// and it must be within maxMarkerSeparationLines lines of the godoc comment. +// +// This handles the "second level comment bug" where markers are separated from type +// declarations by blank lines, which commonly occurs in real-world Kubernetes API code. +// +// Example scenario this handles: +// +// // +kubebuilder:object:root=true +// // +kubebuilder:subresource:status +// +// // ClusterList contains a list of Cluster. +// type ClusterList struct { +// metav1.TypeMeta `json:",inline"` +// metav1.ListMeta `json:"metadata,omitempty"` +// Items []Cluster `json:"items"` +// } +// +// The markers will be detected even though separated by a blank line from the godoc comment. +// Note: Only multi-line marker groups are considered orphaned. Single-line markers are assumed +// to be regular Doc comments already handled by the AST parser. +func extractOrphanedMarkers(docGroup *ast.CommentGroup, file *ast.File, fset *token.FileSet, declMarkers MarkerSet) { + if file == nil || fset == nil { return } - fieldMarkers := NewMarkerSet() + prevGroup := findPreviousCommentGroup(docGroup, file) + if prevGroup == nil { + return + } + + if !isValidOrphanedMarkerGroup(prevGroup, docGroup, file, fset) { + return + } - for _, comment := range field.Doc.List { + // Extract markers from the previous comment group + for _, comment := range prevGroup.List { + if marker := extractMarker(comment); marker.Identifier != "" { + declMarkers.Insert(marker) + } + } +} + +// extractOrphanedFieldMarkers finds markers in the comment group immediately before a field's doc comment +// that are separated by a blank line. This is a specialized version for fields that is more conservative +// than extractOrphanedMarkers to avoid picking up markers from previous fields. +// +// This handles the "second level comment bug" for struct fields where markers are separated +// from field declarations by blank lines. +// +// Example scenario this handles: +// +// type FooStatus struct { +// // +optional +// // +listType=map +// // +listMapKey=type +// // +patchStrategy=merge +// // +patchMergeKey=type +// +// // Conditions update as changes occur in the status. +// Conditions []metav1.Condition `json:"conditions,omitempty"` +// } +// +// The markers will be detected even though separated by a blank line from the field doc comment. +func extractOrphanedFieldMarkers(docGroup *ast.CommentGroup, file *ast.File, fset *token.FileSet, fieldMarkers MarkerSet, fieldDocComments map[*ast.CommentGroup]*ast.Field) { + if file == nil || fset == nil { + return + } + + prevGroup := findPreviousCommentGroup(docGroup, file) + if prevGroup == nil { + return + } + + // For fields, only consider comment groups that contain ONLY markers (no prose documentation) + // and are not Doc comments for other declarations or fields + if !isProperlySeparated(prevGroup, docGroup, fset) { + return + } + + if !containsOnlyMarkers(prevGroup) { + return + } + + if isDocCommentForDeclaration(prevGroup, file) || isDocCommentForField(prevGroup, fieldDocComments) { + return + } + + // Extract markers from the previous comment group + for _, comment := range prevGroup.List { if marker := extractMarker(comment); marker.Identifier != "" { fieldMarkers.Insert(marker) } } +} + +// containsOnlyMarkers checks if a comment group contains ONLY markers and no prose documentation. +// This is a stricter version of containsMarkers used for field orphaned marker detection. +func containsOnlyMarkers(group *ast.CommentGroup) bool { + if len(group.List) == 0 { + return false + } + + hasMarker := false + + // Every comment line must be a marker + for _, comment := range group.List { + text := strings.TrimPrefix(comment.Text, "//") + text = strings.TrimSpace(text) + + // Empty lines are OK (e.g., blank comment lines) + if text == "" { + continue + } + + // If it doesn't start with +, it's not a marker + if !strings.HasPrefix(text, "+") { + return false + } + + // Check if this is a valid marker using regex (more efficient than full parsing) + markerContent := strings.TrimPrefix(text, "+") + if !validMarkerStart.MatchString(markerContent) { + return false + } + + hasMarker = true + } + + return hasMarker +} + +// findPreviousCommentGroup finds the comment group immediately before the given docGroup. +func findPreviousCommentGroup(docGroup *ast.CommentGroup, file *ast.File) *ast.CommentGroup { + for i, cg := range file.Comments { + if cg == docGroup && i > 0 { + return file.Comments[i-1] + } + } + + return nil +} + +// isValidOrphanedMarkerGroup checks if the previous comment group is a valid orphaned marker group. +func isValidOrphanedMarkerGroup(prevGroup, docGroup *ast.CommentGroup, file *ast.File, fset *token.FileSet) bool { + // Check if the comment groups are properly separated + if !isProperlySeparated(prevGroup, docGroup, fset) { + return false + } + + // Only extract if the comment group contains markers + if !containsMarkers(prevGroup) { + return false + } + + // Check if this previous comment group is a Doc comment for another declaration + return !isDocCommentForDeclaration(prevGroup, file) +} + +// isProperlySeparated checks if comment groups are separated by at least one blank line. +func isProperlySeparated(prevGroup, docGroup *ast.CommentGroup, fset *token.FileSet) bool { + docStartLine := fset.Position(docGroup.Pos()).Line + prevEndLine := fset.Position(prevGroup.End()).Line + lineDiff := docStartLine - prevEndLine + + // lineDiff > 1: ensures at least one blank line + // lineDiff <= maxMarkerSeparationLines: ensures not too far apart + return lineDiff > 1 && lineDiff <= maxMarkerSeparationLines +} + +// containsMarkers checks if a comment group contains at least one marker. +// It also ensures the comment group doesn't contain commented-out code. +// +// This function detects both single-line and multi-line marker groups that are +// separated from type declarations by blank lines (orphaned markers). +// +// Single-line comments immediately before a type declaration (without a blank line) +// are already captured as Doc comments by the Go AST parser and processed normally. +// +// Example of what IS detected (orphaned markers separated by blank line): +// +// // +kubebuilder:object:root=true +// +// // MyType does something +// type MyType struct {} +// +// Or multi-line: +// +// // +kubebuilder:object:root=true +// // +kubebuilder:subresource:status +// +// // MyType does something +// type MyType struct {} +// +// Example of what is NOT detected (marker without blank line, already handled as Doc comment): +// +// // +kubebuilder:object:root=true +// // MyType does something +// type MyType struct {} +func containsMarkers(group *ast.CommentGroup) bool { + if len(group.List) == 0 { + return false + } + + hasMarker := false + + for _, comment := range group.List { + text := comment.Text + if strings.HasPrefix(text, markerPrefix) { + hasMarker = true + } else if looksLikeCommentedCode(text) { + // Skip comment groups that contain commented-out code + return false + } + } + + return hasMarker +} + +// looksLikeCommentedCode checks if a comment line looks like commented-out code. +func looksLikeCommentedCode(text string) bool { + content := prepareContentForAnalysis(text) + + // Empty lines or lines starting with markers are not code + if content == "" || strings.HasPrefix(content, "+") { + return false + } + + return hasCodeIndicators(content) +} + +// prepareContentForAnalysis strips comment prefixes and normalizes the content. +func prepareContentForAnalysis(text string) string { + content := strings.TrimPrefix(text, "//") + return strings.TrimSpace(content) +} + +// hasCodeIndicators checks if content contains patterns that indicate Go code. +func hasCodeIndicators(content string) bool { + // Check for struct tags (backticks are a strong signal of Go code) + if strings.Contains(content, "`") { + return true + } + + // Check for field declaration patterns + if hasFieldDeclarationPattern(content) { + return true + } + + // Check for assignment operators + if hasAssignmentOperators(content) { + return true + } + + // Check for function call patterns + if hasFunctionCallPattern(content) { + return true + } + + // Check for Go keywords at the start of the line + return hasCodeKeywordPrefix(content) +} + +// hasAssignmentOperators checks if content contains Go assignment operators. +func hasAssignmentOperators(content string) bool { + assignmentOps := []string{" := ", " = ", " += ", " -= ", " *= ", " /="} + for _, op := range assignmentOps { + if strings.Contains(content, op) { + return true + } + } + + return false +} + +// hasCodeKeywordPrefix checks if content starts with Go code keywords. +func hasCodeKeywordPrefix(content string) bool { + // Go declaration keywords + codeKeywords := []string{"func ", "type ", "var ", "const ", "import ", "package ", "struct ", "interface "} + for _, keyword := range codeKeywords { + if strings.HasPrefix(content, keyword) { + return true + } + } + + // Control flow keywords + controlFlowKeywords := []string{"if ", "for ", "switch ", "case ", "return ", "break ", "continue ", "defer ", "go ", "select "} + for _, keyword := range controlFlowKeywords { + if strings.HasPrefix(content, keyword) { + return true + } + } + + return false +} + +// hasFieldDeclarationPattern checks if the content looks like a Go field declaration. +// Examples: "Name string", "Count int", "Enabled *bool", "*Field Type". +func hasFieldDeclarationPattern(content string) bool { + // Look for common Go type names after a potential field name + if typePattern.MatchString(content) { + return true + } + + // Also check for pointer field declarations: *Type + if strings.HasPrefix(content, "*") && len(content) > 1 && content[1] != ' ' { + return true + } + + return false +} + +// hasFunctionCallPattern checks if the content looks like a function call. +// Examples: "someFunc()", "pkg.Method(arg)", "New()". +func hasFunctionCallPattern(content string) bool { + // Simple heuristic: word followed by ( with something inside ) + return funcPattern.MatchString(content) +} + +// isDocCommentForDeclaration checks if the comment group is a Doc comment for any declaration. +func isDocCommentForDeclaration(group *ast.CommentGroup, file *ast.File) bool { + for _, decl := range file.Decls { + switch d := decl.(type) { + case *ast.GenDecl: + if d.Doc == group { + return true + } + case *ast.FuncDecl: + if d.Doc == group { + return true + } + } + } + + return false +} + +// isDocCommentForField checks if the comment group is a Doc comment for any field. +// Uses a pre-computed map for O(1) lookup instead of O(n) AST traversal. +func isDocCommentForField(group *ast.CommentGroup, fieldDocComments map[*ast.CommentGroup]*ast.Field) bool { + _, found := fieldDocComments[group] + return found +} + +func extractFieldMarkers(field *ast.Field, file *ast.File, fset *token.FileSet, results *markers, fieldDocComments map[*ast.CommentGroup]*ast.Field) { + fieldMarkers := NewMarkerSet() + + // Extract markers from the field's Doc field (comments directly attached to the field) + if field != nil && field.Doc != nil { + for _, comment := range field.Doc.List { + marker := extractMarker(comment) + if marker.Identifier != "" { + fieldMarkers.Insert(marker) + } + } + + // Also collect markers from the comment group immediately before the field's doc comment + // if separated by a blank line (orphaned markers). + // For fields, we use a specialized version that only checks if the markers are immediately + // above the doc comment (within the same logical block) to avoid picking up markers from + // previous fields. + extractOrphanedFieldMarkers(field.Doc, file, fset, fieldMarkers, fieldDocComments) + } results.insertFieldMarkers(field, fieldMarkers) } +// validMarkerStart validates that a marker starts with an alphabetic character +// and contains only valid marker content (letters, numbers, colons, parentheses, quotes, spaces, and commas). +// This excludes markdown tables (e.g., "-------") and other non-marker content, +// while supporting declarative validation tags with parentheses and nested markers. +var validMarkerStart = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9:\(\)\"\" ,])+=?`) + +// typePattern matches common Go field declaration patterns. +// Examples: "Name string", "Count int", "Enabled *bool". +var typePattern = regexp.MustCompile(`^\w+\s+\*?(string|int|int32|int64|uint|uint32|uint64|bool|float32|float64|byte|rune)\b`) + +// funcPattern matches function call patterns. +// Examples: "someFunc()", "pkg.Method(arg)", "New()". +var funcPattern = regexp.MustCompile(`\w+(\.\w+)?\([^)]*\)`) + func extractMarker(comment *ast.Comment) Marker { - if !strings.HasPrefix(comment.Text, "// +") { + if !strings.HasPrefix(comment.Text, markerPrefix) { return Marker{} } - markerContent := strings.TrimPrefix(comment.Text, "// +") - id, expressions := extractMarkerIDAndExpressions(DefaultRegistry(), markerContent) + markerContent := strings.TrimPrefix(comment.Text, markerPrefix) + + // Valid markers must start with an alphabetic character (a-zA-Z). + // This excludes markdown tables (e.g., "// +-------") and other non-marker content, + // while supporting declarative validation tags that may include parentheses and nested markers. + if !validMarkerStart.MatchString(markerContent) { + return Marker{} + } + + if isDeclarativeValidationMarker(markerContent) { + marker := extractDeclarativeValidationMarker(markerContent, comment) + if marker == nil { + return Marker{} + } + + return *marker + } + + return extractKubebuilderMarker(markerContent, comment) +} + +func extractKubebuilderMarker(markerContent string, comment *ast.Comment) Marker { + id, arguments, payload := extractMarkerIDArgumentsAndPayload(DefaultRegistry(), markerContent) return Marker{ - Identifier: id, - Expressions: expressions, - RawComment: comment.Text, - Pos: comment.Pos(), - End: comment.End(), + Type: MarkerTypeKubebuilder, + Identifier: id, + Arguments: arguments, + Payload: payload, + RawComment: comment.Text, + Pos: comment.Pos(), + End: comment.End(), } } -func extractMarkerIDAndExpressions(knownMarkers Registry, marker string) (string, map[string]string) { +func extractMarkerIDArgumentsAndPayload(knownMarkers Registry, marker string) (string, map[string]string, Payload) { if id, ok := knownMarkers.Match(marker); ok { - return extractKnownMarkerIDAndExpressions(id, marker) + return extractKnownMarkerIDArgumentsAndPayload(id, marker) } - return extractUnknownMarkerIDAndExpressions(marker) + return extractUnknownMarkerIDArgumentsAndPayload(marker) } -func extractKnownMarkerIDAndExpressions(id string, marker string) (string, map[string]string) { - return id, extractExpressions(strings.TrimPrefix(marker, id)) +func isDeclarativeValidationMarker(marker string) bool { + return strings.HasPrefix(marker, "k8s:") +} + +func extractDeclarativeValidationMarker(marker string, comment *ast.Comment) *Marker { + tag, err := codetags.Parse(marker) + if err != nil { + return nil + } + + return markerForTag(tag, comment) +} + +func markerForTag(tag codetags.Tag, comment *ast.Comment) *Marker { + out := &Marker{ + Type: MarkerTypeDeclarativeValidation, + Identifier: tag.Name, + Arguments: make(map[string]string), + RawComment: comment.Text, + Pos: comment.Pos(), + End: comment.End(), + } + + for _, arg := range tag.Args { + out.Arguments[arg.Name] = arg.Value + } + + switch tag.ValueType { + case codetags.ValueTypeString, codetags.ValueTypeInt, codetags.ValueTypeBool, codetags.ValueTypeRaw: + // all resolvable to an exact string value + out.Payload = Payload{ + Value: tag.Value, + } + case codetags.ValueTypeNone: + // nothing + case codetags.ValueTypeTag: + out.Payload = Payload{ + Marker: markerForTag(*tag.ValueTag, comment), + } + default: + return nil + } + + return out +} + +func extractKnownMarkerIDArgumentsAndPayload(id string, marker string) (string, map[string]string, Payload) { + args, payload := extractArgumentsAndPayload(strings.TrimPrefix(marker, id)) + return id, args, payload } var expressionRegex = regexp.MustCompile("\\w*=(?:'[^']*'|\"(\\\\\"|[^\"])*\"|[\\w;\\-\"]+|`[^`]*`)") -func extractExpressions(expressionStr string) map[string]string { +func extractArgumentsAndPayload(expressionStr string) (map[string]string, Payload) { expressionsMap := map[string]string{} + var payload Payload + // Do some normalization work to ensure we can parse expressions in // a standard way. Trim any lingering colons (:) and replace all ':='s with '=' expressionStr = strings.TrimPrefix(expressionStr, ":") @@ -247,13 +735,18 @@ func extractExpressions(expressionStr string) map[string]string { continue } + if key == UnnamedArgument { + payload.Value = value + continue + } + expressionsMap[key] = value } - return expressionsMap + return expressionsMap, payload } -func extractUnknownMarkerIDAndExpressions(marker string) (string, map[string]string) { +func extractUnknownMarkerIDArgumentsAndPayload(marker string) (string, map[string]string, Payload) { // if there is only a single "=" split on the equal sign and trim any // dangling ":" characters. if strings.Count(marker, "=") == 1 { @@ -263,10 +756,8 @@ func extractUnknownMarkerIDAndExpressions(marker string) (string, map[string]str identifier := strings.TrimSuffix(splits[0], ":") // If there is a single "=" sign that means the left side of the - // marker is the identifier and there is no real expression identifier. - expressions := map[string]string{UnnamedExpression: splits[1]} - - return identifier, expressions + // marker is the identifier and there is no real argument identifier. + return identifier, make(map[string]string), Payload{Value: splits[1]} } // split on : @@ -301,18 +792,65 @@ func extractUnknownMarkerIDAndExpressions(marker string) (string, map[string]str expressionString = strings.Join([]string{expressionString, item}, ",") } - expressions := extractExpressions(expressionString) + expressions, payload := extractArgumentsAndPayload(expressionString) + + return identifier, expressions, payload +} + +// MarkerType is a representation of the style of marker. +// Currently can be one of Kubebuilder or DeclarativeValidation. +type MarkerType string + +const ( + // MarkerTypeKubebuilder represents a Kubebuilder-style marker. + MarkerTypeKubebuilder MarkerType = "Kubebuilder" + // MarkerTypeDeclarativeValidation represents a Declarative Validation marker. + MarkerTypeDeclarativeValidation MarkerType = "DeclarativeValidation" +) + +// Payload represents the payload of a marker. +type Payload struct { + // Value is the payload value of a marker represented as a string. + // Value is set when the payload value of a marker is not another marker. + Value string - return identifier, expressions + // Marker is the marker in the payload value of another marker. + // Marker is only set when the payload value of a marker is another marker. + Marker *Marker } // Marker represents a marker extracted from a comment on a declaration. type Marker struct { + // Type is the marker representation this marker was identified as. + // Currently, the two marker format types are DeclarativeValidation and Kubebuilder. + // Because the Kubebuilder style has been around the longest and is widely + // used in projects that have CustomResourceDefinitions we default to Kubebuilder + // style parsing unless we detect that the marker follows the declarative validation + // format (i.e begins with +k8s:). + Type MarkerType + // Identifier is the value of the marker once the leading comment, '+', and expressions are trimmed. Identifier string - // Expressions are the set of expressions that have been specified for the marker - Expressions map[string]string + // Arguments are the set of named and unnamed arguments that have been specified for the marker. + // + // For Markers with Type == Kubebuilder, there will only ever be named arguments. The following examples highlight how arguments are extracted: + // - `+kubebuilder:validation:Required` would result in *no* arguments. + // - `+required` would result in *no* arguments. + // - `+kubebuilder:validation:MinLength=10` would result in no arguments`. + // - `+kubebuilder:validation:XValidation:rule="has(self)",message="should have self"` would result in 2 named arguments, `rule` and `message` with their respective values in string representation. + // + // For Markers with Type == DeclarativeValidation, arguments are extracted from the marker parameters. Arguments may be named or unnamed. + // Some examples: + // - `+k8s:forbidden` would result in *no* arguments. + // - `+k8s:ifEnabled("my-feature")=...` would result in a single unnamed argument (represented by key `""`) with a value of `"my-feature"`. + // - `+k8s:item(one: "value", two: "value")=...` would result in 2 named arguments, `one` and `two` with their respective values in string representation. + Arguments map[string]string + + // Payload is the payload specified by the marker. + // In general, it is what is present after the first `=` symbol + // of a marker. + Payload Payload // RawComment is the raw comment line, unfiltered. RawComment string @@ -320,13 +858,13 @@ type Marker struct { // Pos is the starting position in the file for the comment line containing the marker. Pos token.Pos - // End is the ending position in the file for the coment line containing the marker. + // End is the ending position in the file for the comment line containing the marker. End token.Pos } // String returns the string representation of the marker. func (m Marker) String() string { - return strings.TrimPrefix(m.RawComment, "// +") + return strings.TrimPrefix(m.RawComment, markerPrefix) } // MarkerSet is a set implementation for Markers that uses @@ -363,22 +901,33 @@ func (ms MarkerSet) Has(identifier string) bool { } // HasWithValue returns whether marker(s) with the given identifier and -// expression values (i.e "kubebuilder:object:root:=true") is present +// argument/payload values (i.e "kubebuilder:object:root:=true") is present // in the MarkerSet. func (ms MarkerSet) HasWithValue(marker string) bool { - return ms.HasWithExpressions(extractMarkerIDAndExpressions(DefaultRegistry(), marker)) + if isDeclarativeValidationMarker(marker) { + marker := extractDeclarativeValidationMarker(marker, &ast.Comment{}) + if marker == nil { + return false + } + + return ms.HasWithArgumentsAndPayload(marker.Identifier, marker.Arguments, marker.Payload) + } + + id, args, payload := extractMarkerIDArgumentsAndPayload(DefaultRegistry(), marker) + + return ms.HasWithArgumentsAndPayload(id, args, payload) } -// HasWithExpressions returns whether marker(s) with the identifier and -// expressions are present in the MarkerSet. -func (ms MarkerSet) HasWithExpressions(identifier string, expressions map[string]string) bool { +// HasWithArgumentsAndPayload returns whether marker(s) with the +// identifier, arguments, and payload are present in the MarkerSet. +func (ms MarkerSet) HasWithArgumentsAndPayload(identifier string, arguments map[string]string, payload Payload) bool { markers, ok := ms[identifier] if !ok { return false } for _, marker := range markers { - if reflect.DeepEqual(marker.Expressions, expressions) { + if reflect.DeepEqual(marker.Arguments, arguments) && reflect.DeepEqual(marker.Payload, payload) { return true } } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/initializer/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/initializer/initializer.go index 60e5c77419d..c2c464d9abb 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/initializer/initializer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/initializer/initializer.go @@ -37,7 +37,7 @@ type AnalyzerInitializer interface { // It will be passed the complete LintersConfig and is expected to rely only on its own configuration. Init(any) (*analysis.Analyzer, error) - // Default determines whether the inializer intializes an analyzer that should be + // Default determines whether the initializer initializes an analyzer that should be // on by default, or not. Default() bool } @@ -64,7 +64,7 @@ func NewInitializer(name string, analyzer *analysis.Analyzer, isDefault bool) An } } -// NewConfigurableInitializer constructs a new initializer for intializing a +// NewConfigurableInitializer constructs a new initializer for initializing a // configurable Analyzer. func NewConfigurableInitializer[T any](name string, initFunc InitializerFunc[T], isDefault bool, validateFunc ValidateFunc[T]) ConfigurableAnalyzerInitializer { return configurableInitializer[T]{ @@ -88,7 +88,7 @@ func (i initializer[T]) Name() string { return i.name } -// Init returns a newly initializr analyzer. +// Init returns a newly initialized analyzer. func (i initializer[T]) Init(_ any) (*analysis.Analyzer, error) { var cfg *T diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/integers/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/integers/analyzer.go index 3ab832356f2..4029244a358 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/integers/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/integers/analyzer.go @@ -19,9 +19,11 @@ import ( "go/ast" "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" + "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" ) @@ -33,37 +35,35 @@ var Analyzer = &analysis.Analyzer{ Name: name, Doc: "All integers should be explicit about their size, int32 and int64 should be used over plain int. Unsigned ints are not allowed.", Run: run, - Requires: []*analysis.Analyzer{inspect.Analyzer}, + Requires: []*analysis.Analyzer{inspector.Analyzer}, } func run(pass *analysis.Pass) (any, error) { - inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) if !ok { return nil, kalerrors.ErrCouldNotGetInspector } - // Filter to fields so that we can look at fields within structs. - // Filter typespecs so that we can look at type aliases. - nodeFilter := []ast.Node{ - (*ast.StructType)(nil), - (*ast.TypeSpec)(nil), - } + typeChecker := utils.NewTypeChecker(utils.IsBasicType, checkIntegers) - typeChecker := utils.NewTypeChecker(checkIntegers) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, _ markers.Markers, _ string) { + typeChecker.CheckNode(pass, field) + }) - // Preorder visits all the nodes of the AST in depth-first order. It calls - // f(n) for each node n before it visits n's children. - // - // We use the filter defined above, ensuring we only look at struct fields and type declarations. - inspect.Preorder(nodeFilter, func(n ast.Node) { - typeChecker.CheckNode(pass, n) + inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { + typeChecker.CheckNode(pass, typeSpec) }) return nil, nil //nolint:nilnil } // checkIntegers looks for known type of integers that do not match the allowed `int32` or `int64` requirements. -func checkIntegers(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { +func checkIntegers(pass *analysis.Pass, expr ast.Expr, node ast.Node, prefix string) { + ident, ok := expr.(*ast.Ident) + if !ok { + return + } + switch ident.Name { case "int32", "int64": // Valid cases diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/integers/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/integers/doc.go index 88a31e958a5..3383ff7dcaa 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/integers/doc.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/integers/doc.go @@ -25,7 +25,7 @@ values larger than int32. It also states that unsigned integers should be replaced with signed integers, and then numeric lower bounds added to prevent negative integers. -Succinctly this anaylzer checks for int, int8, int16, uint, uint8, uint16, uint32 and uint64 types +Succinctly this analyzer checks for int, int8, int16, uint, uint8, uint16, uint32 and uint64 types and highlights that they should not be used. */ package integers diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags/analyzer.go index 22a3142b1bc..0553bcfae7e 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags/analyzer.go @@ -20,13 +20,11 @@ import ( "go/ast" "regexp" + "golang.org/x/tools/go/analysis" 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" - - "golang.org/x/tools/go/analysis" ) const ( @@ -71,20 +69,20 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - a.checkField(pass, field, jsonTagInfo) + inspect.InspectFieldsIncludingListTypes(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, _ markers.Markers, qualifiedFieldName string) { + a.checkField(pass, field, jsonTagInfo, qualifiedFieldName) }) return nil, nil //nolint:nilnil } -func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo) { +func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) { prefix := "field %s" if len(field.Names) == 0 || field.Names[0] == nil { prefix = "embedded field %s" } - prefix = fmt.Sprintf(prefix, utils.FieldName(field)) + prefix = fmt.Sprintf(prefix, qualifiedFieldName) if tagInfo.Missing { pass.Reportf(field.Pos(), "%s is missing json tag", prefix) diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/maxlength/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/maxlength/analyzer.go index 82b01fb6dd7..3a9a282b8b6 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/maxlength/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/maxlength/analyzer.go @@ -47,20 +47,15 @@ func run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) { - checkField(pass, field, markersAccess) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, qualifiedFieldName) }) return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) { - fieldName := utils.FieldName(field) - if fieldName == "" { - return - } - - prefix := fmt.Sprintf("field %s", fieldName) +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) { + prefix := fmt.Sprintf("field %s", qualifiedFieldName) checkTypeExpr(pass, field.Type, field, nil, markersAccess, prefix, markers.KubebuilderMaxLengthMarker, needsStringMaxLength) } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/analyzer.go new file mode 100644 index 00000000000..8ad16e48962 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/analyzer.go @@ -0,0 +1,258 @@ +/* +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 minlength + +import ( + "fmt" + "go/ast" + + "golang.org/x/tools/go/analysis" + 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" + markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +const ( + name = "minlength" +) + +// Analyzer is the analyzer for the minlength package. +// It checks that strings and arrays have minimum lengths and minimum items respectively. +var Analyzer = &analysis.Analyzer{ + Name: name, + Doc: "Checks that all strings formatted fields are marked with a minimum length, and that arrays are marked with min items, maps are marked with min properties, and structs that do not have required fields are marked with min properties", + Run: run, + Requires: []*analysis.Analyzer{inspector.Analyzer}, +} + +func 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, _ extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, qualifiedFieldName) + }) + + return nil, nil //nolint:nilnil +} + +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) { + prefix := fmt.Sprintf("field %s", qualifiedFieldName) + + checkTypeExpr(pass, field.Type, field, nil, markersAccess, prefix, markers.KubebuilderMinLengthMarker, needsStringMinLength) +} + +func checkIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMaxLength func(markershelper.MarkerSet) bool) { + if utils.IsBasicType(pass, ident) { // Built-in type + checkString(pass, ident, node, aliases, markersAccess, prefix, marker, needsMaxLength) + + return + } + + tSpec, ok := utils.LookupTypeSpec(pass, ident) + if !ok { + return + } + + checkTypeSpec(pass, tSpec, node, append(aliases, tSpec), markersAccess, fmt.Sprintf("%s type", prefix), marker, needsMaxLength) +} + +func checkString(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMinLength func(markershelper.MarkerSet) bool) { + if ident.Name != "string" { + return + } + + markers := getCombinedMarkers(markersAccess, node, aliases) + + if needsMinLength(markers) { + pass.Reportf(node.Pos(), "%s must have a minimum length, add %s marker", prefix, marker) + } +} + +func checkTypeSpec(pass *analysis.Pass, tSpec *ast.TypeSpec, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMinLength func(markershelper.MarkerSet) bool) { + if tSpec.Name == nil { + return + } + + typeName := tSpec.Name.Name + prefix = fmt.Sprintf("%s %s", prefix, typeName) + + checkTypeExpr(pass, tSpec.Type, node, aliases, markersAccess, prefix, marker, needsMinLength) +} + +func checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMinLength func(markershelper.MarkerSet) bool) { + switch typ := typeExpr.(type) { + case *ast.Ident: + checkIdent(pass, typ, node, aliases, markersAccess, prefix, marker, needsMinLength) + case *ast.StarExpr: + checkTypeExpr(pass, typ.X, node, aliases, markersAccess, prefix, marker, needsMinLength) + case *ast.ArrayType: + checkArrayType(pass, typ, node, aliases, markersAccess, prefix) + case *ast.MapType: + checkMapType(pass, node, aliases, markersAccess, prefix) + case *ast.StructType: + checkStructType(pass, typ, node, aliases, markersAccess, prefix) + } +} + +func checkArrayType(pass *analysis.Pass, arrayType *ast.ArrayType, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) { + if arrayType.Elt != nil { + if ident, ok := arrayType.Elt.(*ast.Ident); ok { + if ident.Name == "byte" { + // byte slices are a special case as they are treated as strings. + // Pretend the ident is a string so that checkString can process it as expected. + i := &ast.Ident{ + NamePos: ident.NamePos, + Name: "string", + } + checkString(pass, i, node, aliases, markersAccess, prefix, markers.KubebuilderMinLengthMarker, needsStringMinLength) + + return + } + + checkArrayElementIdent(pass, ident, node, aliases, markersAccess, fmt.Sprintf("%s array element", prefix)) + } + } + + markerSet := getCombinedMarkers(markersAccess, node, aliases) + + if !markerSet.Has(markers.KubebuilderMinItemsMarker) { + pass.Reportf(node.Pos(), "%s must have a minimum items, add %s marker", prefix, markers.KubebuilderMinItemsMarker) + } +} + +func checkArrayElementIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) { + if ident.Obj == nil { // Built-in type + checkString(pass, ident, node, aliases, markersAccess, prefix, markers.KubebuilderItemsMinLengthMarker, needsItemsMinLength) + + return + } + + tSpec, ok := ident.Obj.Decl.(*ast.TypeSpec) + if !ok { + return + } + + // If the array element wasn't directly a string, allow a string alias to be used + // with either the items style markers or the on alias style markers. + checkTypeSpec(pass, tSpec, node, append(aliases, tSpec), markersAccess, fmt.Sprintf("%s type", prefix), markers.KubebuilderMinLengthMarker, func(ms markershelper.MarkerSet) bool { + return needsStringMinLength(ms) && needsItemsMinLength(ms) + }) +} + +func checkMapType(pass *analysis.Pass, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) { + markerSet := getCombinedMarkers(markersAccess, node, aliases) + + if !markerSet.Has(markers.KubebuilderMinPropertiesMarker) { + pass.Reportf(node.Pos(), "%s must have a minimum properties, add %s marker", prefix, markers.KubebuilderMinPropertiesMarker) + } +} + +func checkStructType(pass *analysis.Pass, structType *ast.StructType, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) { + markerSet := getCombinedMarkers(markersAccess, node, aliases) + + minProperties, err := utils.GetMinProperties(markerSet) + if err != nil { + pass.Reportf(node.Pos(), "could not get min properties for struct: %v", err) + return + } + + if minProperties != nil { + // There's already a min properties specified. + return + } + + // Check if the struct has union markers that satisfy the required constraint + if markerSet.Has(markers.KubebuilderExactlyOneOf) || markerSet.Has(markers.KubebuilderAtLeastOneOfMarker) { + // ExactlyOneOf / AtLeastOneOf markers enforce that at least one field is required, + // this means that `{}` is not valid. + return + } + + for _, field := range structType.Fields.List { + if utils.IsFieldRequired(field, markersAccess) { + // The struct has at least one required field, + // this means that `{}` is not valid. + return + } + } + + // The field does not have a min properties, and does not have any required fields. + pass.Reportf(node.Pos(), "%s must have a minimum properties, add %s marker", prefix, markers.KubebuilderMinPropertiesMarker) +} + +func getCombinedMarkers(markersAccess markershelper.Markers, node ast.Node, aliases []*ast.TypeSpec) markershelper.MarkerSet { + base := markershelper.NewMarkerSet(getMarkers(markersAccess, node).UnsortedList()...) + + for _, a := range aliases { + base.Insert(getMarkers(markersAccess, a).UnsortedList()...) + } + + return base +} + +func getMarkers(markersAccess markershelper.Markers, node ast.Node) markershelper.MarkerSet { + switch t := node.(type) { + case *ast.Field: + return markersAccess.FieldMarkers(t) + case *ast.TypeSpec: + return markersAccess.TypeMarkers(t) + } + + return nil +} + +// needsMinLength returns true if the field needs a minimum length. +// Fields do not need a minimum length if they are already marked with a minimum length, +// or if they are an enum, or if they are a date, date-time or duration. +func needsStringMinLength(markerSet markershelper.MarkerSet) bool { + switch { + case markerSet.Has(markers.KubebuilderMinLengthMarker), + markerSet.Has(markers.KubebuilderEnumMarker), + markerSet.HasWithValue(kubebuilderFormatWithValue("date")), + markerSet.HasWithValue(kubebuilderFormatWithValue("date-time")), + markerSet.HasWithValue(kubebuilderFormatWithValue("duration")): + return false + } + + return true +} + +func needsItemsMinLength(markerSet markershelper.MarkerSet) bool { + switch { + case markerSet.Has(markers.KubebuilderItemsMinLengthMarker), + markerSet.Has(markers.KubebuilderItemsEnumMarker), + markerSet.HasWithValue(kubebuilderItemsFormatWithValue("date")), + markerSet.HasWithValue(kubebuilderItemsFormatWithValue("date-time")), + markerSet.HasWithValue(kubebuilderItemsFormatWithValue("duration")): + return false + } + + return true +} + +func kubebuilderFormatWithValue(value string) string { + return fmt.Sprintf("%s:=%s", markers.KubebuilderFormatMarker, value) +} + +func kubebuilderItemsFormatWithValue(value string) string { + return fmt.Sprintf("%s:=%s", markers.KubebuilderItemsFormatMarker, value) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/doc.go new file mode 100644 index 00000000000..080849e305e --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/doc.go @@ -0,0 +1,49 @@ +/* +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. +*/ + +/* +minlength is an analyzer that checks that all string fields have a minimum length, and that all array fields have a minimum number of items, +that maps have a minimum number of properties, and that structs that do not have required fields have a minimum number of fields. + +String fields that are not otherwise bound in length, through being an enum or formatted in a certain way, should have a minimum length. +This ensures that authors make a choice about whether or not the empty string is a valid choice for users. + +Array fields should have a minimum number of items. +This ensures that empty arrays are not allowed. +Empty arrays are generally not recommended and API authors should generally not distinguish between empty and omitted arrays. +When the empty array is a valid choice, setting the minimum items marker to 0 can be used to indicate that this is an explicit choice. + +Maps should have a minimum number of properties. +This ensures that empty maps are not allowed. +Empty maps are generally not recommended and API authors should generally not distinguish between empty and omitted maps. +When the empty map is a valid choice, setting the minimum properties marker to 0 can be used to indicate that this is an explicit choice. + +Structs that do not have required fields and do not define an equivalent constraint, i.e., `kubebuilder:validation:ExactlyOneOf` or `kubebuilder:validation:AtLeastOneOf`, +should have a minimum number of fields. +This ensures that empty structs are not allowed. +Empty structs are generally not recommended and API authors should generally not distinguish between empty and omitted structs. +When the empty struct is a valid choice, setting the minimum properties marker to 0 can be used to indicate that this is an explicit choice. + +For strings, the minimum length can be set using the `kubebuilder:validation:MinLength` tag. +For arrays, the minimum number of items can be set using the `kubebuilder:validation:MinItems` tag. +For maps, the minimum number of properties can be set using the `kubebuilder:validation:MinProperties` tag. +For structs, the minimum number of fields can be set using the `kubebuilder:validation:MinProperties` tag. + +For arrays of strings, the minimum length of each string can be set using the `kubebuilder:validation:items:MinLength` tag, +on the array field itself. +Or, if the array uses a string type alias, the `kubebuilder:validation:MinLength` tag can be used on the alias. +*/ +package minlength diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/initializer.go new file mode 100644 index 00000000000..9d0823f31e3 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/minlength/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 minlength + +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, + false, // For now, CRD only, and so not on by default. + ) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions/analyzer.go index 382dbfe213c..ee9f6e65928 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions/analyzer.go @@ -59,14 +59,14 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTags extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - checkField(pass, field, jsonTags, a.conventions...) + inspect.InspectFields(func(field *ast.Field, jsonTags extractjsontags.FieldTagInfo, _ markers.Markers, qualifiedFieldName string) { + checkField(pass, field, jsonTags, qualifiedFieldName, a.conventions...) }) return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo, conventions ...Convention) { +func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string, conventions ...Convention) { if field == nil || len(field.Names) == 0 { return } @@ -85,29 +85,29 @@ func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.F switch convention.Operation { case OperationInform: - reportConventionWithSuggestedFixes(pass, field, convention) + reportConventionWithSuggestedFixes(pass, field, convention, qualifiedFieldName) case OperationDropField: - reportDropField(pass, field, convention) + reportDropField(pass, field, convention, qualifiedFieldName) case OperationDrop: - reportDrop(pass, field, tagInfo, convention, matcher) + reportDrop(pass, field, tagInfo, convention, matcher, qualifiedFieldName) case OperationReplacement: - reportReplace(pass, field, tagInfo, convention, matcher) + reportReplace(pass, field, tagInfo, convention, matcher, qualifiedFieldName) } } } -func reportConventionWithSuggestedFixes(pass *analysis.Pass, field *ast.Field, convention Convention, suggestedFixes ...analysis.SuggestedFix) { +func reportConventionWithSuggestedFixes(pass *analysis.Pass, field *ast.Field, convention Convention, qualifiedFieldName string, suggestedFixes ...analysis.SuggestedFix) { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("naming convention %q: field %s: %s", convention.Name, utils.FieldName(field), convention.Message), + Message: fmt.Sprintf("naming convention %q: field %s: %s", convention.Name, qualifiedFieldName, convention.Message), SuggestedFixes: suggestedFixes, }) } -func reportDropField(pass *analysis.Pass, field *ast.Field, convention Convention) { +func reportDropField(pass *analysis.Pass, field *ast.Field, convention Convention, qualifiedFieldName string) { suggestedFixes := []analysis.SuggestedFix{ { Message: "remove the field", @@ -121,17 +121,17 @@ func reportDropField(pass *analysis.Pass, field *ast.Field, convention Conventio }, } - reportConventionWithSuggestedFixes(pass, field, convention, suggestedFixes...) + reportConventionWithSuggestedFixes(pass, field, convention, qualifiedFieldName, suggestedFixes...) } -func reportDrop(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo, convention Convention, matcher *regexp.Regexp) { +func reportDrop(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo, convention Convention, matcher *regexp.Regexp, qualifiedFieldName string) { suggestedFixes := suggestedFixesForReplacement(field, tagInfo, matcher, "") - reportConventionWithSuggestedFixes(pass, field, convention, suggestedFixes...) + reportConventionWithSuggestedFixes(pass, field, convention, qualifiedFieldName, suggestedFixes...) } -func reportReplace(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo, convention Convention, matcher *regexp.Regexp) { +func reportReplace(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo, convention Convention, matcher *regexp.Regexp, qualifiedFieldName string) { suggestedFixes := suggestedFixesForReplacement(field, tagInfo, matcher, convention.Replacement) - reportConventionWithSuggestedFixes(pass, field, convention, suggestedFixes...) + reportConventionWithSuggestedFixes(pass, field, convention, qualifiedFieldName, suggestedFixes...) } func suggestedFixesForReplacement(field *ast.Field, tagInfo extractjsontags.FieldTagInfo, matcher *regexp.Regexp, replacementStr string) []analysis.SuggestedFix { diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions/doc.go index 588a2576a05..0398a7c609e 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions/doc.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions/doc.go @@ -25,12 +25,12 @@ Naming conventions must have: - A unique human-readable name. - A human-readable message to be included in violation errors. - A regular expression that will match text within the field name that violates the convention. -- A defined "operation". Allowed operations are "Inform", "Drop", "DropField", and "Replace". +- A defined "operation". Allowed operations are "Inform", "Drop", "DropField", and "Replacement". The "Inform" operation will simply inform via a linter error when a field name violates the naming convention. The "Drop" operation will suggest a fix that drops violating text from the field name. The "DropField" operation will suggest a fix that removes the field in it's entirety. -The "Replace" operation will suggest a fix that replaces the violating text in the field name with a defined replacement value. +The "Replacement" operation will suggest a fix that replaces the violating text in the field name with a defined replacement value. Some example configurations: diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nobools/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nobools/analyzer.go index cd7661284b5..febdf6b6298 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nobools/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nobools/analyzer.go @@ -19,9 +19,11 @@ import ( "go/ast" "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" + "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" ) @@ -33,36 +35,34 @@ var Analyzer = &analysis.Analyzer{ Name: name, Doc: "Boolean values cannot evolve over time, use an enum with meaningful values instead", Run: run, - Requires: []*analysis.Analyzer{inspect.Analyzer}, + Requires: []*analysis.Analyzer{inspector.Analyzer}, } func run(pass *analysis.Pass) (any, error) { - inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) if !ok { return nil, kalerrors.ErrCouldNotGetInspector } - // Filter to fields so that we can look at fields within structs. - // Filter typespecs so that we can look at type aliases. - nodeFilter := []ast.Node{ - (*ast.StructType)(nil), - (*ast.TypeSpec)(nil), - } + typeChecker := utils.NewTypeChecker(utils.IsBasicType, checkBool) - typeChecker := utils.NewTypeChecker(checkBool) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, _ markers.Markers, _ string) { + typeChecker.CheckNode(pass, field) + }) - // Preorder visits all the nodes of the AST in depth-first order. It calls - // f(n) for each node n before it visits n's children. - // - // We use the filter defined above, ensuring we only look at struct fields and type declarations. - inspect.Preorder(nodeFilter, func(n ast.Node) { - typeChecker.CheckNode(pass, n) + inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { + typeChecker.CheckNode(pass, typeSpec) }) return nil, nil //nolint:nilnil } -func checkBool(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { +func checkBool(pass *analysis.Pass, expr ast.Expr, node ast.Node, prefix string) { + ident, ok := expr.(*ast.Ident) + if !ok { + return + } + if ident.Name == "bool" { pass.Reportf(node.Pos(), "%s should not use a bool. Use a string type with meaningful constant values as an enum.", prefix) } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nobools/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nobools/doc.go index e4b09d18a70..d68e7975d2f 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nobools/doc.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nobools/doc.go @@ -25,7 +25,7 @@ This is confusing and error-prone. It is recommended instead to use a string type with a set of constants to represent the different states, creating an enum. -By using an enum, not only can you provide meaningul names for the various states of the API, +By using an enum, not only can you provide meaningful names for the various states of the API, but you can also add additional states in the future without breaking the API. */ package nobools diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nodurations/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nodurations/analyzer.go index e2d915e9060..80771eb98d0 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nodurations/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nodurations/analyzer.go @@ -17,7 +17,6 @@ limitations under the License. package nodurations import ( - "fmt" "go/ast" "golang.org/x/tools/go/analysis" @@ -45,83 +44,41 @@ func run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, _ []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - checkField(pass, field) + typeChecker := utils.NewTypeChecker(isDurationType, checkDuration) + + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, _ markers.Markers, _ string) { + typeChecker.CheckNode(pass, field) }) inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { - checkTypeSpec(pass, typeSpec, typeSpec, "type") + typeChecker.CheckNode(pass, typeSpec) }) return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field) { - fieldName := utils.FieldName(field) - if fieldName == "" { - return +func isDurationType(pass *analysis.Pass, expr ast.Expr) bool { + typ, ok := expr.(*ast.SelectorExpr) + if !ok { + return false } - prefix := fmt.Sprintf("field %s", fieldName) - - checkTypeExpr(pass, field.Type, field, prefix) -} - -//nolint:cyclop -func checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node ast.Node, prefix string) { - switch typ := typeExpr.(type) { - case *ast.SelectorExpr: - pkg, ok := typ.X.(*ast.Ident) - if !ok { - return - } - - if typ.X == nil || (pkg.Name != "time" && pkg.Name != "metav1") { - return - } - - // Array element is not a metav1.Condition. - if typ.Sel == nil || typ.Sel.Name != "Duration" { - return - } - - pass.Reportf(node.Pos(), "%s should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing.", prefix) - case *ast.Ident: - checkIdent(pass, typ, node, prefix) - case *ast.StarExpr: - checkTypeExpr(pass, typ.X, node, fmt.Sprintf("%s pointer", prefix)) - case *ast.ArrayType: - checkTypeExpr(pass, typ.Elt, node, fmt.Sprintf("%s array element", prefix)) - case *ast.MapType: - checkTypeExpr(pass, typ.Key, node, fmt.Sprintf("%s map key", prefix)) - checkTypeExpr(pass, typ.Value, node, fmt.Sprintf("%s map value", prefix)) + pkg, ok := typ.X.(*ast.Ident) + if !ok { + return false } -} -// checkIdent calls the checkFunc with the ident, when we have hit a built-in type. -// If the ident is not a built in, we look at the underlying type until we hit a built-in type. -func checkIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { - if utils.IsBasicType(pass, ident) { - // We've hit a built-in type, no need to check further. - return + if typ.X == nil || (pkg.Name != "time" && pkg.Name != "metav1") { + return false } - tSpec, ok := utils.LookupTypeSpec(pass, ident) - if !ok { - return + if typ.Sel == nil || typ.Sel.Name != "Duration" { + return false } - // The field is using a type alias, check if the alias is an int. - checkTypeSpec(pass, tSpec, node, fmt.Sprintf("%s type", prefix)) + return true } -func checkTypeSpec(pass *analysis.Pass, tSpec *ast.TypeSpec, node ast.Node, prefix string) { - if tSpec.Name == nil { - return - } - - typeName := tSpec.Name.Name - prefix = fmt.Sprintf("%s %s", prefix, typeName) - - checkTypeExpr(pass, tSpec.Type, node, prefix) +func checkDuration(pass *analysis.Pass, expr ast.Expr, node ast.Node, prefix string) { + pass.Reportf(node.Pos(), "%s should not use a Duration. Use an integer type with units in the name to avoid the need for clients to implement Go style duration parsing.", prefix) } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nofloats/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nofloats/analyzer.go index e898116c531..9950d34d6e2 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nofloats/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nofloats/analyzer.go @@ -19,9 +19,11 @@ import ( "go/ast" "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" + "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" ) @@ -33,36 +35,34 @@ var Analyzer = &analysis.Analyzer{ Name: name, Doc: "Float values cannot be reliably round-tripped without changing and have varying precisions and representations across languages and architectures.", Run: run, - Requires: []*analysis.Analyzer{inspect.Analyzer}, + Requires: []*analysis.Analyzer{inspector.Analyzer}, } func run(pass *analysis.Pass) (any, error) { - inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) if !ok { return nil, kalerrors.ErrCouldNotGetInspector } - // Filter to structs so that we can look at fields within structs. - // Filter typespecs so that we can look at type aliases. - nodeFilter := []ast.Node{ - (*ast.StructType)(nil), - (*ast.TypeSpec)(nil), - } + typeChecker := utils.NewTypeChecker(utils.IsBasicType, checkFloat) - typeChecker := utils.NewTypeChecker(checkFloat) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, _ markers.Markers, _ string) { + typeChecker.CheckNode(pass, field) + }) - // Preorder visits all the nodes of the AST in depth-first order. It calls - // f(n) for each node n before it visits n's children. - // - // We use the filter defined above, ensuring we only look at struct fields and type declarations. - inspect.Preorder(nodeFilter, func(n ast.Node) { - typeChecker.CheckNode(pass, n) + inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { + typeChecker.CheckNode(pass, typeSpec) }) return nil, nil //nolint:nilnil } -func checkFloat(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { +func checkFloat(pass *analysis.Pass, expr ast.Expr, node ast.Node, prefix string) { + ident, ok := expr.(*ast.Ident) + if !ok { + return + } + if ident.Name == "float32" || ident.Name == "float64" { pass.Reportf(node.Pos(), "%s should not use a float value because they cannot be reliably round-tripped.", prefix) } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps/analyzer.go index 952a635a9af..f550cfabb02 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps/analyzer.go @@ -19,7 +19,6 @@ import ( "fmt" "go/ast" "go/token" - "go/types" "golang.org/x/tools/go/analysis" kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" @@ -63,52 +62,51 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - a.checkField(pass, field) + typeChecker := utils.NewTypeChecker(isMap, a.checkMap) + + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, _ markers.Markers, _ string) { + typeChecker.CheckNode(pass, field) + }) + + inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, _ markers.Markers) { + typeChecker.CheckNode(pass, typeSpec) }) return nil, nil //nolint:nilnil } -func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field) { - underlyingType := pass.TypesInfo.TypeOf(field.Type).Underlying() +func isMap(pass *analysis.Pass, expr ast.Expr) bool { + _, ok := expr.(*ast.MapType) - if ptr, ok := underlyingType.(*types.Pointer); ok { - underlyingType = ptr.Elem().Underlying() - } + return ok +} - m, ok := underlyingType.(*types.Map) +func (a *analyzer) checkMap(pass *analysis.Pass, expr ast.Expr, node ast.Node, prefix string) { + mapType, ok := expr.(*ast.MapType) if !ok { return } - if a.policy == NoMapsEnforce { - report(pass, field.Pos(), utils.FieldName(field)) - return - } - - if a.policy == NoMapsAllowStringToStringMaps { - if types.AssignableTo(m.Elem().Underlying(), types.Typ[types.String]) && - types.AssignableTo(m.Key().Underlying(), types.Typ[types.String]) { - return + switch a.policy { + case NoMapsEnforce: + report(pass, node.Pos(), prefix) + case NoMapsAllowStringToStringMaps: + if !isStringToStringMap(pass, mapType) { + report(pass, node.Pos(), prefix) + } + case NoMapsIgnore: + if !isBasicMap(pass, mapType) { + report(pass, node.Pos(), prefix) } - - report(pass, field.Pos(), utils.FieldName(field)) } +} - if a.policy == NoMapsIgnore { - key := m.Key().Underlying() - _, ok := key.(*types.Basic) - - elm := m.Elem().Underlying() - _, ok2 := elm.(*types.Basic) - - if ok && ok2 { - return - } +func isStringToStringMap(pass *analysis.Pass, mapType *ast.MapType) bool { + return utils.IsStringType(pass, mapType.Key) && utils.IsStringType(pass, mapType.Value) +} - report(pass, field.Pos(), utils.FieldName(field)) - } +func isBasicMap(pass *analysis.Pass, mapType *ast.MapType) bool { + return utils.IsBasicType(pass, mapType.Key) && utils.IsBasicType(pass, mapType.Value) } func report(pass *analysis.Pass, pos token.Pos, fieldName string) { diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps/initializer.go index 4ed65d6ce5c..cd2d2cb8326 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps/initializer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps/initializer.go @@ -39,7 +39,7 @@ func Initializer() initializer.AnalyzerInitializer { ) } -// Init returns the intialized Analyzer. +// Init returns the initialized Analyzer. func initAnalyzer(nmc *NoMapsConfig) (*analysis.Analyzer, error) { return newAnalyzer(nmc), nil } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/analyzer.go new file mode 100644 index 00000000000..08448b1ad24 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/analyzer.go @@ -0,0 +1,208 @@ +/* +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 nonpointerstructs + +import ( + "fmt" + "go/ast" + + "golang.org/x/tools/go/analysis" + 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" + markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +const name = "nonpointerstructs" + +func newAnalyzer(cfg *Config) *analysis.Analyzer { + if cfg == nil { + cfg = &Config{} + } + + defaultConfig(cfg) + + a := &analyzer{ + preferredRequiredMarker: cfg.PreferredRequiredMarker, + preferredOptionalMarker: cfg.PreferredOptionalMarker, + } + + return &analysis.Analyzer{ + Name: name, + Doc: "Checks that non-pointer structs that contain required fields are marked as required. Non-pointer structs that contain no required fields are marked as optional.", + Run: a.run, + Requires: []*analysis.Analyzer{inspector.Analyzer}, + } +} + +type analyzer struct { + preferredRequiredMarker string + preferredOptionalMarker string +} + +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, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + a.checkField(pass, field, markersAccess, jsonTagInfo, qualifiedFieldName) + }) + + return nil, nil //nolint:nilnil +} + +func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) { + if field.Type == nil { + return + } + + if jsonTagInfo.Inline { + return + } + + structType, ok := asNonPointerStruct(pass, field.Type) + if !ok { + return + } + + hasRequiredField := hasRequiredField(structType, markersAccess) + isOptional := utils.IsFieldOptional(field, markersAccess) + isRequired := utils.IsFieldRequired(field, markersAccess) + + switch { + case hasRequiredField && isRequired, !hasRequiredField && isOptional: + // This is the desired case. + case hasRequiredField: + a.handleShouldBeRequired(pass, field, markersAccess, qualifiedFieldName) + case !hasRequiredField: + a.handleShouldBeOptional(pass, field, markersAccess, qualifiedFieldName) + } +} + +func asNonPointerStruct(pass *analysis.Pass, field ast.Expr) (*ast.StructType, bool) { + switch typ := field.(type) { + case *ast.StructType: + return typ, true + case *ast.Ident: + typeSpec, ok := utils.LookupTypeSpec(pass, typ) + if !ok { + return nil, false + } + + return asNonPointerStruct(pass, typeSpec.Type) + default: + return nil, false + } +} + +func hasRequiredField(structType *ast.StructType, markersAccess markershelper.Markers) bool { + for _, field := range structType.Fields.List { + if utils.IsFieldRequired(field, markersAccess) { + return true + } + } + + structMarkers := markersAccess.StructMarkers(structType) + + if structMarkers.Has(markers.KubebuilderMinPropertiesMarker) && !structMarkers.HasWithValue(fmt.Sprintf("%s=0", markers.KubebuilderMinPropertiesMarker)) { + // A non-zero min properties marker means that the struct is validated to have at least one field. + // This means it can be treated the same as having a required field. + return true + } + + return false +} + +func defaultConfig(cfg *Config) { + if cfg.PreferredRequiredMarker == "" { + cfg.PreferredRequiredMarker = markers.RequiredMarker + } + + if cfg.PreferredOptionalMarker == "" { + cfg.PreferredOptionalMarker = markers.OptionalMarker + } +} + +func (a *analyzer) handleShouldBeRequired(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) { + fieldMarkers := markersAccess.FieldMarkers(field) + + textEdits := []analysis.TextEdit{} + + for _, marker := range []string{markers.OptionalMarker, markers.KubebuilderOptionalMarker, markers.K8sOptionalMarker} { + for _, m := range fieldMarkers.Get(marker) { + textEdits = append(textEdits, analysis.TextEdit{ + Pos: m.Pos, + End: m.End + 1, // Add 1 to include the newline character + NewText: nil, + }) + } + } + + textEdits = append(textEdits, analysis.TextEdit{ + Pos: field.Pos(), + End: field.Pos(), + NewText: fmt.Appendf(nil, "// +%s\n", a.preferredRequiredMarker), + }) + + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + End: field.Pos(), + Message: fmt.Sprintf("field %s is a non-pointer struct with required fields. It must be marked as required.", qualifiedFieldName), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "should mark the field as required", + TextEdits: textEdits, + }, + }, + }) +} + +func (a *analyzer) handleShouldBeOptional(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) { + fieldMarkers := markersAccess.FieldMarkers(field) + + textEdits := []analysis.TextEdit{} + + for _, marker := range []string{markers.RequiredMarker, markers.KubebuilderRequiredMarker, markers.K8sRequiredMarker} { + for _, m := range fieldMarkers.Get(marker) { + textEdits = append(textEdits, analysis.TextEdit{ + Pos: m.Pos, + End: m.End + 1, // Add 1 to include the newline character + NewText: nil, + }) + } + } + + textEdits = append(textEdits, analysis.TextEdit{ + Pos: field.Pos(), + End: field.Pos(), + NewText: fmt.Appendf(nil, "// +%s\n", a.preferredOptionalMarker), + }) + + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s is a non-pointer struct with no required fields. It must be marked as optional.", qualifiedFieldName), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "should mark the field as optional", + TextEdits: textEdits, + }, + }, + }) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/config.go new file mode 100644 index 00000000000..c4956590916 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/config.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 nonpointerstructs + +// Config is the configuration for the nonpointerstructs linter. +type Config struct { + // preferredRequiredMarker is the preferred marker to use for required fields when providing fixes. + // If this field is not set, the default value is "required". + // Valid values are "required" and "kubebuilder:validation:Required" and "k8s:required". + PreferredRequiredMarker string `json:"preferredRequiredMarker"` + + // preferredOptionalMarker is the preferred marker to use for optional fields when providing fixes. + // If this field is not set, the default value is "optional". + // Valid values are "optional" and "kubebuilder:validation:Optional" and "k8s:optional". + PreferredOptionalMarker string `json:"preferredOptionalMarker"` +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/doc.go new file mode 100644 index 00000000000..6c938494130 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/doc.go @@ -0,0 +1,34 @@ +/* +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. +*/ + +/* +nonpointerstructs is a linter that checks that non-pointer structs that contain required fields are marked as required. +Non-pointer structs that contain no required fields are marked as optional. + +This linter is important for types validated in Go as there is no way to validate the optionality of the fields at runtime, +aside from checking the fields within them. + +This linter is NOT intended to be used to check for CRD types. +The advice of this linter may be applied to CRD types, but it is not necessary for CRD types due to optionality being validated by openapi and no native Go code. +For CRD types, the optionalfields and requiredfields linters should be used instead. + +If a struct is marked required, this can only be validated by having a required field within it. +If there are no required fields, the struct is implicitly optional and must be marked as so. + +To have an optional struct field that includes required fields, the struct must be a pointer. +To have a required struct field that includes no required fields, the struct must be a pointer. +*/ +package nonpointerstructs diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/initializer.go new file mode 100644 index 00000000000..c615e7d133c --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs/initializer.go @@ -0,0 +1,67 @@ +/* +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 nonpointerstructs + +import ( + "fmt" + + "golang.org/x/tools/go/analysis" + "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" + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +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, + true, + validateConfig, + ) +} + +func initAnalyzer(cfg *Config) (*analysis.Analyzer, error) { + return newAnalyzer(cfg), nil +} + +func validateConfig(cfg *Config, fldPath *field.Path) field.ErrorList { + if cfg == nil { + return field.ErrorList{} + } + + fieldErrors := field.ErrorList{} + + switch cfg.PreferredRequiredMarker { + case "", markers.RequiredMarker, markers.KubebuilderRequiredMarker, markers.K8sRequiredMarker: + default: + fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("preferredRequiredMarker"), cfg.PreferredRequiredMarker, fmt.Sprintf("invalid value, must be one of %q, %q, %q or omitted", markers.RequiredMarker, markers.KubebuilderRequiredMarker, markers.K8sRequiredMarker))) + } + + switch cfg.PreferredOptionalMarker { + case "", markers.OptionalMarker, markers.KubebuilderOptionalMarker, markers.K8sOptionalMarker: + default: + fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("preferredOptionalMarker"), cfg.PreferredOptionalMarker, fmt.Sprintf("invalid value, must be one of %q, %q, %q or omitted", markers.OptionalMarker, markers.KubebuilderOptionalMarker, markers.K8sOptionalMarker))) + } + + return fieldErrors +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/analyzer.go new file mode 100644 index 00000000000..eea669f1447 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/analyzer.go @@ -0,0 +1,109 @@ +/* +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 noreferences + +import ( + "errors" + "fmt" + + "golang.org/x/tools/go/analysis" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/kube-api-linter/pkg/analysis/initializer" + "sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions" +) + +const ( + name = "noreferences" + doc = "Enforces that fields use Ref/Refs and not Reference/References" +) + +var ( + errUnexpectedInitializerType = errors.New("expected namingconventions.Initializer() to be of type initializer.ConfigurableAnalyzerInitializer, but was not") + errInvalidPolicy = errors.New("invalid policy") +) + +// newAnalyzer creates a new analyzer for the noreferences linter that is a wrapper around the namingconventions linter. +func newAnalyzer(cfg *Config) *analysis.Analyzer { + if cfg == nil { + cfg = &Config{} + } + + // Default to PreferAbbreviatedReference if no policy is specified + policy := cfg.Policy + if policy == "" { + policy = PolicyPreferAbbreviatedReference + } + + // Build the namingconventions config based on the policy + ncConfig := &namingconventions.Config{ + Conventions: buildConventions(policy), + } + + // Get the configurable initializer for namingconventions + configInit, ok := namingconventions.Initializer().(initializer.ConfigurableAnalyzerInitializer) + if !ok { + panic(fmt.Errorf("getting initializer: %w", errUnexpectedInitializerType)) + } + + // Validate generated namingconventions configuration + errs := configInit.ValidateConfig(ncConfig, field.NewPath("noreferences")) + if err := errs.ToAggregate(); err != nil { + panic(fmt.Errorf("noreferences linter has an invalid namingconventions configuration: %w", err)) + } + + // Initialize the wrapped analyzer + analyzer, err := configInit.Init(ncConfig) + if err != nil { + panic(fmt.Errorf("noreferences linter encountered an error initializing wrapped namingconventions analyzer: %w", err)) + } + + analyzer.Name = name + analyzer.Doc = doc + + return analyzer +} + +// buildConventions creates the naming conventions based on the policy. +func buildConventions(policy Policy) []namingconventions.Convention { + switch policy { + case PolicyPreferAbbreviatedReference: + // Replace "Reference" or "References" with "Ref" or "Refs" anywhere in field name + return []namingconventions.Convention{ + { + Name: "reference-to-ref", + ViolationMatcher: "^[Rr]eference|Reference(s?)$", + Operation: namingconventions.OperationReplacement, + Replacement: "Ref$1", + Message: "field names should use 'Ref' instead of 'Reference'", + }, + } + + case PolicyNoReferences: + // Warn about reference words anywhere in field name without providing fixes + return []namingconventions.Convention{ + { + Name: "no-references", + ViolationMatcher: "^[Rr]ef(erence)?(s?)([A-Z])|Ref(erence)?(s?)$", + Operation: namingconventions.OperationInform, + Message: "field names should not contain reference-related words", + }, + } + + default: + // Should not happen due to validation + panic(fmt.Errorf("%w: %s", errInvalidPolicy, policy)) + } +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/config.go new file mode 100644 index 00000000000..eda46ba297d --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/config.go @@ -0,0 +1,36 @@ +/* +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 noreferences + +// Policy defines the policy for handling references in field names. +type Policy string + +const ( + // PolicyPreferAbbreviatedReference allows abbreviated forms (Ref/Refs) in field names. + // It suggests replacing Reference/References with Ref/Refs. + PolicyPreferAbbreviatedReference Policy = "PreferAbbreviatedReference" + // PolicyNoReferences forbids any reference-related words in field names. + // It suggests removing Ref/Refs/Reference/References entirely. + PolicyNoReferences Policy = "NoReferences" +) + +// Config represents the configuration for the noreferences linter. +type Config struct { + // policy controls how reference-related words are handled in field names. + // When set to PreferAbbreviatedReference (default), Reference/References are replaced with Ref/Refs. + // When set to NoReferences, all reference-related words are suggested to be removed. + Policy Policy `json:"policy,omitempty"` +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/doc.go new file mode 100644 index 00000000000..436a16726be --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/doc.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. +*/ + +/* +The `noreferences` linter ensures that field names use 'Ref'/'Refs' instead of 'Reference'/'References'. +By default, `noreferences` is enabled and enforces this naming convention. +The linter checks that 'Reference' is present at the beginning or end of the field name, and replaces it with 'Ref'. +Similarly, 'References' anywhere in field names is replaced with 'Refs'. + +Example configuration: +Default behavior (allow Ref/Refs in field names): + + lintersConfig: + noreferences: + policy: PreferAbbreviatedReference + +Strict mode (forbid Ref/Refs in field names): + + lintersConfig: + noreferences: + policy: NoReferences + +When `policy` is set to `PreferAbbreviatedReference` (the default), fields containing 'Ref' or 'Refs' are allowed. +The policy can be set to `NoReferences` to also report errors for 'Ref' or 'Refs' in field names. +*/ +package noreferences diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/initializer.go new file mode 100644 index 00000000000..df4a9d9cb3e --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences/initializer.go @@ -0,0 +1,64 @@ +/* +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 noreferences + +import ( + "golang.org/x/tools/go/analysis" + "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, + true, + validateConfig, + ) +} + +func initAnalyzer(cfg *Config) (*analysis.Analyzer, error) { + return newAnalyzer(cfg), nil +} + +// validateConfig validates the configuration for the noreferences linter. +func validateConfig(cfg *Config, fldPath *field.Path) field.ErrorList { + if cfg == nil { + return nil // nil config is valid, will use defaults + } + + var errs field.ErrorList + + // Validate Policy enum if provided + switch cfg.Policy { + case PolicyPreferAbbreviatedReference, PolicyNoReferences, "": + default: + errs = append(errs, field.NotSupported( + fldPath.Child("policy"), + cfg.Policy, + []string{string(PolicyPreferAbbreviatedReference), string(PolicyNoReferences)}, + )) + } + + return errs +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/analyzer.go index 80c0d840343..6d174663584 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/analyzer.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" "sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization" "sigs.k8s.io/kube-api-linter/pkg/markers" ) @@ -93,20 +94,19 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) { - a.checkField(pass, field, markersAccess, jsonTagInfo) + inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + a.checkField(pass, field, markersAccess, jsonTagInfo, qualifiedFieldName) }) return nil, nil //nolint:nilnil } -func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo) { +func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo, qualifiedFieldName string) { if field == nil || len(field.Names) == 0 { return } - fieldMarkers := markersAccess.FieldMarkers(field) - if !isFieldOptional(fieldMarkers) { + if !utils.IsFieldOptional(field, markersAccess) { // The field is not marked optional, so we don't need to check it. return } @@ -116,7 +116,7 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce return } - a.serializationCheck.Check(pass, field, markersAccess, jsonTags) + a.serializationCheck.Check(pass, field, markersAccess, jsonTags, qualifiedFieldName) } func defaultConfig(cfg *OptionalFieldsConfig) { @@ -136,8 +136,3 @@ func defaultConfig(cfg *OptionalFieldsConfig) { cfg.OmitZero.Policy = OptionalFieldsOmitZeroPolicySuggestFix } } - -// isFieldOptional checks if a field has an optional marker. -func isFieldOptional(fieldMarkers markershelper.MarkerSet) bool { - return fieldMarkers.Has(markers.OptionalMarker) || fieldMarkers.Has(markers.KubebuilderOptionalMarker) -} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/config.go index f627addc43b..02269a15f5d 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/config.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/config.go @@ -40,7 +40,7 @@ type OptionalFieldsPointers struct { // Valid values are "Always" and "WhenRequired". // When set to "Always", the linter will prefer pointers for all optional fields. // When set to "WhenRequired", the linter will prefer pointers for optional fields where validation or serialization requires a pointer. - // The "WhenRequired" option requires bounds on strings and numerical values to be able to acurately determine the correct pointer vs non-pointer decision. + // The "WhenRequired" option requires bounds on strings and numerical values to be able to accurately determine the correct pointer vs non-pointer decision. // When otherwise not specified, the default value is "Always". Preference OptionalFieldsPointerPreference `json:"preference"` // policy is the policy for the pointer preferences for optional fields. diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/doc.go index a31a73bb9a6..b9035dcc835 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/doc.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/doc.go @@ -23,7 +23,7 @@ However, where the zero value for a field is not a valid value (e.g. the empty s In this case, the field may not need to be a pointer, and, with the WhenRequired preference, the linter will point out where the fields do not need to be pointers. Structs are also inspected to determine if they require a pointer. -If a struct has any required fields, or a minimum numebr of properties, then fields leveraging the struct should be pointers. +If a struct has any required fields, or a minimum number of properties, then fields leveraging the struct should be pointers. Optional structs do not always need to be pointers, but may be marshalled as `{}` because the JSON marshaller in Go cannot determine whether a struct is empty or not. */ diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/initializer.go index 14980741fa4..23cb329ee61 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/initializer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/initializer.go @@ -39,7 +39,7 @@ func Initializer() initializer.AnalyzerInitializer { ) } -// Init returns the intialized Analyzer. +// Init returns the initialized Analyzer. func initAnalyzer(ofc *OptionalFieldsConfig) (*analysis.Analyzer, error) { return newAnalyzer(ofc), nil } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired/analyzer.go index 934e7725935..3af5f6d6e9f 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired/analyzer.go @@ -24,7 +24,6 @@ import ( "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" - "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" "sigs.k8s.io/kube-api-linter/pkg/markers" ) @@ -93,8 +92,8 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) { - a.checkField(pass, field, markersAccess.FieldMarkers(field), jsonTagInfo) + inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + a.checkField(pass, field, markersAccess.FieldMarkers(field), jsonTagInfo, qualifiedFieldName) }) inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markershelper.Markers) { @@ -105,7 +104,7 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { } //nolint:cyclop -func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, fieldMarkers markershelper.MarkerSet, fieldTagInfo extractjsontags.FieldTagInfo) { +func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, fieldMarkers markershelper.MarkerSet, fieldTagInfo extractjsontags.FieldTagInfo, qualifiedFieldName string) { if fieldTagInfo.Inline { // Inline fields would have no effect if they were marked as optional/required. return @@ -116,7 +115,7 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, fieldMarker prefix = "embedded field %s" } - prefix = fmt.Sprintf(prefix, utils.FieldName(field)) + prefix = fmt.Sprintf(prefix, qualifiedFieldName) hasPrimaryOptional := fieldMarkers.Has(a.primaryOptionalMarker) hasPrimaryRequired := fieldMarkers.Has(a.primaryRequiredMarker) diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/analyzer.go new file mode 100644 index 00000000000..494a03da6d0 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/analyzer.go @@ -0,0 +1,315 @@ +/* +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 preferredmarkers + +import ( + "fmt" + "go/ast" + "go/token" + "sort" + "strings" + + "golang.org/x/tools/go/analysis" + 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" +) + +const name = "preferredmarkers" + +type analyzer struct { + // equivalentToPreferred maps equivalent marker identifiers to their preferred identifiers + equivalentToPreferred map[string]string +} + +// newAnalyzer creates a new analysis.Analyzer for the preferredmarkers +// linter based on the provided Config. +func newAnalyzer(cfg *Config) *analysis.Analyzer { + a := &analyzer{ + equivalentToPreferred: make(map[string]string), + } + + // Build the mapping from equivalent identifiers to preferred identifiers + for _, marker := range cfg.Markers { + for _, equivalent := range marker.EquivalentIdentifiers { + a.equivalentToPreferred[equivalent.Identifier] = marker.PreferredIdentifier + } + } + + analyzer := &analysis.Analyzer{ + Name: name, + Doc: "Check that preferred markers are used instead of equivalent markers.", + Run: a.run, + Requires: []*analysis.Analyzer{inspector.Analyzer}, + } + + // Register all equivalent identifiers so they can be parsed. + // Note: The marker registry is thread-safe and idempotent, so it's safe + // to register the same marker multiple times or from concurrent goroutines. + for equivalent := range a.equivalentToPreferred { + markers.DefaultRegistry().Register(equivalent) + } + + return analyzer +} + +// run is the main analysis function that inspects all types and fields in the package. +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, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, a.equivalentToPreferred, qualifiedFieldName) + }) + + inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { + checkType(pass, typeSpec, markersAccess, a.equivalentToPreferred) + }) + + return nil, nil //nolint:nilnil +} + +// checkField validates a single struct field for marker usage. +// Only checks markers directly on the field, not inherited from type aliases, +// since inherited markers are already reported at the type level. +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, equivalentToPreferred map[string]string, qualifiedFieldName string) { + if field == nil || len(field.Names) == 0 { + return + } + + markerSet := markersAccess.FieldMarkers(field) + check(markerSet, equivalentToPreferred, func(marks []markers.Marker, preferredIdentifier string, preferredExists bool) { + reportMarkers(pass, marks, preferredIdentifier, qualifiedFieldName, field.Pos(), "field", preferredExists) + }) +} + +// checkType validates a single type definition for marker usage. +func checkType(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markers.Markers, equivalentToPreferred map[string]string) { + if typeSpec == nil { + return + } + + markerSet := markersAccess.TypeMarkers(typeSpec) + check(markerSet, equivalentToPreferred, func(marks []markers.Marker, preferredIdentifier string, preferredExists bool) { + reportMarkers(pass, marks, preferredIdentifier, typeSpec.Name.Name, typeSpec.Pos(), "type", preferredExists) + }) +} + +// check examines a set of markers for equivalent identifiers that should be replaced. +func check(markerSet markers.MarkerSet, equivalentToPreferred map[string]string, reportFunc func(markers []markers.Marker, preferredIdentifier string, preferredExists bool)) { + // Group markers by their preferred identifier to handle duplicates correctly + preferredToMarkers := make(map[string][]markers.Marker) + + for equivalentIdentifier, preferredIdentifier := range equivalentToPreferred { + marks := markerSet.Get(equivalentIdentifier) + if len(marks) > 0 { + preferredToMarkers[preferredIdentifier] = append(preferredToMarkers[preferredIdentifier], marks...) + } + } + + // Sort preferred identifiers for deterministic reporting + preferredIdentifiers := make([]string, 0, len(preferredToMarkers)) + for preferredIdentifier := range preferredToMarkers { + preferredIdentifiers = append(preferredIdentifiers, preferredIdentifier) + } + + sort.Strings(preferredIdentifiers) + + // Report each group of markers + for _, preferredIdentifier := range preferredIdentifiers { + marks := preferredToMarkers[preferredIdentifier] + // Check if the preferred marker already exists + preferredExists := len(markerSet.Get(preferredIdentifier)) > 0 + reportFunc(marks, preferredIdentifier, preferredExists) + } +} + +// formatMarkerList formats a list of markers as a sorted, quoted, comma-separated string. +// For example, [marker1, marker2] becomes `"marker1", "marker2"`. +func formatMarkerList(marks []markers.Marker) string { + names := make([]string, len(marks)) + for i, m := range marks { + names[i] = fmt.Sprintf("%q", m.Identifier) + } + + sort.Strings(names) + + return strings.Join(names, ", ") +} + +// buildTextEdits generates the text edits to fix equivalent markers. +// If preferredExists is true, all markers are deleted. Otherwise, the first +// marker is replaced with the preferred identifier and the rest are deleted. +func buildTextEdits(marks []markers.Marker, preferredIdentifier string, preferredExists bool) []analysis.TextEdit { + // Sort markers by position to ensure deterministic text edits + sort.Slice(marks, func(i, j int) bool { + return marks[i].Pos < marks[j].Pos + }) + + edits := make([]analysis.TextEdit, 0, len(marks)) + + // If the preferred marker doesn't exist, replace the first equivalent marker + if !preferredExists { + edits = append(edits, analysis.TextEdit{ + Pos: marks[0].Pos, + End: marks[0].End, + NewText: []byte(buildReplacementMarker(marks[0], preferredIdentifier)), + }) + marks = marks[1:] // Process remaining markers for deletion + } + + // Delete remaining markers to avoid duplicates + // Note: We add 1 to the end position to include the newline character, + // which removes the entire line and prevents blank lines in the output. + // This works correctly for most cases. At end of file without a trailing + // newline, the go/analysis framework handles the extra position gracefully. + for _, mark := range marks { + edits = append(edits, analysis.TextEdit{ + Pos: mark.Pos, + End: mark.End + 1, // +1 to include the newline character + NewText: []byte(""), + }) + } + + return edits +} + +// reportMarkers generates a diagnostic report for markers that should be +// replaced. This function handles the common logic for both field and type +// reporting. +func reportMarkers(pass *analysis.Pass, marks []markers.Marker, preferredIdentifier, elementName string, pos token.Pos, elementType string, preferredExists bool) { + if len(marks) == 0 { + return + } + + markerWord := "marker" + if len(marks) > 1 { + markerWord = "markers" + } + + message := fmt.Sprintf("%s %s uses %s %s, should use preferred marker %q instead", + elementType, elementName, markerWord, formatMarkerList(marks), preferredIdentifier) + + fixMessage := "remove equivalent markers" + if !preferredExists { + fixMessage = fmt.Sprintf("replace with %q", preferredIdentifier) + } + + pass.Report(analysis.Diagnostic{ + Pos: pos, + Message: message, + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fixMessage, + TextEdits: buildTextEdits(marks, preferredIdentifier, preferredExists), + }, + }, + }) +} + +// buildReplacementMarker constructs the replacement marker text with the +// preferred identifier while preserving the structure from the original marker. +// It handles different marker types (DeclarativeValidation vs Kubebuilder) and +// properly reconstructs arguments and payloads. +func buildReplacementMarker(marker markers.Marker, preferredIdentifier string) string { + // Determine the target marker type based on the preferred identifier + // DeclarativeValidation markers start with "k8s:", others are Kubebuilder style + targetType := markers.MarkerTypeKubebuilder + if strings.HasPrefix(preferredIdentifier, "k8s:") { + targetType = markers.MarkerTypeDeclarativeValidation + } + + switch targetType { + case markers.MarkerTypeDeclarativeValidation: + return buildDeclarativeValidationMarker(marker, preferredIdentifier) + case markers.MarkerTypeKubebuilder: + return buildKubebuilderMarker(marker, preferredIdentifier) + default: + // Fallback for unknown marker types (should not happen in practice) + return "// +" + preferredIdentifier + } +} + +// getSortedKeys returns sorted keys from a map for deterministic output. +func getSortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + + sort.Strings(keys) + + return keys +} + +// buildDeclarativeValidationMarker reconstructs a DeclarativeValidation marker. +// Format: // +identifier(argName: argValue, ...)={payload.Value || reconstruct(payload.Marker)}. +func buildDeclarativeValidationMarker(marker markers.Marker, preferredIdentifier string) string { + result := "// +" + preferredIdentifier + + // Add arguments if present + if len(marker.Arguments) > 0 { + parts := make([]string, 0, len(marker.Arguments)) + + for _, key := range getSortedKeys(marker.Arguments) { + if key == "" { + parts = append(parts, marker.Arguments[key]) + } else { + parts = append(parts, fmt.Sprintf("%s: %s", key, marker.Arguments[key])) + } + } + + result += "(" + strings.Join(parts, ", ") + ")" + } + + // Add payload if present + if marker.Payload.Value != "" { + result += "=" + marker.Payload.Value + } else if marker.Payload.Marker != nil { + // Nested marker - reconstruct without "// +" prefix + nested := buildReplacementMarker(*marker.Payload.Marker, marker.Payload.Marker.Identifier) + result += "=" + strings.TrimPrefix(nested, "// +") + } + + return result +} + +// buildKubebuilderMarker reconstructs a Kubebuilder marker. +// Format with arguments: // +identifier:argOne="valueOne",argTwo="valueTwo". +// Format without arguments: // +identifier={payload.Value}. +func buildKubebuilderMarker(marker markers.Marker, preferredIdentifier string) string { + result := "// +" + preferredIdentifier + + // Handle case with arguments + if len(marker.Arguments) > 0 { + parts := make([]string, 0, len(marker.Arguments)) + for _, key := range getSortedKeys(marker.Arguments) { + parts = append(parts, fmt.Sprintf("%s=%s", key, marker.Arguments[key])) + } + + return result + ":" + strings.Join(parts, ",") + } + + // Handle case without arguments but with payload + if marker.Payload.Value != "" { + result += "=" + marker.Payload.Value + } + + return result +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/config.go new file mode 100644 index 00000000000..44a092eca44 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/config.go @@ -0,0 +1,49 @@ +/* +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 preferredmarkers + +// Config is the configuration type +// for the preferredmarkers linter. +type Config struct { + // markers is the unique set of preferred markers + // and their equivalent identifiers. + // Uniqueness is keyed on the `preferredIdentifier` + // field of entries. + // Must have at least one entry. + Markers []Marker `json:"markers"` +} + +// Marker is a representation of a preferred marker +// and its equivalent identifiers that should be replaced. +type Marker struct { + // preferredIdentifier is the identifier for the preferred marker. + PreferredIdentifier string `json:"preferredIdentifier"` + + // equivalentIdentifiers is a unique set of marker identifiers + // that are equivalent to the preferred identifier. + // When any of these markers are found, they will be reported + // and a fix will be suggested to replace them with the + // preferred identifier. + // Must have at least one entry. + EquivalentIdentifiers []EquivalentIdentifier `json:"equivalentIdentifiers"` +} + +// EquivalentIdentifier represents a marker identifier that should be +// replaced with the preferred identifier. +type EquivalentIdentifier struct { + // identifier is the marker identifier to replace. + Identifier string `json:"identifier"` +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/doc.go new file mode 100644 index 00000000000..810115864e0 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/doc.go @@ -0,0 +1,96 @@ +/* +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. +*/ + +/* +* The `preferredmarkers` linter ensures that types and fields use preferred markers +* instead of equivalent but different marker identifiers. +* +* By default, `preferredmarkers` is not enabled. +* +* This linter is useful for projects that want to enforce consistent marker usage +* across their codebase, especially when multiple equivalent markers exist. +* For example, Kubernetes has multiple ways to mark fields as optional: +* - `+k8s:optional` +* - `+kubebuilder:validation:Optional` +* +* The linter can be configured to enforce using one preferred marker identifier +* and report any equivalent markers that should be replaced. +* +* **Configuration:** +* +* The linter requires a configuration that specifies preferred markers and their +* equivalent identifiers. +* +* **Scenario:** Enforce using `+k8s:optional` instead of `+kubebuilder:validation:Optional` +* +* ```yaml +* linterConfig: +* preferredmarkers: +* markers: +* - preferredIdentifier: "k8s:optional" +* equivalentIdentifiers: +* - "kubebuilder:validation:Optional" +* ``` +* +* **Scenario:** Enforce using a custom marker instead of multiple equivalent markers +* +* ```yaml +* linterConfig: +* preferredmarkers: +* markers: +* - preferredIdentifier: "custom:preferred" +* equivalentIdentifiers: +* - "custom:old:marker" +* - "custom:deprecated:marker" +* - "custom:legacy:marker" +* ``` +* +* **Scenario:** Multiple preferred markers with different equivalents +* +* ```yaml +* linterConfig: +* preferredmarkers: +* markers: +* - preferredIdentifier: "k8s:optional" +* equivalentIdentifiers: +* - "kubebuilder:validation:Optional" +* - preferredIdentifier: "k8s:required" +* equivalentIdentifiers: +* - "kubebuilder:validation:Required" +* ``` +* +* **Behavior:** +* +* When one or more equivalent markers are found, the linter will: +* 1. Report a diagnostic message indicating which marker(s) should be preferred +* 2. Suggest a fix that: +* - If the preferred marker does not already exist: replaces the first equivalent +* marker with the preferred identifier and preserves any marker expressions +* (e.g., `=value` or `:key=value`) +* - If the preferred marker already exists: removes all equivalent markers to +* avoid duplicates +* - Removes any additional equivalent markers +* +* For example, if both `+kubebuilder:validation:Optional` and `+custom:optional` +* are configured as equivalents to `+k8s:optional`, they will both be replaced +* with a single `+k8s:optional` marker. If `+k8s:optional` already exists alongside +* equivalent markers, only the equivalent markers will be removed. +* +* The linter checks both type-level and field-level markers, including markers +* inherited from type aliases. +* + */ +package preferredmarkers diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/initializer.go new file mode 100644 index 00000000000..e79cfd7360e --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers/initializer.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 preferredmarkers + +import ( + "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, + ) +} + +func initAnalyzer(cfg *Config) (*analysis.Analyzer, error) { + return newAnalyzer(cfg), nil +} + +// validateConfig implements validation of the preferredmarkers linter config. +func validateConfig(cfg *Config, fldPath *field.Path) field.ErrorList { + if cfg == nil { + return field.ErrorList{field.Required(fldPath, "configuration is required for the preferredmarkers linter when it is enabled")} + } + + return validateMarkers(fldPath.Child("markers"), cfg.Markers...) +} + +// validateEquivalentIdentifiers validates a single marker's equivalent identifiers. +func validateEquivalentIdentifiers( + equivalents []EquivalentIdentifier, + fldPath *field.Path, + knownPreferredMarkers, + knownEquivalentMarkers sets.Set[string], +) field.ErrorList { + if len(equivalents) == 0 { + return field.ErrorList{field.Required(fldPath, "must contain at least one equivalent identifier")} + } + + var ( + errs field.ErrorList + localEquivalents = sets.New[string]() + ) + + for i, equivalent := range equivalents { + equivalentPath := fldPath.Index(i) + identifier := equivalent.Identifier + + // Check for duplicates within this marker's equivalent identifiers + if localEquivalents.Has(identifier) { + errs = append(errs, field.Duplicate(equivalentPath.Child("identifier"), identifier)) + continue + } + + localEquivalents.Insert(identifier) + + // Check if this equivalent identifier is already used as a preferred identifier + if knownPreferredMarkers.Has(identifier) { + errs = append(errs, field.Invalid(equivalentPath.Child("identifier"), identifier, "equivalent identifier cannot be the same as a preferred identifier")) + continue + } + + // Check if this equivalent identifier was already used in another marker's equivalent list + if knownEquivalentMarkers.Has(identifier) { + errs = append(errs, field.Duplicate(equivalentPath.Child("identifier"), identifier)) + continue + } + + knownEquivalentMarkers.Insert(identifier) + } + + return errs +} + +func validateMarkers(fldPath *field.Path, markers ...Marker) field.ErrorList { + if len(markers) == 0 { + return field.ErrorList{field.Required(fldPath, "must contain at least one preferred marker")} + } + + var ( + errs field.ErrorList + knownPreferredMarkers = sets.New[string]() + knownEquivalentMarkers = sets.New[string]() + ) + + for i, marker := range markers { + indexPath := fldPath.Index(i) + + // Check for duplicate preferred identifiers + if knownPreferredMarkers.Has(marker.PreferredIdentifier) { + errs = append(errs, field.Duplicate(indexPath.Child("preferredIdentifier"), marker.PreferredIdentifier)) + continue // Skip equivalent validation for duplicate preferred identifiers + } + + knownPreferredMarkers.Insert(marker.PreferredIdentifier) + + // Validate equivalent identifiers + errs = append(errs, validateEquivalentIdentifiers( + marker.EquivalentIdentifiers, + indexPath.Child("equivalentIdentifiers"), + knownPreferredMarkers, + knownEquivalentMarkers, + )...) + } + + return errs +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields/analyzer.go index d3ffc6491b6..3e4927ad32c 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields/analyzer.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" "sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization" "sigs.k8s.io/kube-api-linter/pkg/markers" ) @@ -92,20 +93,19 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) { - a.checkField(pass, field, markersAccess, jsonTagInfo) + inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + a.checkField(pass, field, markersAccess, jsonTagInfo, qualifiedFieldName) }) return nil, nil //nolint:nilnil } -func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo) { +func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo, qualifiedFieldName string) { if field == nil || len(field.Names) == 0 { return } - fieldMarkers := markersAccess.FieldMarkers(field) - if !isFieldRequired(fieldMarkers) { + if !utils.IsFieldRequired(field, markersAccess) { // The field is not marked required, so we don't need to check it. return } @@ -115,7 +115,7 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce return } - a.serializationCheck.Check(pass, field, markersAccess, jsonTags) + a.serializationCheck.Check(pass, field, markersAccess, jsonTags, qualifiedFieldName) } func defaultConfig(cfg *RequiredFieldsConfig) { @@ -131,8 +131,3 @@ func defaultConfig(cfg *RequiredFieldsConfig) { cfg.OmitZero.Policy = RequiredFieldsOmitZeroPolicySuggestFix } } - -// isFieldRequired checks if a field has an required marker. -func isFieldRequired(fieldMarkers markershelper.MarkerSet) bool { - return fieldMarkers.Has(markers.RequiredMarker) || fieldMarkers.Has(markers.KubebuilderRequiredMarker) -} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields/config.go index a5872c57465..a8fee9bc184 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields/config.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields/config.go @@ -65,7 +65,7 @@ type RequiredFieldsOmitZero struct { // Valid values are "SuggestFix", "Warn" and "Forbid". // When set to "SuggestFix", the linter will suggest adding the `omitzero` tag when an required field does not have it. // When set to "Warn", the linter will emit a warning if the field does not have the `omitzero` tag. - // When set to "Forbid", 'omitzero' tags wont be considered. + // When set to "Forbid", 'omitzero' tags will not be considered. // Note, when set to "Forbid", and a field have the `omitzero` tag, the linter will suggest to remove the `omitzero` tag. // Note, `omitzero` tag is supported in go version starting from go 1.24. // Note, Configure omitzero policy to 'Forbid', if using with go version less than go 1.24. diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags/analyzer.go index 4f159fe3409..0ce0f1d8dcd 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags/analyzer.go @@ -66,14 +66,14 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - a.checkField(pass, field, markersAccess) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) { + a.checkField(pass, field, markersAccess, qualifiedFieldName) }) return nil, nil //nolint:nilnil } -func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers) { +func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, qualifiedFieldName string) { if !utils.IsArrayTypeOrAlias(pass, field) { return } @@ -89,10 +89,10 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce for _, marker := range listTypeMarkers { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("%s is a byte array, which does not support the listType marker. Remove the listType marker", utils.FieldName(field)), + Message: fmt.Sprintf("%s is a byte array, which does not support the listType marker. Remove the listType marker", qualifiedFieldName), SuggestedFixes: []analysis.SuggestedFix{ { - Message: fmt.Sprintf("Remove listType marker from %s", utils.FieldName(field)), + Message: fmt.Sprintf("Remove listType marker from %s", qualifiedFieldName), TextEdits: []analysis.TextEdit{ { Pos: marker.Pos, @@ -108,56 +108,52 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce return } - fieldName := utils.FieldName(field) listTypeMarkers := fieldMarkers.Get(kubebuildermarkers.KubebuilderListTypeMarker) if len(listTypeMarkers) == 0 { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("%s should have a listType marker for proper Server-Side Apply behavior (atomic, set, or map)", fieldName), + Message: fmt.Sprintf("%s should have a listType marker for proper Server-Side Apply behavior (atomic, set, or map)", qualifiedFieldName), }) return } for _, marker := range listTypeMarkers { - listType := marker.Expressions[""] + listType := marker.Payload.Value - a.checkListTypeMarker(pass, listType, field) + a.checkListTypeMarker(pass, listType, field, qualifiedFieldName) if listType == listTypeMap { - a.checkListTypeMap(pass, fieldMarkers, field) + a.checkListTypeMap(pass, fieldMarkers, field, qualifiedFieldName) } if listType == listTypeSet { - a.checkListTypeSet(pass, field) + a.checkListTypeSet(pass, field, qualifiedFieldName) } } } -func (a *analyzer) checkListTypeMarker(pass *analysis.Pass, listType string, field *ast.Field) { - fieldName := utils.FieldName(field) - +func (a *analyzer) checkListTypeMarker(pass *analysis.Pass, listType string, field *ast.Field, qualifiedFieldName string) { if !validListType(listType) { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("%s has invalid listType %q, must be one of: atomic, set, map", fieldName, listType), + Message: fmt.Sprintf("%s has invalid listType %q, must be one of: atomic, set, map", qualifiedFieldName, listType), }) return } } -func (a *analyzer) checkListTypeMap(pass *analysis.Pass, fieldMarkers markers.MarkerSet, field *ast.Field) { +func (a *analyzer) checkListTypeMap(pass *analysis.Pass, fieldMarkers markers.MarkerSet, field *ast.Field, qualifiedFieldName string) { listMapKeyMarkers := fieldMarkers.Get(kubebuildermarkers.KubebuilderListMapKeyMarker) - fieldName := utils.FieldName(field) isObjectList := utils.IsObjectList(pass, field) if !isObjectList { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("%s with listType=map can only be used for object lists, not primitive lists", fieldName), + Message: fmt.Sprintf("%s with listType=map can only be used for object lists, not primitive lists", qualifiedFieldName), }) return @@ -166,16 +162,16 @@ func (a *analyzer) checkListTypeMap(pass *analysis.Pass, fieldMarkers markers.Ma if len(listMapKeyMarkers) == 0 { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("%s with listType=map must have at least one listMapKey marker", fieldName), + Message: fmt.Sprintf("%s with listType=map must have at least one listMapKey marker", qualifiedFieldName), }) return } - a.validateListMapKeys(pass, field, listMapKeyMarkers) + a.validateListMapKeys(pass, field, listMapKeyMarkers, qualifiedFieldName) } -func (a *analyzer) checkListTypeSet(pass *analysis.Pass, field *ast.Field) { +func (a *analyzer) checkListTypeSet(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string) { if a.listTypeSetUsage == SSATagsListTypeSetUsageIgnore { return } @@ -185,16 +181,15 @@ func (a *analyzer) checkListTypeSet(pass *analysis.Pass, field *ast.Field) { return } - fieldName := utils.FieldName(field) diagnostic := analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("%s with listType=set is not recommended due to Server-Side Apply compatibility issues. Consider using listType=%s or listType=%s instead", fieldName, listTypeAtomic, listTypeMap), + Message: fmt.Sprintf("%s with listType=set is not recommended due to Server-Side Apply compatibility issues. Consider using listType=%s or listType=%s instead", qualifiedFieldName, listTypeAtomic, listTypeMap), } pass.Report(diagnostic) } -func (a *analyzer) validateListMapKeys(pass *analysis.Pass, field *ast.Field, listMapKeyMarkers []markers.Marker) { +func (a *analyzer) validateListMapKeys(pass *analysis.Pass, field *ast.Field, listMapKeyMarkers []markers.Marker, qualifiedFieldName string) { jsonTags, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags) if !ok { return @@ -205,10 +200,8 @@ func (a *analyzer) validateListMapKeys(pass *analysis.Pass, field *ast.Field, li return } - fieldName := utils.FieldName(field) - for _, marker := range listMapKeyMarkers { - keyName := marker.Expressions[""] + keyName := marker.Payload.Value if keyName == "" { continue } @@ -216,7 +209,7 @@ func (a *analyzer) validateListMapKeys(pass *analysis.Pass, field *ast.Field, li if !a.hasFieldWithJSONTag(structFields, jsonTags, keyName) { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("%s listMapKey %q does not exist as a field in the struct", fieldName, keyName), + Message: fmt.Sprintf("%s listMapKey %q does not exist as a field in the struct", qualifiedFieldName, keyName), }) } } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags/initializer.go index 7f65ec4c137..4859e09df57 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags/initializer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags/initializer.go @@ -41,7 +41,7 @@ func Initializer() initializer.AnalyzerInitializer { ) } -// Init returns the intialized Analyzer. +// Init returns the initialized Analyzer. func initAnalyzer(stc *SSATagsConfig) (*analysis.Analyzer, error) { return newAnalyzer(stc), nil } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional/analyzer.go index 1abbaa9dd1c..34cfffac338 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional/analyzer.go @@ -79,7 +79,7 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetJSONTags } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) { + inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, _ string) { if jsonTagInfo.Name != statusJSONTag { return } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional/initializer.go index d7dd41f2413..9b12b3a8ce7 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional/initializer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional/initializer.go @@ -35,7 +35,7 @@ func Initializer() initializer.AnalyzerInitializer { return initializer.NewConfigurableInitializer( name, initAnalyzer, - true, + false, validateConfig, ) } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statussubresource/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statussubresource/analyzer.go index a669df078a4..096e4efaf76 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statussubresource/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/statussubresource/analyzer.go @@ -26,6 +26,7 @@ import ( kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" "sigs.k8s.io/kube-api-linter/pkg/markers" ) @@ -97,40 +98,47 @@ func checkStruct(pass *analysis.Pass, sTyp *ast.StructType, name string, structM return } + // Skip Kubernetes List types as they follow a different pattern + // and don't use the status subresource + if utils.IsKubernetesListType(sTyp, name) { + return + } + hasStatusSubresourceMarker := structMarkers.Has(markers.KubebuilderStatusSubresourceMarker) hasStatusField := hasStatusField(sTyp, jsonTags) - switch { - case (hasStatusSubresourceMarker && hasStatusField), (!hasStatusSubresourceMarker && !hasStatusField): - // acceptable state - case hasStatusSubresourceMarker && !hasStatusField: - // Might be able to have some suggested fixes here, but it is likely much more complex - // so for now leave it with a descriptive failure message. + // Both present or both absent is acceptable + if hasStatusSubresourceMarker == hasStatusField { + return + } + + // Marker present but no status field + if hasStatusSubresourceMarker { pass.Reportf(sTyp.Pos(), "root object type %q is marked to enable the status subresource with marker %q but has no status field", name, markers.KubebuilderStatusSubresourceMarker) - case !hasStatusSubresourceMarker && hasStatusField: - // In this case we can suggest the autofix to add the status subresource marker - pass.Report(analysis.Diagnostic{ - Pos: sTyp.Pos(), - Message: fmt.Sprintf("root object type %q has a status field but does not have the marker %q to enable the status subresource", name, markers.KubebuilderStatusSubresourceMarker), - SuggestedFixes: []analysis.SuggestedFix{ - { - Message: "should add the kubebuilder:subresource:status marker", - TextEdits: []analysis.TextEdit{ - // go one line above the struct and add the marker - { - // sTyp.Pos() is the beginning of the 'struct' keyword. Subtract - // the length of the struct name + 7 (2 for spaces surrounding type name, 4 for the 'type' keyword, - // and 1 for the newline) to position at the end of the line above the struct - // definition. - Pos: sTyp.Pos() - token.Pos(len(name)+7), - // prefix with a newline to ensure we aren't appending to a previous comment - NewText: []byte("\n// +kubebuilder:subresource:status"), - }, + return + } + + // Status field present but no marker - suggest autofix + pass.Report(analysis.Diagnostic{ + Pos: sTyp.Pos(), + Message: fmt.Sprintf("root object type %q has a status field but does not have the marker %q to enable the status subresource", name, markers.KubebuilderStatusSubresourceMarker), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "should add the kubebuilder:subresource:status marker", + TextEdits: []analysis.TextEdit{ + { + // sTyp.Pos() is the beginning of the 'struct' keyword. Subtract + // the length of the struct name + 7 (2 for spaces surrounding type name, 4 for the 'type' keyword, + // and 1 for the newline) to position at the end of the line above the struct + // definition. + Pos: sTyp.Pos() - token.Pos(len(name)+7), + // prefix with a newline to ensure we aren't appending to a previous comment + NewText: []byte("\n// +kubebuilder:subresource:status"), }, }, }, - }) - } + }, + }) } func hasStatusField(sTyp *ast.StructType, jsonTags extractjsontags.StructFieldTags) bool { diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/uniquemarkers/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/uniquemarkers/analyzer.go index 07363ad3e00..7a38b6418bb 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/uniquemarkers/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/uniquemarkers/analyzer.go @@ -65,8 +65,8 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, kalerrors.ErrCouldNotGetInspector } - inspect.InspectFields(func(field *ast.Field, stack []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) { - checkField(pass, field, markersAccess, a.uniqueMarkers) + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, a.uniqueMarkers, qualifiedFieldName) }) inspect.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markers.Markers) { @@ -76,13 +76,13 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, nil //nolint:nilnil } -func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, uniqueMarkers []UniqueMarker) { +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, uniqueMarkers []UniqueMarker, qualifiedFieldName string) { if field == nil || len(field.Names) == 0 { return } markers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field) - check(markers, uniqueMarkers, reportField(pass, field)) + check(markers, uniqueMarkers, reportField(pass, field, qualifiedFieldName)) } func checkType(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markers.Markers, uniqueMarkers []UniqueMarker) { @@ -121,24 +121,49 @@ func constructIdentifier(marker markers.Marker, attributes ...string) string { return marker.Identifier } - // If a marker doesn't specify the attribute, we should assume it is equivalent - // to the empty string ("") so that we can still key on uniqueness of other attributes - // effectively. - id := fmt.Sprintf("%s:", marker.Identifier) - for _, attr := range attributes { - id += fmt.Sprintf("%s=%s,", attr, marker.Expressions[attr]) - } + switch marker.Type { + case markers.MarkerTypeDeclarativeValidation: + // If a marker doesn't specify the attribute, we should assume it is equivalent + // to the empty string ("") so that we can still key on uniqueness of other attributes + // effectively. + id := fmt.Sprintf("%s(", marker.Identifier) - id = strings.TrimSuffix(id, ",") + for _, attr := range attributes { + if attr == "" { + id += marker.Arguments[attr] + continue + } + + id += fmt.Sprintf("%s: %s,", attr, marker.Arguments[attr]) + } - return id + id = strings.TrimSuffix(id, ",") + id += ")" + + return id + case markers.MarkerTypeKubebuilder: + // If a marker doesn't specify the attribute, we should assume it is equivalent + // to the empty string ("") so that we can still key on uniqueness of other attributes + // effectively. + id := fmt.Sprintf("%s:", marker.Identifier) + for _, attr := range attributes { + id += fmt.Sprintf("%s=%s,", attr, marker.Arguments[attr]) + } + + id = strings.TrimSuffix(id, ",") + + return id + default: + // programmer error + panic(fmt.Sprintf("unknown marker format %s", marker.Type)) + } } -func reportField(pass *analysis.Pass, field *ast.Field) func(id string) { +func reportField(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string) func(id string) { return func(id string) { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("field %s has multiple definitions of marker %s when only a single definition should exist", field.Names[0].Name, id), + Message: fmt.Sprintf("field %s has multiple definitions of marker %s when only a single definition should exist", qualifiedFieldName, id), }) } } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/uniquemarkers/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/uniquemarkers/initializer.go index 571a3476f5b..50566fb01fd 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/uniquemarkers/initializer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/uniquemarkers/initializer.go @@ -38,7 +38,7 @@ func Initializer() initializer.AnalyzerInitializer { ) } -// Init returns the intialized Analyzer. +// Init returns the initialized Analyzer. func initAnalyzer(umc *UniqueMarkersConfig) (*analysis.Analyzer, error) { return newAnalyzer(umc), nil } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/config.go index 82ce747050f..294dc43ddbd 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/config.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/config.go @@ -45,7 +45,7 @@ const ( // OmitEmptyPolicy is the policy for omitempty. // SuggestFix will suggest a fix for the field to add omitempty. // Warn will warn about the field to add omitempty. -// Ignore will ignore the the absence of omitempty. +// Ignore will ignore the absence of omitempty. type OmitEmptyPolicy string const ( diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/serialization_check.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/serialization_check.go index d22880ad8ee..3e0071d0709 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/serialization_check.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/serialization_check.go @@ -23,11 +23,12 @@ import ( "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" + "sigs.k8s.io/kube-api-linter/pkg/markers" ) // SerializationCheck is an interface for checking serialization of fields. type SerializationCheck interface { - Check(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo) + Check(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo, qualifiedFieldName string) } // New creates a new SerializationCheck with the given configuration. @@ -86,57 +87,70 @@ type serializationCheck struct { // Check checks the serialization of the field. // It will check if the zero value of the field is valid, and whether the field should be a pointer or not. -func (s *serializationCheck) Check(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo) { +func (s *serializationCheck) Check(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo, qualifiedFieldName string) { fieldName := utils.FieldName(field) - hasValidZeroValue, completeValidation := utils.IsZeroValueValid(pass, field, field.Type, markersAccess, s.omitZeroPolicy != OmitZeroPolicyForbid) + hasValidZeroValue, completeValidation := utils.IsZeroValueValid(pass, field, field.Type, markersAccess, s.omitZeroPolicy != OmitZeroPolicyForbid, qualifiedFieldName) hasOmitEmpty := jsonTags.OmitEmpty hasOmitZero := jsonTags.OmitZero isPointer, underlying := utils.IsStarExpr(field.Type) isStruct := utils.IsStructType(pass, field.Type) + // Check if this struct should be treated as a non-struct type (e.g., Type=string marker). + // This handles structs with custom marshalling that serialize as other types. + if isStruct { + typeValue := utils.GetTypeMarkerValue(pass, field, markersAccess) + // If the type marker indicates this is not a struct, treat it accordingly. + // Type "object" means it's still a struct/object type in the OpenAPI sense. + // Other types (string, number, integer, boolean, array) indicate custom marshalling + // that changes the serialization format from a struct to that type. + if typeValue != "" && typeValue != "object" { + isStruct = false + } + } + switch s.pointerPreference { case PointersPreferenceAlways: // The field must always be a pointer, pointers require omitempty, so enforce that too. - s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, "should be a pointer.") - s.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags) + s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, "should be a pointer.", qualifiedFieldName) + s.handleFieldShouldHaveOmitEmpty(pass, field, qualifiedFieldName, hasOmitEmpty, jsonTags) case PointersPreferenceWhenRequired: - s.handleFieldOmitZero(pass, field, fieldName, jsonTags, hasOmitZero, hasValidZeroValue, isPointer, isStruct) + s.handleFieldOmitZero(pass, field, fieldName, jsonTags, underlying, hasOmitZero, hasValidZeroValue, isPointer, isStruct, markersAccess, qualifiedFieldName) if s.omitEmptyPolicy != OmitEmptyPolicyIgnore || hasOmitEmpty { // If we require omitempty, or the field has omitempty, we can check the field properties based on it being an omitempty field. - s.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct) + s.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct, markersAccess, qualifiedFieldName) } else { // The field does not have omitempty, and does not require it. - s.checkFieldPropertiesWithoutOmitEmpty(pass, field, fieldName, jsonTags, underlying, hasValidZeroValue, completeValidation, isPointer, isStruct) + s.checkFieldPropertiesWithoutOmitEmpty(pass, field, fieldName, jsonTags, underlying, hasValidZeroValue, completeValidation, isPointer, isStruct, markersAccess, qualifiedFieldName) } default: panic(fmt.Sprintf("unknown pointer preference: %s", s.pointerPreference)) } } -func (s *serializationCheck) handleFieldOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, hasOmitZero, hasValidZeroValue, isPointer, isStruct bool) { +func (s *serializationCheck) handleFieldOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitZero, hasValidZeroValue, isPointer, isStruct bool, markersAccess markershelper.Markers, qualifiedFieldName string) { switch s.omitZeroPolicy { case OmitZeroPolicyForbid: // when the omitzero policy is set to forbid, we need to report removing omitzero if set on the struct fields. - s.checkFieldPropertiesWithOmitZeroForbidPolicy(pass, field, fieldName, isStruct, hasOmitZero, jsonTags) + s.checkFieldPropertiesWithOmitZeroForbidPolicy(pass, field, qualifiedFieldName, isStruct, hasOmitZero, jsonTags) case OmitZeroPolicyWarn, OmitZeroPolicySuggestFix: // If we require omitzero, or the field has omitzero, we can check the field properties based on it being an omitzero field. - s.checkFieldPropertiesWithOmitZeroRequired(pass, field, fieldName, jsonTags, hasOmitZero, isPointer, isStruct, hasValidZeroValue) + s.checkFieldPropertiesWithOmitZeroRequired(pass, field, fieldName, jsonTags, underlying, hasOmitZero, isPointer, isStruct, hasValidZeroValue, markersAccess, qualifiedFieldName) default: panic(fmt.Sprintf("unknown omit zero policy: %s", s.omitZeroPolicy)) } } -func (s *serializationCheck) handleFieldShouldHaveOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, hasOmitEmpty bool, jsonTags extractjsontags.FieldTagInfo) { +func (s *serializationCheck) handleFieldShouldHaveOmitEmpty(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string, hasOmitEmpty bool, jsonTags extractjsontags.FieldTagInfo) { if hasOmitEmpty { return } - reportShouldAddOmitEmpty(pass, field, s.omitEmptyPolicy, fieldName, "field %s should have the omitempty tag.", jsonTags) + reportShouldAddOmitEmpty(pass, field, s.omitEmptyPolicy, qualifiedFieldName, "field %s should have the omitempty tag.", jsonTags) } -func (s *serializationCheck) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct bool) { +func (s *serializationCheck) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct bool, markersAccess markershelper.Markers, qualifiedFieldName string) { switch { case isStruct && !hasValidZeroValue && s.omitZeroPolicy != OmitZeroPolicyForbid: // The struct field need not be pointer if it does not have a valid zero value. @@ -145,75 +159,76 @@ func (s *serializationCheck) checkFieldPropertiesWithOmitEmptyRequired(pass *ana zeroValue := utils.GetTypedZeroValue(pass, underlying) validationHint := utils.GetTypedValidationHint(pass, underlying) - s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, fmt.Sprintf("has a valid zero value (%s), but the validation is not complete (e.g. %s). The field should be a pointer to allow the zero value to be set. If the zero value is not a valid use case, complete the validation and remove the pointer.", zeroValue, validationHint)) + s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, fmt.Sprintf("has a valid zero value (%s), but the validation is not complete (e.g. %s). The field should be a pointer to allow the zero value to be set. If the zero value is not a valid use case, complete the validation and remove the pointer.", zeroValue, validationHint), qualifiedFieldName) case hasValidZeroValue, isStruct: // The field validation infers that the zero value is valid, the field needs to be a pointer. // Structs with omitempty should always be pointers, else they won't actually be omitted. zeroValue := utils.GetTypedZeroValue(pass, underlying) - s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, fmt.Sprintf("has a valid zero value (%s) and should be a pointer.", zeroValue)) + s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, fmt.Sprintf("has a valid zero value (%s) and should be a pointer.", zeroValue), qualifiedFieldName) case !hasValidZeroValue && completeValidation && !isStruct: // The validation is fully complete, and the zero value is not valid, so we don't need a pointer. - s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s does not allow the zero value. The field does not need to be a pointer.") + s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, "field %s does not allow the zero value. The field does not need to be a pointer.", qualifiedFieldName) } // In this case, we should always add the omitempty if it isn't present. - s.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags) + s.handleFieldShouldHaveOmitEmpty(pass, field, qualifiedFieldName, hasOmitEmpty, jsonTags) } -func (s *serializationCheck) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasValidZeroValue, completeValidation, isPointer, isStruct bool) { +func (s *serializationCheck) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasValidZeroValue, completeValidation, isPointer, isStruct bool, markersAccess markershelper.Markers, qualifiedFieldName string) { switch { case hasValidZeroValue: // The field is not omitempty, and the zero value is valid, the field does not need to be a pointer. - s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s does not have omitempty and allows the zero value. The field does not need to be a pointer.") + s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, "field %s does not have omitempty and allows the zero value. The field does not need to be a pointer.", qualifiedFieldName) case !hasValidZeroValue: - // The zero value would not be accepted, so the field needs to have omitempty. - // Force the omitempty policy to suggest a fix. We can only get to this function when the policy is configured to Ignore. - // Since we absolutely have to add the omitempty tag, we can report it as a suggestion. - reportShouldAddOmitEmpty(pass, field, OmitEmptyPolicySuggestFix, fieldName, "field %s does not allow the zero value. It must have the omitempty tag.", jsonTags) + if s.omitZeroPolicy == OmitZeroPolicyForbid || !isStruct { + // The zero value would not be accepted, so the field needs to have omitempty. + // Force the omitempty policy to suggest a fix. We can only get to this function when the policy is configured to Ignore. + // Since we absolutely have to add the omitempty tag, we can report it as a suggestion. + // If we are checking omitzero separately, and it's a struct, this wouldn't apply so we skip. + reportShouldAddOmitEmpty(pass, field, OmitEmptyPolicySuggestFix, qualifiedFieldName, "field %s does not allow the zero value. It must have the omitempty tag.", jsonTags) + } + // Once it has the omitempty tag, it will also need to be a pointer in some cases. // Now handle it as if it had the omitempty already. // We already handle the omitempty tag above, so force the `hasOmitEmpty` to true. - s.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, true, hasValidZeroValue, completeValidation, isPointer, isStruct) + s.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, true, hasValidZeroValue, completeValidation, isPointer, isStruct, markersAccess, qualifiedFieldName) } } -func (s *serializationCheck) checkFieldPropertiesWithOmitZeroRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, hasOmitZero, isPointer, isStruct, hasValidZeroValue bool) { +func (s *serializationCheck) checkFieldPropertiesWithOmitZeroRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitZero, isPointer, isStruct, hasValidZeroValue bool, markersAccess markershelper.Markers, qualifiedFieldName string) { if !isStruct || hasValidZeroValue { return } - s.handleFieldShouldHaveOmitZero(pass, field, fieldName, hasOmitZero, jsonTags) - s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s does not allow the zero value. The field does not need to be a pointer.") + s.handleFieldShouldHaveOmitZero(pass, field, qualifiedFieldName, hasOmitZero, jsonTags) + s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, "field %s does not allow the zero value. The field does not need to be a pointer.", qualifiedFieldName) } -func (s *serializationCheck) checkFieldPropertiesWithOmitZeroForbidPolicy(pass *analysis.Pass, field *ast.Field, fieldName string, isStruct, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) { +func (s *serializationCheck) checkFieldPropertiesWithOmitZeroForbidPolicy(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string, isStruct, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) { if !isStruct || !hasOmitZero { // Handle omitzero only for struct field having omitZero tag. return } - reportShouldRemoveOmitZero(pass, field, fieldName, jsonTags) + reportShouldRemoveOmitZero(pass, field, qualifiedFieldName, jsonTags) } -func (s *serializationCheck) handleFieldShouldHaveOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) { +func (s *serializationCheck) handleFieldShouldHaveOmitZero(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) { if hasOmitZero { return } // Currently, add omitzero tags to only struct fields. - reportShouldAddOmitZero(pass, field, s.omitZeroPolicy, fieldName, "field %s does not allow the zero value. It must have the omitzero tag.", jsonTags) + reportShouldAddOmitZero(pass, field, s.omitZeroPolicy, qualifiedFieldName, "field %s does not allow the zero value. It must have the omitzero tag.", jsonTags) } -func (s *serializationCheck) handleFieldShouldBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr, reason string) { +func (s *serializationCheck) handleFieldShouldBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr, markersAccess markershelper.Markers, reason, qualifiedFieldName string) { if utils.IsPointerType(pass, underlying) { if isPointer { - switch s.pointerPolicy { - case PointersPolicySuggestFix: - reportShouldRemovePointer(pass, field, PointersPolicySuggestFix, fieldName, "field %s underlying type does not need to be a pointer. The pointer should be removed.", fieldName) - case PointersPolicyWarn: - pass.Reportf(field.Pos(), "field %s underlying type does not need to be a pointer. The pointer should be removed.", fieldName) - } + s.handlePointerToPointerType(pass, field, fieldName, underlying, markersAccess, qualifiedFieldName) + } else if s.pointerPreference == PointersPreferenceAlways { + s.handleNonPointerToPointerType(pass, field, fieldName, underlying, markersAccess, qualifiedFieldName) } return @@ -223,18 +238,75 @@ func (s *serializationCheck) handleFieldShouldBePointer(pass *analysis.Pass, fie return } + s.reportShouldAddPointerMessage(pass, field, fieldName, reason, qualifiedFieldName) +} + +func (s *serializationCheck) handlePointerToPointerType(pass *analysis.Pass, field *ast.Field, fieldName string, underlying ast.Expr, markersAccess markershelper.Markers, qualifiedFieldName string) { + // Check if this is a pointer-to-slice/map with explicit MinItems=0 or MinProperties=0 + // In this case, the pointer is intentional to distinguish nil from empty + if hasExplicitZeroMinValidation(pass, field, underlying, markersAccess) { + return + } + + switch s.pointerPolicy { + case PointersPolicySuggestFix: + reportShouldRemovePointer(pass, field, PointersPolicySuggestFix, fieldName, "field %s underlying type does not need to be a pointer. The pointer should be removed.", qualifiedFieldName) + case PointersPolicyWarn: + pass.Reportf(field.Pos(), "field %s underlying type does not need to be a pointer. The pointer should be removed.", qualifiedFieldName) + } +} + +func (s *serializationCheck) handleNonPointerToPointerType(pass *analysis.Pass, field *ast.Field, fieldName string, underlying ast.Expr, markersAccess markershelper.Markers, qualifiedFieldName string) { + // Check if this is a slice/map WITHOUT a pointer but with explicit MinItems=0 or MinProperties=0 + // In this case, we should suggest adding a pointer to distinguish nil from empty + if !hasExplicitZeroMinValidation(pass, field, underlying, markersAccess) { + return + } + + s.reportShouldAddPointerMessage(pass, field, fieldName, "with MinItems=0/MinProperties=0, underlying type should be a pointer to distinguish nil (unset) from empty.", qualifiedFieldName) +} + +func (s *serializationCheck) reportShouldAddPointerMessage(pass *analysis.Pass, field *ast.Field, fieldName, reason, qualifiedFieldName string) { switch s.pointerPolicy { case PointersPolicySuggestFix: - reportShouldAddPointer(pass, field, PointersPolicySuggestFix, fieldName, "field %s %s", fieldName, reason) + reportShouldAddPointer(pass, field, PointersPolicySuggestFix, fieldName, "field %s %s", qualifiedFieldName, reason) case PointersPolicyWarn: - pass.Reportf(field.Pos(), "field %s %s", fieldName, reason) + pass.Reportf(field.Pos(), "field %s %s", qualifiedFieldName, reason) } } -func (s *serializationCheck) handleFieldShouldNotBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, message string) { +func (s *serializationCheck) handleFieldShouldNotBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr, markersAccess markershelper.Markers, message, qualifiedFieldName string) { if !isPointer { return } - reportShouldRemovePointer(pass, field, s.pointerPolicy, fieldName, message, fieldName) + // Check if this is a pointer-to-slice/map with explicit MinItems=0 or MinProperties=0 + // In this case, the pointer is intentional to distinguish nil from empty + if hasExplicitZeroMinValidation(pass, field, underlying, markersAccess) { + return + } + + reportShouldRemovePointer(pass, field, s.pointerPolicy, fieldName, message, qualifiedFieldName) +} + +// hasExplicitZeroMinValidation checks if a field has an explicit MinItems=0 or MinProperties=0 marker. +// This indicates the developer intentionally wants to distinguish between nil and empty for slices/maps: +// - nil: field not provided by the user, use defaults or treat as unset +// - []/{}: explicitly set to empty by the user +// +// Using a pointer allows preserving this semantic difference, which is why MinItems=0/MinProperties=0 +// combined with a pointer is a valid pattern despite slices/maps being reference types. +func hasExplicitZeroMinValidation(pass *analysis.Pass, field *ast.Field, underlying ast.Expr, markersAccess markershelper.Markers) bool { + fieldMarkers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field) + + switch underlying.(type) { + case *ast.ArrayType: + // Check for explicit MinItems=0 + return fieldMarkers.HasWithValue(markers.KubebuilderMinItemsMarker + "=0") + case *ast.MapType: + // Check for explicit MinProperties=0 + return fieldMarkers.HasWithValue(markers.KubebuilderMinPropertiesMarker + "=0") + } + + return false } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/util.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/util.go index 3695f6ce925..8b101a2a020 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/util.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization/util.go @@ -80,15 +80,15 @@ func reportShouldRemovePointer(pass *analysis.Pass, field *ast.Field, pointerPol } // reportShouldAddOmitEmpty adds an analysis diagnostic that explains that an omitempty tag should be added. -func reportShouldAddOmitEmpty(pass *analysis.Pass, field *ast.Field, omitEmptyPolicy OmitEmptyPolicy, fieldName, messageFmt string, fieldTagInfo extractjsontags.FieldTagInfo) { +func reportShouldAddOmitEmpty(pass *analysis.Pass, field *ast.Field, omitEmptyPolicy OmitEmptyPolicy, qualifiedFieldName, messageFmt string, fieldTagInfo extractjsontags.FieldTagInfo) { switch omitEmptyPolicy { case OmitEmptyPolicySuggestFix: pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf(messageFmt, fieldName), + Message: fmt.Sprintf(messageFmt, qualifiedFieldName), SuggestedFixes: []analysis.SuggestedFix{ { - Message: fmt.Sprintf("should add 'omitempty' to the field tag for field %s", fieldName), + Message: fmt.Sprintf("should add 'omitempty' to the field tag for field %s", qualifiedFieldName), TextEdits: []analysis.TextEdit{ { Pos: fieldTagInfo.Pos + token.Pos(len(fieldTagInfo.Name)), @@ -99,7 +99,7 @@ func reportShouldAddOmitEmpty(pass *analysis.Pass, field *ast.Field, omitEmptyPo }, }) case OmitEmptyPolicyWarn: - pass.Reportf(field.Pos(), messageFmt, fieldName) + pass.Reportf(field.Pos(), messageFmt, qualifiedFieldName) case OmitEmptyPolicyIgnore: // Do nothing, as the policy is to ignore the missing omitempty tag. default: @@ -108,15 +108,15 @@ func reportShouldAddOmitEmpty(pass *analysis.Pass, field *ast.Field, omitEmptyPo } // reportShouldAddOmitZero adds an analysis diagnostic that explains that an omitzero tag should be added. -func reportShouldAddOmitZero(pass *analysis.Pass, field *ast.Field, omitZeroPolicy OmitZeroPolicy, fieldName, messageFmt string, fieldTagInfo extractjsontags.FieldTagInfo) { +func reportShouldAddOmitZero(pass *analysis.Pass, field *ast.Field, omitZeroPolicy OmitZeroPolicy, qualifiedFieldName, messageFmt string, fieldTagInfo extractjsontags.FieldTagInfo) { switch omitZeroPolicy { case OmitZeroPolicySuggestFix: pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf(messageFmt, fieldName), + Message: fmt.Sprintf(messageFmt, qualifiedFieldName), SuggestedFixes: []analysis.SuggestedFix{ { - Message: fmt.Sprintf("should add 'omitzero' to the field tag for field %s", fieldName), + Message: fmt.Sprintf("should add 'omitzero' to the field tag for field %s", qualifiedFieldName), TextEdits: []analysis.TextEdit{ { Pos: fieldTagInfo.Pos + token.Pos(len(fieldTagInfo.Name)), @@ -127,7 +127,7 @@ func reportShouldAddOmitZero(pass *analysis.Pass, field *ast.Field, omitZeroPoli }, }) case OmitZeroPolicyWarn: - pass.Reportf(field.Pos(), messageFmt, fieldName) + pass.Reportf(field.Pos(), messageFmt, qualifiedFieldName) case OmitZeroPolicyForbid: // Do nothing, as the policy is to forbid the missing omitzero tag. default: @@ -136,12 +136,12 @@ func reportShouldAddOmitZero(pass *analysis.Pass, field *ast.Field, omitZeroPoli } // reportShouldRemoveOmitZero adds an analysis diagnostic that explains that an omitzero tag should be removed. -func reportShouldRemoveOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo) { +func reportShouldRemoveOmitZero(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string, jsonTags extractjsontags.FieldTagInfo) { omitZeroPos := jsonTags.Pos + token.Pos(strings.Index(jsonTags.RawValue, ",omitzero")) pass.Report(analysis.Diagnostic{ Pos: field.Pos(), - Message: fmt.Sprintf("field %s has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed.", fieldName), + Message: fmt.Sprintf("field %s has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed.", qualifiedFieldName), SuggestedFixes: []analysis.SuggestedFix{ { Message: "should remove the omitzero tag", diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/type_check.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/type_check.go index d6b8e1b4e63..adeb99c4bc8 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/type_check.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/type_check.go @@ -31,14 +31,16 @@ type TypeChecker interface { } // NewTypeChecker returns a new TypeChecker with the provided checkFunc. -func NewTypeChecker(checkFunc func(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string)) TypeChecker { +func NewTypeChecker(isTypeFunc func(pass *analysis.Pass, ident ast.Expr) bool, checkFunc func(pass *analysis.Pass, expr ast.Expr, node ast.Node, prefix string)) TypeChecker { return &typeChecker{ - checkFunc: checkFunc, + isTypeFunc: isTypeFunc, + checkFunc: checkFunc, } } type typeChecker struct { - checkFunc func(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) + isTypeFunc func(pass *analysis.Pass, expr ast.Expr) bool + checkFunc func(pass *analysis.Pass, expr ast.Expr, node ast.Node, prefix string) } // CheckNode checks the provided node for built-in types. @@ -62,7 +64,7 @@ func (t *typeChecker) CheckNode(pass *analysis.Pass, node ast.Node) { } func (t *typeChecker) checkField(pass *analysis.Pass, field *ast.Field) { - fieldName := FieldName(field) + fieldName := GetQualifiedFieldName(pass, field) if fieldName == "" { return } @@ -84,6 +86,11 @@ func (t *typeChecker) checkTypeSpec(pass *analysis.Pass, tSpec *ast.TypeSpec, no } func (t *typeChecker) checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node ast.Node, prefix string) { + if t.isTypeFunc(pass, typeExpr) { + t.checkFunc(pass, typeExpr, node, prefix) + return + } + switch typ := typeExpr.(type) { case *ast.Ident: t.checkIdent(pass, typ, node, prefix) @@ -94,18 +101,14 @@ func (t *typeChecker) checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node case *ast.MapType: t.checkTypeExpr(pass, typ.Key, node, fmt.Sprintf("%s map key", prefix)) t.checkTypeExpr(pass, typ.Value, node, fmt.Sprintf("%s map value", prefix)) + case *ast.IndexExpr: + t.checkTypeExpr(pass, typ.X, node, prefix) } } // checkIdent calls the checkFunc with the ident, when we have hit a built-in type. // If the ident is not a built in, we look at the underlying type until we hit a built-in type. func (t *typeChecker) checkIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { - if IsBasicType(pass, ident) { - // We've hit a built-in type, no need to check further. - t.checkFunc(pass, ident, node, prefix) - return - } - tSpec, ok := LookupTypeSpec(pass, ident) if !ok { return diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/utils.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/utils.go index bbcf93c7542..0735040fcec 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/utils.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/utils.go @@ -16,22 +16,52 @@ limitations under the License. package utils import ( + "errors" + "fmt" "go/ast" "go/token" "go/types" "slices" + "strings" "golang.org/x/tools/go/analysis" - "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/markers" ) +const stringTypeName = "string" + // IsBasicType checks if the type of the given identifier is a basic type. // Basic types are types like int, string, bool, etc. -func IsBasicType(pass *analysis.Pass, ident *ast.Ident) bool { - _, ok := pass.TypesInfo.TypeOf(ident).(*types.Basic) +func IsBasicType(pass *analysis.Pass, expr ast.Expr) bool { + _, ok := pass.TypesInfo.TypeOf(expr).(*types.Basic) return ok } +// IsStringType checks if the type of the given expression is a string type.. +func IsStringType(pass *analysis.Pass, expr ast.Expr) bool { + // In case the expr is a pointer. + underlying := getUnderlyingType(expr) + + ident, ok := underlying.(*ast.Ident) + if !ok { + return false + } + + if ident.Name == stringTypeName { + return true + } + + // Is either an alias or another basic type, try to look up the alias. + tSpec, ok := LookupTypeSpec(pass, ident) + if !ok { + // Basic type and not a string. + return false + } + + return IsStringType(pass, tSpec.Type) +} + // IsStructType checks if the given expression is a struct type. func IsStructType(pass *analysis.Pass, expr ast.Expr) bool { underlying := getUnderlyingType(expr) @@ -63,6 +93,12 @@ func IsStarExpr(expr ast.Expr) (bool, ast.Expr) { return false, expr } +// IsPointer checks if the expression is a pointer. +func IsPointer(expr ast.Expr) bool { + _, ok := expr.(*ast.StarExpr) + return ok +} + // IsPointerType checks if the expression is a pointer type. // This is for types that are always implemented as pointers and therefore should // not be the underlying type of a star expr. @@ -150,6 +186,78 @@ func FieldName(field *ast.Field) string { return "" } +// GetStructName returns the name of the struct that the field is in. +func GetStructName(pass *analysis.Pass, field *ast.Field) string { + _, astFile := getFilesForField(pass, field) + if astFile == nil { + return "" + } + + return GetStructNameFromFile(astFile, field) +} + +// GetStructNameFromFile returns the name of the struct that the field is in. +func GetStructNameFromFile(file *ast.File, field *ast.Field) string { + var ( + structName string + found bool + ) + + ast.Inspect(file, func(n ast.Node) bool { + if found { + return false + } + + typeSpec, ok := n.(*ast.TypeSpec) + if !ok { + return true + } + + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + return true + } + + structName = typeSpec.Name.Name + + if structType.Fields == nil { + return true + } + + if slices.Contains(structType.Fields.List, field) { + found = true + return false + } + + return true + }) + + if found { + return structName + } + + return "" +} + +// GetQualifiedFieldName returns the qualified field name. +func GetQualifiedFieldName(pass *analysis.Pass, field *ast.Field) string { + fieldName := FieldName(field) + structName := GetStructName(pass, field) + + return fmt.Sprintf("%s.%s", structName, fieldName) +} + +func getFilesForField(pass *analysis.Pass, field *ast.Field) (*token.File, *ast.File) { + tokenFile := pass.Fset.File(field.Pos()) + for _, astFile := range pass.Files { + if astFile.FileStart == token.Pos(tokenFile.Base()) { + return tokenFile, astFile + } + } + + return tokenFile, nil +} + func getFilesForType(pass *analysis.Pass, ident *ast.Ident) (*token.File, *ast.File) { namedType, ok := pass.TypesInfo.TypeOf(ident).(*types.Named) if !ok { @@ -176,7 +284,7 @@ func isInPassPackage(pass *analysis.Pass, namedType *types.Named) bool { // the type and include them in the markers.MarkerSet that is returned. // It will look through *ast.StarExpr to the underlying type. // Markers on the type will always come before markers on the field in the list of markers for an identifier. -func TypeAwareMarkerCollectionForField(pass *analysis.Pass, markersAccess markers.Markers, field *ast.Field) markers.MarkerSet { +func TypeAwareMarkerCollectionForField(pass *analysis.Pass, markersAccess markershelper.Markers, field *ast.Field) markershelper.MarkerSet { markers := markersAccess.FieldMarkers(field) var underlyingType ast.Expr @@ -350,47 +458,93 @@ func isTypeBasic(t types.Type) bool { return false } -// GetStructNameForField inspects the AST of the package and returns the name of the struct -// that contains the field being inspected. -func GetStructNameForField(pass *analysis.Pass, field *ast.Field) string { - for _, file := range pass.Files { - var ( - structName string - found bool - ) - - ast.Inspect(file, func(n ast.Node) bool { - if found { - return false - } +// GetMinProperties returns the value of the minimum properties marker. +// It returns a nil value when the marker is not present, and an error +// when the marker is present, but malformed. +func GetMinProperties(markerSet markershelper.MarkerSet) (*int, error) { + minProperties, err := getMarkerNumericValueByName[int](markerSet, markers.KubebuilderMinPropertiesMarker) + if err != nil && !errors.Is(err, errMarkerMissingValue) { + return nil, fmt.Errorf("invalid format for minimum properties marker: %w", err) + } - typeSpec, ok := n.(*ast.TypeSpec) - if !ok { - return true - } + return minProperties, nil +} - structType, ok := typeSpec.Type.(*ast.StructType) - if !ok { - return true - } +// IsKubernetesListType checks if a struct is a Kubernetes List type. +// A Kubernetes List type has: +// - Name ending with "List" (only checked if name is provided) +// - Exactly 3 fields: TypeMeta, ListMeta, and Items (slice type) +// +// The name parameter is optional and can be an empty string. When empty, only +// the structural pattern (3 fields: TypeMeta, ListMeta, Items) is checked without +// validating the type name suffix. This is useful for generic field inspection +// where the type name may not be readily available. +// +// Example: +// +// type FooList struct { +// metav1.TypeMeta `json:",inline"` +// metav1.ListMeta `json:"metadata,omitempty"` +// Items []Foo `json:"items"` +// } +func IsKubernetesListType(sTyp *ast.StructType, name string) bool { + if sTyp == nil || sTyp.Fields == nil || sTyp.Fields.List == nil { + return false + } - structName = typeSpec.Name.Name + // Check name suffix if name is provided + if name != "" && !strings.HasSuffix(name, "List") { + return false + } - if structType.Fields == nil { - return true - } + // Must have exactly 3 fields + if len(sTyp.Fields.List) != 3 { + return false + } - if slices.Contains(structType.Fields.List, field) { - found = true - return false - } + return hasListFields(sTyp.Fields.List) +} - return true - }) +// hasListFields checks if the field list contains TypeMeta, ListMeta, and Items. +func hasListFields(fields []*ast.Field) bool { + hasTypeMeta := false + hasListMeta := false + hasItems := false - if found { - return structName + for _, field := range fields { + typeName := getFieldTypeName(field) + + // Check for TypeMeta (embedded or named) + if typeName == "TypeMeta" { + hasTypeMeta = true + continue } + + // Check for ListMeta (embedded or named) + if typeName == "ListMeta" { + hasListMeta = true + continue + } + + // Check for Items field (must be named "Items" and be a slice type) + if len(field.Names) > 0 && field.Names[0].Name == "Items" { + if _, ok := field.Type.(*ast.ArrayType); ok { + hasItems = true + } + } + } + + return hasTypeMeta && hasListMeta && hasItems +} + +// getFieldTypeName returns the type name of a field, handling both embedded fields +// and named fields with simple or qualified identifiers (e.g., TypeMeta or metav1.TypeMeta). +func getFieldTypeName(field *ast.Field) string { + switch t := field.Type.(type) { + case *ast.Ident: + return t.Name + case *ast.SelectorExpr: + return t.Sel.Name } return "" diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/zero_value.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/zero_value.go index 39281ce480b..c1146217d28 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/zero_value.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/zero_value.go @@ -41,22 +41,22 @@ var ( // For structs, for the zero value to be valid, all fields within the struct that would not be omitted must accept their zero values. // The second return value indicates whether the field validation is complete. Complete validation means that we are certain whether or not the zero value is valid. // Incomplete validation means that if additional validation were added (e.g. to add a min length to a string), the zero value might become invalid. -func IsZeroValueValid(pass *analysis.Pass, field *ast.Field, typeExpr ast.Expr, markersAccess markershelper.Markers, considerOmitzero bool) (bool, bool) { +func IsZeroValueValid(pass *analysis.Pass, field *ast.Field, typeExpr ast.Expr, markersAccess markershelper.Markers, considerOmitzero bool, qualifiedFieldName string) (bool, bool) { underlyingType := getUnderlyingType(typeExpr) switch t := underlyingType.(type) { case *ast.StructType: // For structs, we have to check if there are any non-omitted fields, that do not accept a zero value. - return isStructZeroValueValid(pass, field, t, markersAccess, considerOmitzero) + return isStructZeroValueValid(pass, field, t, markersAccess, considerOmitzero, qualifiedFieldName) case *ast.Ident: - return isIdentZeroValueValid(pass, field, t, markersAccess, considerOmitzero) + return isIdentZeroValueValid(pass, field, t, markersAccess, considerOmitzero, qualifiedFieldName) case *ast.MapType: return isMapZeroValueValid(pass, field, markersAccess) case *ast.ArrayType: // For arrays, we can use a zero value if the array is not required to have a minimum number of items. return isArrayZeroValueValid(pass, field, t, markersAccess) case *ast.StarExpr: - return IsZeroValueValid(pass, field, t.X, markersAccess, considerOmitzero) + return IsZeroValueValid(pass, field, t.X, markersAccess, considerOmitzero, qualifiedFieldName) } // We don't know what the type is so can't assert the zero value is valid. @@ -73,25 +73,54 @@ func getUnderlyingType(expr ast.Expr) ast.Expr { return expr } +// GetTypeMarkerValue returns the value of the kubebuilder Type marker for a field. +// Returns empty string if no Type marker is present. +// The Type marker indicates how the field serializes (e.g., "string", "number", "object"). +func GetTypeMarkerValue(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) string { + fieldMarkers := TypeAwareMarkerCollectionForField(pass, markersAccess, field) + typeMarkers := fieldMarkers.Get(markers.KubebuilderTypeMarker) + + for _, typeMarker := range typeMarkers { + // The value might be "string" (with quotes) or string (without quotes) + typeValue := strings.Trim(typeMarker.Payload.Value, `"`) + if typeValue != "" { + return typeValue + } + } + + return "" +} + // isStructZeroValueValid checks if the zero value of a struct is valid. // It checks if all non-omitted fields within the struct accept their zero values. // It also checks if the struct has a minProperties marker, and if so, whether the number of non-omitted fields is greater than or equal to the minProperties value. -func isStructZeroValueValid(pass *analysis.Pass, field *ast.Field, structType *ast.StructType, markersAccess markershelper.Markers, considerOmitzero bool) (bool, bool) { +// Special case: If the struct has Type=string marker with string validation markers (MinLength/MaxLength), +// treat it as a string for validation purposes (e.g., for structs with custom marshalling). +func isStructZeroValueValid(pass *analysis.Pass, field *ast.Field, structType *ast.StructType, markersAccess markershelper.Markers, considerOmitzero bool, qualifiedFieldName string) (bool, bool) { if structType == nil { return false, false } + // Check if this struct should be validated as a string (Type=string marker). + // This handles structs with custom marshalling that serialize as strings. + if GetTypeMarkerValue(pass, field, markersAccess) == stringTypeName { + // Use string validation logic instead of struct validation logic. + // This ensures that string-specific validation markers (MinLength, MaxLength, Pattern) + // are properly evaluated for structs that marshal as strings. + return isStringZeroValueValid(pass, field, markersAccess) + } + jsonTagInfo, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags) if !ok { panic("could not get struct field tags from pass result") } - zeroValueValid, nonOmittedFields := areStructFieldZeroValuesValid(pass, structType, markersAccess, jsonTagInfo, considerOmitzero) + zeroValueValid, nonOmittedFields := areStructFieldZeroValuesValid(pass, structType, markersAccess, jsonTagInfo, considerOmitzero, qualifiedFieldName) markerSet := TypeAwareMarkerCollectionForField(pass, markersAccess, field) - minProperties, err := getMarkerNumericValueByName[int](markerSet, markers.KubebuilderMinPropertiesMarker) - if err != nil && !errors.Is(err, errMarkerMissingValue) { + minProperties, err := GetMinProperties(markerSet) + if err != nil { pass.Reportf(field.Pos(), "struct %s has an invalid minProperties marker: %v", FieldName(field), err) return false, false } @@ -114,14 +143,15 @@ func isStructZeroValueValid(pass *analysis.Pass, field *ast.Field, structType *a // areStructFieldZeroValuesValid checks if all non-omitted fields within a struct accept their zero values. // //nolint:cyclop -func areStructFieldZeroValuesValid(pass *analysis.Pass, structType *ast.StructType, markersAccess markershelper.Markers, jsonTagInfo extractjsontags.StructFieldTags, considerOmitzero bool) (bool, int) { +func areStructFieldZeroValuesValid(pass *analysis.Pass, structType *ast.StructType, markersAccess markershelper.Markers, jsonTagInfo extractjsontags.StructFieldTags, considerOmitzero bool, qualifiedFieldName string) (bool, int) { zeroValueValid := true nonOmittedFields := 0 for _, field := range structType.Fields.List { - fieldRequired := isFieldRequired(field, markersAccess) + fieldRequired := IsFieldRequired(field, markersAccess) fieldTagInfo := jsonTagInfo.FieldTags(field) isStruct := IsStructType(pass, field.Type) + isPointer := IsPointer(field.Type) // Assume the field has omitempty. // Then the zero value (omitted) for a required field is not valid, and for an optional field it is valid. @@ -141,8 +171,12 @@ func areStructFieldZeroValuesValid(pass *analysis.Pass, structType *ast.StructTy // When the field is not omitted, we need to check if the zero value is valid (required or not). switch { case isStruct && considerOmitzero && fieldTagInfo.OmitZero: + case isPointer: + // A field that is a pointer and does not have an omitempty would marshal as null. + // This is silently dropped by the API server, or is accepted as a valid value with +nullable. + // If the field does have omitempty, then the zero value is valid based on the requiredness of the field. case !fieldTagInfo.OmitEmpty: - validValue, _ = IsZeroValueValid(pass, field, field.Type, markersAccess, considerOmitzero) + validValue, _ = IsZeroValueValid(pass, field, field.Type, markersAccess, considerOmitzero, qualifiedFieldName) } // If either value is false then the collected values will be false. @@ -153,7 +187,7 @@ func areStructFieldZeroValuesValid(pass *analysis.Pass, structType *ast.StructTy } // isIdentZeroValueValid checks if the zero value of an identifier is valid. -func isIdentZeroValueValid(pass *analysis.Pass, field *ast.Field, ident *ast.Ident, markersAccess markershelper.Markers, considerOmitzero bool) (bool, bool) { +func isIdentZeroValueValid(pass *analysis.Pass, field *ast.Field, ident *ast.Ident, markersAccess markershelper.Markers, considerOmitzero bool, qualifiedFieldName string) (bool, bool) { if ident == nil { return false, false } @@ -163,9 +197,9 @@ func isIdentZeroValueValid(pass *analysis.Pass, field *ast.Field, ident *ast.Ide case isStringIdent(ident): return isStringZeroValueValid(pass, field, markersAccess) case isIntegerIdent(ident): - return isNumericZeroValueValid[int](pass, field, markersAccess) + return isNumericZeroValueValid[int](pass, field, markersAccess, qualifiedFieldName) case isFloatIdent(ident): - return isNumericZeroValueValid[float64](pass, field, markersAccess) + return isNumericZeroValueValid[float64](pass, field, markersAccess, qualifiedFieldName) case isBoolIdent(ident): // For bool, we can always use a zero value. return true, true @@ -177,7 +211,7 @@ func isIdentZeroValueValid(pass *analysis.Pass, field *ast.Field, ident *ast.Ide return false, false } - return IsZeroValueValid(pass, field, typeSpec.Type, markersAccess, considerOmitzero) + return IsZeroValueValid(pass, field, typeSpec.Type, markersAccess, considerOmitzero, qualifiedFieldName) } // isStringZeroValueValid checks if a string field can have a zero value. @@ -234,7 +268,7 @@ func enumFieldAllowsEmpty(fieldMarkers markershelper.MarkerSet) bool { enumMarker := fieldMarkers.Get(markers.KubebuilderEnumMarker) for _, marker := range enumMarker { - return slices.Contains(strings.Split(marker.Expressions[""], ";"), "\"\"") + return slices.Contains(strings.Split(marker.Payload.Value, ";"), "\"\"") } return false @@ -249,18 +283,18 @@ type number interface { // isIntegerZeroValueValid checks if an integer field can have a zero value. // //nolint:cyclop -func isNumericZeroValueValid[N number](pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) (bool, bool) { +func isNumericZeroValueValid[N number](pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) (bool, bool) { fieldMarkers := TypeAwareMarkerCollectionForField(pass, markersAccess, field) minimum, err := getMarkerNumericValueByName[N](fieldMarkers, markers.KubebuilderMinimumMarker) if err != nil && !errors.Is(err, errMarkerMissingValue) { - pass.Reportf(field.Pos(), "field %s has an invalid minimum marker: %v", FieldName(field), err) + pass.Reportf(field.Pos(), "field %s has an invalid minimum marker: %v", qualifiedFieldName, err) return false, false } maximum, err := getMarkerNumericValueByName[N](fieldMarkers, markers.KubebuilderMaximumMarker) if err != nil && !errors.Is(err, errMarkerMissingValue) { - pass.Reportf(field.Pos(), "field %s has an invalid maximum marker: %v", FieldName(field), err) + pass.Reportf(field.Pos(), "field %s has an invalid maximum marker: %v", qualifiedFieldName, err) return false, false } @@ -290,11 +324,12 @@ func getMarkerNumericValueByName[N number](marker markershelper.MarkerSet, marke // getMarkerNumericValue extracts a numeric value from the default value of a marker. // Works for markers like MaxLength, MinLength, etc. func getMarkerNumericValue[N number](marker markershelper.Marker) (N, error) { - rawValue, ok := marker.Expressions[""] - if !ok { + if marker.Payload.Value == "" { return N(0), errMarkerMissingValue } + rawValue := marker.Payload.Value + value, err := strconv.ParseFloat(rawValue, 64) if err != nil { return N(0), fmt.Errorf("error converting value to number: %w", err) @@ -421,7 +456,7 @@ func isIntegerIdent(ident *ast.Ident) bool { // isStringIdent checks if the identifier is a string type. func isStringIdent(ident *ast.Ident) bool { - return ident.Name == "string" + return ident.Name == stringTypeName } // isBoolIdent checks if the identifier is a boolean type. @@ -434,12 +469,22 @@ func isFloatIdent(ident *ast.Ident) bool { return ident.Name == "float32" || ident.Name == "float64" } -// isFieldRequired checks if the field is required. +// IsFieldRequired checks if the field is required. // It checks for the presence of the required marker, the kubebuilder required marker, or the k8s required marker. -func isFieldRequired(field *ast.Field, markersAccess markershelper.Markers) bool { +func IsFieldRequired(field *ast.Field, markersAccess markershelper.Markers) bool { fieldMarkers := markersAccess.FieldMarkers(field) return fieldMarkers.Has(markers.RequiredMarker) || fieldMarkers.Has(markers.KubebuilderRequiredMarker) || fieldMarkers.Has(markers.K8sRequiredMarker) } + +// IsFieldOptional checks if the field is optional. +// It checks for the presence of the optional marker, the kubebuilder optional marker, or the k8s optional marker. +func IsFieldOptional(field *ast.Field, markersAccess markershelper.Markers) bool { + fieldMarkers := markersAccess.FieldMarkers(field) + + return fieldMarkers.Has(markers.OptionalMarker) || + fieldMarkers.Has(markers.KubebuilderOptionalMarker) || + fieldMarkers.Has(markers.K8sOptionalMarker) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/markers/markers.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/markers/markers.go index d1a5c0ac05a..f93bd7303f8 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/markers/markers.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/markers/markers.go @@ -36,6 +36,9 @@ const ( // KubebuilderStatusSubresourceMarker is the marker that indicates that the CRD generated for a struct should include the /status subresource. KubebuilderStatusSubresourceMarker = "kubebuilder:subresource:status" + // KubebuilderAtLeastOneOfMarker is the marker that indicates that a type has a CEL validation in kubebuilder enforcing that at least one field is set. + KubebuilderAtLeastOneOfMarker = "kubebuilder:validation:AtLeastOneOf" + // KubebuilderEnumMarker is the marker that indicates that a field has an enum in kubebuilder. KubebuilderEnumMarker = "kubebuilder:validation:Enum" @@ -93,6 +96,9 @@ const ( // KubebuilderRequiredMarker is the marker that indicates that a field is required in kubebuilder. KubebuilderRequiredMarker = "kubebuilder:validation:Required" + // KubebuilderExactlyOneOf is the marker that indicates that a type has a CEL validation in kubebuilder enforcing that exactly one field is set. + KubebuilderExactlyOneOf = "kubebuilder:validation:ExactlyOneOf" + // KubebuilderItemsMaxLengthMarker is the marker that indicates that a field has a maximum length in kubebuilder. KubebuilderItemsMaxLengthMarker = "kubebuilder:validation:items:MaxLength" @@ -202,4 +208,7 @@ const ( // K8sListMapKeyMarker is the marker that indicates that a field is a map in k8s declarative validation. K8sListMapKeyMarker = "k8s:listMapKey" + + // K8sDefaultMarker is the marker that indicates the default value for a field in k8s declarative validation. + K8sDefaultMarker = "k8s:default" ) diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/plugin/base/base.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/plugin/base/base.go index 9fa1b87bf07..fbce4aa466c 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/plugin/base/base.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/plugin/base/base.go @@ -43,7 +43,7 @@ func New(settings any) (register.LinterPlugin, error) { // GolangCIPlugin constructs a new plugin for the golangci-lint // plugin pattern. // This allows golangci-lint to build a version of itself, containing -// all of the anaylzers included in KAL. +// all of the analyzers included in KAL. type GolangCIPlugin struct { config config.GolangCIConfig } diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/registration/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/registration/doc.go index 663f3bb4f50..cb8c5e4e20c 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/registration/doc.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/registration/doc.go @@ -13,6 +13,7 @@ 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. */ + /* This package is used as an internal registration of linters. @@ -27,20 +28,28 @@ 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/defaultorrequired" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/defaults" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/dependenttags" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/forbiddenmarkers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/integers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/maxlength" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/minlength" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/namingconventions" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/nobools" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/nodurations" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/nofloats" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/nonpointerstructs" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/nonullable" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/nophase" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/notimestamp" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/ssatags" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/statusoptional"