├── .custom-gcl.yml ├── .github └── workflows │ ├── go.yaml │ └── lint.yaml ├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── SECURITY_CONTACTS ├── doc.go ├── docs └── new-linter.md ├── go.mod ├── go.sum ├── pkg ├── analysis │ ├── analysis_suite_test.go │ ├── commentstart │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ ├── a.go │ │ │ ├── a.go.golden │ │ │ └── pkg │ │ │ ├── a.go │ │ │ └── a.go.golden │ ├── conditions │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ ├── a │ │ │ ├── a.go │ │ │ └── a.go.golden │ │ │ ├── b │ │ │ ├── a.go │ │ │ └── a.go.golden │ │ │ ├── c │ │ │ ├── a.go │ │ │ └── a.go.golden │ │ │ ├── d │ │ │ ├── a.go │ │ │ └── a.go.golden │ │ │ ├── e │ │ │ ├── a.go │ │ │ └── a.go.golden │ │ │ ├── f │ │ │ ├── a.go │ │ │ └── a.go.golden │ │ │ └── k8s.io │ │ │ └── apimachinery │ │ │ └── pkg │ │ │ └── apis │ │ │ └── meta │ │ │ └── v1 │ │ │ └── types.go │ ├── doc.go │ ├── duplicatemarkers │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ ├── a.go │ │ │ ├── a.go.golden │ │ │ └── b.go │ ├── errors │ │ └── errors.go │ ├── helpers │ │ ├── doc.go │ │ ├── extractjsontags │ │ │ ├── analyzer.go │ │ │ └── doc.go │ │ ├── inspector │ │ │ ├── analyzer.go │ │ │ ├── analyzer_test.go │ │ │ ├── doc.go │ │ │ ├── inspector.go │ │ │ └── testdata │ │ │ │ └── src │ │ │ │ ├── a │ │ │ │ └── a.go │ │ │ │ └── k8s.io │ │ │ │ └── apimachinery │ │ │ │ └── pkg │ │ │ │ ├── apis │ │ │ │ └── meta │ │ │ │ │ └── v1 │ │ │ │ │ └── types.go │ │ │ │ └── types │ │ │ │ └── types.go │ │ └── markers │ │ │ ├── analyzer.go │ │ │ ├── analyzer_test.go │ │ │ ├── doc.go │ │ │ ├── registry.go │ │ │ └── registry_test.go │ ├── integers │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ ├── a.go │ │ │ └── b.go │ ├── jsontags │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ ├── a │ │ │ └── a.go │ │ │ ├── b │ │ │ └── b.go │ │ │ └── k8s.io │ │ │ └── apimachinery │ │ │ └── pkg │ │ │ └── apis │ │ │ └── meta │ │ │ └── v1 │ │ │ └── types.go │ ├── maxlength │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ ├── a.go │ │ │ └── b.go │ ├── nobools │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ ├── a.go │ │ │ └── b.go │ ├── nofloats │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ ├── a.go │ │ │ └── b.go │ ├── nomaps │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ ├── a │ │ │ ├── a.go │ │ │ └── b.go │ │ │ ├── b │ │ │ └── b.go │ │ │ ├── c │ │ │ └── c.go │ │ │ └── d │ │ │ └── d.go │ ├── nophase │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ └── a.go │ ├── optionalfields │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ ├── testdata │ │ │ └── src │ │ │ │ ├── a │ │ │ │ ├── a.go │ │ │ │ ├── a.go.golden │ │ │ │ └── b.go │ │ │ │ ├── b │ │ │ │ ├── a.go │ │ │ │ ├── a.go.golden │ │ │ │ └── b.go │ │ │ │ └── c │ │ │ │ ├── a.go │ │ │ │ ├── a.go.golden │ │ │ │ └── b.go │ │ └── util.go │ ├── optionalorrequired │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ ├── a │ │ │ ├── a.go │ │ │ └── a.go.golden │ │ │ ├── b │ │ │ ├── b.go │ │ │ └── b.go.golden │ │ │ └── c │ │ │ ├── c.go │ │ │ └── c.go.golden │ ├── registry.go │ ├── registry_test.go │ ├── requiredfields │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ ├── a │ │ │ ├── a.go │ │ │ └── a.go.golden │ │ │ └── b │ │ │ ├── a.go │ │ │ └── a.go.golden │ ├── statusoptional │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ ├── a │ │ │ ├── a.go │ │ │ ├── a.go.golden │ │ │ └── b.go │ │ │ ├── b │ │ │ ├── b.go │ │ │ └── b.go.golden │ │ │ └── c │ │ │ ├── c.go │ │ │ └── c.go.golden │ ├── statussubresource │ │ ├── analyzer.go │ │ ├── analyzer_test.go │ │ ├── doc.go │ │ ├── initializer.go │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ ├── a.go │ │ │ └── a.go.golden │ └── utils │ │ ├── testdata │ │ └── src │ │ │ └── a │ │ │ ├── a.go │ │ │ └── b.go │ │ ├── type_check.go │ │ ├── type_check_test.go │ │ ├── utils.go │ │ ├── utils_suite_test.go │ │ └── utils_test.go ├── config │ ├── config.go │ ├── linters.go │ └── linters_config.go ├── markers │ └── markers.go ├── plugin │ └── plugin.go └── validation │ ├── config.go │ ├── linters.go │ ├── linters_config.go │ ├── linters_config_test.go │ ├── linters_test.go │ └── validation_suite_test.go ├── plugin.go └── tools ├── go.mod └── go.sum /.custom-gcl.yml: -------------------------------------------------------------------------------- 1 | version: v2.0.2 2 | name: golangci-kube-api-linter 3 | destination: ./bin 4 | plugins: 5 | - module: 'sigs.k8s.io/kube-api-linter' 6 | path: ./ 7 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version-file: 'go.mod' 19 | - name: Build 20 | run: make build 21 | - name: Test 22 | run: make test 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 11 | # pull-requests: read 12 | 13 | jobs: 14 | lint: 15 | name: golangci-lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version-file: 'go.mod' 22 | - name: golangci-lint 23 | run: | 24 | make lint 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin 8 | testbin/* 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # editor and IDE paraphernalia 17 | .idea 18 | *.swp 19 | *.swo 20 | *~ 21 | .vscode/ 22 | 23 | # Do not check in vendor if it's set up locally 24 | vendor/ 25 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | # Enable specific linter 5 | # https://golangci-lint.run/usage/linters/#enabled-by-default-linters 6 | enable: 7 | - asciicheck 8 | - bidichk 9 | - bodyclose 10 | - contextcheck 11 | - cyclop 12 | - dogsled 13 | - dupl 14 | - durationcheck 15 | - err113 16 | - errcheck 17 | - errname 18 | - errorlint 19 | - exhaustive 20 | - forcetypeassert 21 | - funlen 22 | - gochecknoglobals 23 | - gocognit 24 | - goconst 25 | - gocritic 26 | - gocyclo 27 | - godot 28 | - goheader 29 | - goprintffuncname 30 | - gosec 31 | - govet 32 | - importas 33 | - ineffassign 34 | - makezero 35 | - misspell 36 | - nakedret 37 | - nestif 38 | - nilerr 39 | - nilnil 40 | - nlreturn 41 | - noctx 42 | - nolintlint 43 | - prealloc 44 | - predeclared 45 | - revive 46 | - staticcheck 47 | - tagliatelle 48 | - unconvert 49 | - unparam 50 | - unused 51 | - wastedassign 52 | - whitespace 53 | - wrapcheck 54 | - wsl 55 | settings: 56 | goheader: 57 | values: 58 | regexp: 59 | license-year: (202[5-9]|20[3-9][0-9]) 60 | template: |- 61 | Copyright {{license-year}} The Kubernetes Authors. 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | nlreturn: 75 | block-size: 2 76 | revive: 77 | confidence: 0 78 | rules: 79 | - name: exported 80 | arguments: 81 | - checkPrivateReceivers 82 | - disableStutteringCheck 83 | severity: warning 84 | disabled: false 85 | staticcheck: 86 | # https://staticcheck.io/docs/options#checks 87 | checks: 88 | - all 89 | - -ST1000 90 | dot-import-whitelist: 91 | - github.com/onsi/ginkgo/v2 92 | - github.com/onsi/gomega 93 | exclusions: 94 | generated: lax 95 | rules: 96 | - linters: 97 | - dupl 98 | - err113 99 | - funlen 100 | - gochecknoglobals 101 | - gocyclo 102 | - gosec 103 | # Exclude some linters from running on tests files. 104 | path: _test\.go 105 | - linters: 106 | - all 107 | path: testdata 108 | - path: (.+)\.go$ 109 | text: Analyzer is a global variable 110 | paths: 111 | - third_party$ 112 | - builtin$ 113 | - examples$ 114 | formatters: 115 | enable: 116 | - gofmt 117 | - goimports 118 | exclusions: 119 | generated: lax 120 | paths: 121 | - third_party$ 122 | - builtin$ 123 | - examples$ 124 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 2 | GOLANGCI_LINT = go tool -modfile tools/go.mod github.com/golangci/golangci-lint/v2/cmd/golangci-lint 3 | MODERNIZE = go tool -modfile tools/go.mod golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize 4 | 5 | VERSION ?= $(shell git describe --always --abbrev=7) 6 | 7 | .PHONY: all 8 | all: build 9 | 10 | ##@ General 11 | 12 | # The help target prints out all targets with their descriptions organized 13 | # beneath their categories. The categories are represented by '##@' and the 14 | # target descriptions by '##'. The awk commands is responsible for reading the 15 | # entire set of makefiles included in this invocation, looking for lines of the 16 | # file as xyz: ## something, and then pretty-format the target and help. Then, 17 | # if there's a line with ##@ something, that gets pretty-printed as a category. 18 | # More info on the usage of ANSI control characters for terminal formatting: 19 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 20 | # More info on the awk command: 21 | # http://linuxcommand.org/lc3_adv_awk.php 22 | 23 | .PHONY: help 24 | help: ## Display this help. 25 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 26 | 27 | ##@ Development 28 | 29 | .PHONY: fmt 30 | fmt: ## Run go fmt against code. 31 | go fmt ./... 32 | 33 | .PHONY: vet 34 | vet: ## Run go vet against code. 35 | go vet ./... 36 | 37 | .PHONY: lint 38 | lint: golangci-lint modernize 39 | 40 | .PHONY: lint-fix 41 | lint-fix: golangci-lint-fix modernize-fix 42 | 43 | .PHONY: golangci-lint 44 | golangci-lint: ## Run golangci-lint over the codebase. 45 | ${GOLANGCI_LINT} run ./... --timeout 5m -v ${GOLANGCI_LINT_EXTRA_ARGS} 46 | 47 | .PHONY: golangci-lint-fix 48 | golangci-lint-fix: GOLANGCI_LINT_EXTRA_ARGS := --fix 49 | golangci-lint-fix: golangci-lint ## Run golangci-lint over the codebase and run auto-fixers if supported by the linter 50 | 51 | .PHONY: modernize 52 | modernize: ## Run modernize on the codebase. 53 | ${MODERNIZE} -diff ./... 54 | 55 | .PHONY: modernize-fix 56 | modernize-fix: ## Run modernize on the codebase and apply fixes. 57 | ${MODERNIZE} -fix ./... 58 | 59 | .PHONY: test 60 | test: fmt vet unit ## Run tests. 61 | 62 | .PHONY: unit 63 | unit: ## Run unit tests. 64 | go test ./... 65 | 66 | ##@ Build 67 | 68 | .PHONY: build 69 | build: ## Build the golangci-lint custom plugin binary. 70 | ${GOLANGCI_LINT} custom 71 | 72 | .PHONY: verify-% 73 | verify-%: 74 | make $* 75 | git diff --exit-code 76 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | reviewers: 2 | - JoelSpeed 3 | - jpbetz 4 | - sivchari 5 | - everettraven 6 | approvers: 7 | - JoelSpeed 8 | - jpbetz 9 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Team to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/sig-release/security-release-process-documentation/security-release-process.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | JoelSpeed 14 | jpbetz 15 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Kube API Linter (KAL) is a linter for Kubernetes API types, that implements API conventions and best practices. 19 | 20 | This package provides a GolangCI-Lint plugin that can be used to build a custom linter for Kubernetes API types. 21 | The custom golangci-lint binary can be built by checking out the Kube API Linter repository and running `make build`. 22 | This will generate a custom golangci-lint binary in the `bin` directory. 23 | 24 | The custom golangci-lint binary can be run with the `run` command, and the Kube API Linter rules can be enabled by setting the `kube-api-linter` linter in the `.golangci.yml` configuration file. 25 | 26 | Example `.golangci.yml` configuration file: 27 | 28 | linters-settings: 29 | custom: 30 | kubeapilinter:: 31 | type: "module" 32 | description: Kube API Linter lints Kube like APIs based on API conventions and best practices. 33 | settings: 34 | linters: 35 | enabled: [] 36 | disabled: [] 37 | lintersConfig: 38 | jsonTags: 39 | jsonTagRegex: "" 40 | optionalOrRequired: 41 | preferredOptionalMarker: "" 42 | preferredRequiredMarker: "" 43 | linters: 44 | disable-all: true 45 | enable: 46 | - kubeapilinter 47 | 48 | New linters can be added in the [sigs.k8s.io/kube-api-linter/pkg/analysis] package. 49 | */ 50 | package kubeapilinter 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/kube-api-linter 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/golangci/plugin-module-register v0.1.1 7 | github.com/onsi/ginkgo/v2 v2.23.3 8 | github.com/onsi/gomega v1.36.3 9 | golang.org/x/tools v0.32.0 10 | k8s.io/apimachinery v0.32.3 11 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 12 | ) 13 | 14 | require ( 15 | github.com/go-logr/logr v1.4.2 // indirect 16 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 17 | github.com/google/go-cmp v0.7.0 // indirect 18 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 19 | github.com/kr/pretty v0.3.1 // indirect 20 | github.com/rogpeppe/go-internal v1.14.1 // indirect 21 | github.com/stretchr/testify v1.10.0 // indirect 22 | golang.org/x/mod v0.24.0 // indirect 23 | golang.org/x/net v0.39.0 // indirect 24 | golang.org/x/sync v0.13.0 // indirect 25 | golang.org/x/sys v0.32.0 // indirect 26 | golang.org/x/text v0.24.0 // indirect 27 | google.golang.org/protobuf v1.36.6 // indirect 28 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /pkg/analysis/analysis_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package analysis_test 17 | 18 | import ( 19 | "testing" 20 | 21 | . "github.com/onsi/ginkgo/v2" 22 | . "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestValidation(t *testing.T) { 26 | RegisterFailHandler(Fail) 27 | RunSpecs(t, "Validation") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/analysis/commentstart/analyzer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package commentstart 17 | 18 | import ( 19 | "fmt" 20 | "go/ast" 21 | "go/token" 22 | "go/types" 23 | "strings" 24 | 25 | "golang.org/x/tools/go/analysis" 26 | kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" 27 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" 28 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" 29 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" 30 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 31 | ) 32 | 33 | const name = "commentstart" 34 | 35 | // Analyzer is the analyzer for the commentstart package. 36 | // It checks that all struct fields in an API have a godoc, and that the godoc starts with the serialised field name. 37 | var Analyzer = &analysis.Analyzer{ 38 | Name: name, 39 | Doc: "Check that all struct fields in an API have a godoc, and that the godoc starts with the serialised field name", 40 | Run: run, 41 | Requires: []*analysis.Analyzer{inspector.Analyzer}, 42 | } 43 | 44 | func run(pass *analysis.Pass) (any, error) { 45 | inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) 46 | if !ok { 47 | return nil, kalerrors.ErrCouldNotGetInspector 48 | } 49 | 50 | inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) { 51 | checkField(pass, field, jsonTagInfo) 52 | }) 53 | 54 | return nil, nil //nolint:nilnil 55 | } 56 | 57 | func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo) { 58 | if tagInfo.Name == "" { 59 | return 60 | } 61 | 62 | fieldName := utils.FieldName(field) 63 | if fieldName == "" { 64 | fieldName = types.ExprString(field.Type) 65 | } 66 | 67 | if field.Doc == nil { 68 | pass.Reportf(field.Pos(), "field %s is missing godoc comment", fieldName) 69 | return 70 | } 71 | 72 | firstLine := field.Doc.List[0] 73 | if !strings.HasPrefix(firstLine.Text, "// "+tagInfo.Name+" ") { 74 | if strings.HasPrefix(strings.ToLower(firstLine.Text), strings.ToLower("// "+tagInfo.Name+" ")) { 75 | // The comment start is correct, apart from the casing, we can fix that. 76 | pass.Report(analysis.Diagnostic{ 77 | Pos: firstLine.Pos(), 78 | Message: fmt.Sprintf("godoc for field %s should start with '%s ...'", fieldName, tagInfo.Name), 79 | SuggestedFixes: []analysis.SuggestedFix{ 80 | { 81 | Message: fmt.Sprintf("should replace first word with `%s`", tagInfo.Name), 82 | TextEdits: []analysis.TextEdit{ 83 | { 84 | Pos: firstLine.Pos(), 85 | End: firstLine.Pos() + token.Pos(len(tagInfo.Name)) + token.Pos(4), 86 | NewText: []byte("// " + tagInfo.Name + " "), 87 | }, 88 | }, 89 | }, 90 | }, 91 | }) 92 | } else { 93 | pass.Reportf(field.Doc.List[0].Pos(), "godoc for field %s should start with '%s ...'", fieldName, tagInfo.Name) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pkg/analysis/commentstart/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package commentstart_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/commentstart" 23 | ) 24 | 25 | func Test(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | analysistest.RunWithSuggestedFixes(t, testdata, commentstart.Analyzer, "a/...") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/analysis/commentstart/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | commentstart is a simple analysis tool that checks if the first line of a comment is the same as the json tag. 19 | 20 | By convention in Go, comments typically start with the name of the item being described. 21 | In the case of field names, this would mean for a field Foo, the comment should look like: 22 | 23 | // Foo is a field that does something. 24 | Foo string `json:"foo"` 25 | 26 | However, in Kubernetes API types, the json tag is often used to generate documentation. 27 | In this case, the comment should start with the json tag, like so: 28 | 29 | // foo is a field that does something. 30 | Foo string `json:"foo"` 31 | 32 | This ensures that for any generated documentation, the documentation refers to the serialized field name. 33 | We expect most readers of Kubernetes API documentation will be more familiar with the serialized field names than the Go field names. 34 | */ 35 | package commentstart 36 | -------------------------------------------------------------------------------- /pkg/analysis/commentstart/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package commentstart 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return Analyzer, nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/commentstart/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | import "a/pkg" 4 | 5 | type CommentStartTestStruct struct { 6 | NoJSONTag string 7 | EmptyJSONTag string `json:""` 8 | InlineJSONTag string `json:",inline"` 9 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 10 | Ignored string `json:"-"` 11 | Hyphen string `json:"-,"` // want "field Hyphen is missing godoc comment" 12 | 13 | AnonymousStruct struct { // want "field AnonymousStruct is missing godoc comment" 14 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 15 | } `json:"anonymousStruct"` 16 | 17 | AnonymousStructInlineJSONTag struct { 18 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 19 | } `json:",inline"` 20 | 21 | IgnoredAnonymousStruct struct { 22 | NoComment string `json:"noComment"` 23 | } `json:"-"` 24 | 25 | StructForInlineField `json:",inline"` 26 | 27 | A `json:"a"` // want "field A is missing godoc comment" 28 | 29 | PkgA pkg.A `json:"pkgA"` // want "field PkgA is missing godoc comment" 30 | 31 | pkg.Embedded `json:"embedded"` // want "field pkg.Embedded is missing godoc comment" 32 | 33 | *pkg.EmbeddedPointer `json:"embeddedPointer"` // want "field \\*pkg.EmbeddedPointer is missing godoc comment" 34 | 35 | // IncorrectStartComment is a field with an incorrect start to the comment. // want "godoc for field IncorrectStartComment should start with 'incorrectStartComment ...'" 36 | IncorrectStartComment string `json:"incorrectStartComment"` 37 | 38 | // IncorrectStartOptionalComment is a field with an incorrect start to the comment. // want "godoc for field IncorrectStartOptionalComment should start with 'incorrectStartOptionalComment ...'" 39 | IncorrectStartOptionalComment string `json:"incorrectStartOptionalComment"` 40 | 41 | // correctStartComment is a field with a correct start to the comment. 42 | CorrectStartComment string `json:"correctStartComment"` 43 | 44 | // correctStartOptionalComment is a field with a correct start to the comment. 45 | CorrectStartOptionalComment string `json:"correctStartOptionalComment,omitempty"` 46 | 47 | // IncorrectMultiLineComment is a field with an incorrect start to the comment. // want "godoc for field IncorrectMultiLineComment should start with 'incorrectMultiLineComment ...'" 48 | // Except this time there are multiple lines to the comment. 49 | IncorrectMultiLineComment string `json:"incorrectMultiLineComment"` 50 | 51 | // correctMultiLineComment is a field with a correct start to the comment. 52 | // Except this time there are multiple lines to the comment. 53 | CorrectMultiLineComment string `json:"correctMultiLineComment"` 54 | 55 | // This comment just isn't correct at all, doesn't even start with anything resembling the field names. // want "godoc for field IncorrectComment should start with 'incorrectComment ...'" 56 | IncorrectComment string `json:"incorrectComment"` 57 | } 58 | 59 | // DoNothing is used to check that the analyser doesn't report on methods. 60 | func (CommentStartTestStruct) DoNothing() {} 61 | 62 | type StructForInlineField struct { 63 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 64 | } 65 | 66 | type A struct { 67 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 68 | } 69 | 70 | type unexportedStruct struct { 71 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 72 | } 73 | 74 | type ( 75 | MultipleTypeDeclaration1 struct { 76 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 77 | } 78 | MultipleTypeDeclaration2 struct { 79 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 80 | } 81 | ) 82 | 83 | func FunctionWithStructs() { 84 | type InaccessibleStruct struct { 85 | NoComment string `json:"noComment"` 86 | } 87 | } 88 | 89 | type Interface interface { 90 | InaccessibleFunction() string 91 | } 92 | -------------------------------------------------------------------------------- /pkg/analysis/commentstart/testdata/src/a/a.go.golden: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | import "a/pkg" 4 | 5 | type CommentStartTestStruct struct { 6 | NoJSONTag string 7 | EmptyJSONTag string `json:""` 8 | InlineJSONTag string `json:",inline"` 9 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 10 | Ignored string `json:"-"` 11 | Hyphen string `json:"-,"` // want "field Hyphen is missing godoc comment" 12 | 13 | AnonymousStruct struct { // want "field AnonymousStruct is missing godoc comment" 14 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 15 | } `json:"anonymousStruct"` 16 | 17 | AnonymousStructInlineJSONTag struct { 18 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 19 | } `json:",inline"` 20 | 21 | IgnoredAnonymousStruct struct { 22 | NoComment string `json:"noComment"` 23 | } `json:"-"` 24 | 25 | StructForInlineField `json:",inline"` 26 | 27 | A `json:"a"` // want "field A is missing godoc comment" 28 | 29 | PkgA pkg.A `json:"pkgA"` // want "field PkgA is missing godoc comment" 30 | 31 | pkg.Embedded `json:"embedded"` // want "field pkg.Embedded is missing godoc comment" 32 | 33 | *pkg.EmbeddedPointer `json:"embeddedPointer"` // want "field \\*pkg.EmbeddedPointer is missing godoc comment" 34 | 35 | // incorrectStartComment is a field with an incorrect start to the comment. // want "godoc for field IncorrectStartComment should start with 'incorrectStartComment ...'" 36 | IncorrectStartComment string `json:"incorrectStartComment"` 37 | 38 | // incorrectStartOptionalComment is a field with an incorrect start to the comment. // want "godoc for field IncorrectStartOptionalComment should start with 'incorrectStartOptionalComment ...'" 39 | IncorrectStartOptionalComment string `json:"incorrectStartOptionalComment"` 40 | 41 | // correctStartComment is a field with a correct start to the comment. 42 | CorrectStartComment string `json:"correctStartComment"` 43 | 44 | // correctStartOptionalComment is a field with a correct start to the comment. 45 | CorrectStartOptionalComment string `json:"correctStartOptionalComment,omitempty"` 46 | 47 | // incorrectMultiLineComment is a field with an incorrect start to the comment. // want "godoc for field IncorrectMultiLineComment should start with 'incorrectMultiLineComment ...'" 48 | // Except this time there are multiple lines to the comment. 49 | IncorrectMultiLineComment string `json:"incorrectMultiLineComment"` 50 | 51 | // correctMultiLineComment is a field with a correct start to the comment. 52 | // Except this time there are multiple lines to the comment. 53 | CorrectMultiLineComment string `json:"correctMultiLineComment"` 54 | 55 | // This comment just isn't correct at all, doesn't even start with anything resembling the field names. // want "godoc for field IncorrectComment should start with 'incorrectComment ...'" 56 | IncorrectComment string `json:"incorrectComment"` 57 | } 58 | 59 | // DoNothing is used to check that the analyser doesn't report on methods. 60 | func (CommentStartTestStruct) DoNothing() {} 61 | 62 | type StructForInlineField struct { 63 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 64 | } 65 | 66 | type A struct { 67 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 68 | } 69 | 70 | type unexportedStruct struct { 71 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 72 | } 73 | 74 | type ( 75 | MultipleTypeDeclaration1 struct { 76 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 77 | } 78 | MultipleTypeDeclaration2 struct { 79 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 80 | } 81 | ) 82 | 83 | func FunctionWithStructs() { 84 | type InaccessibleStruct struct { 85 | NoComment string `json:"noComment"` 86 | } 87 | } 88 | 89 | type Interface interface { 90 | InaccessibleFunction() string 91 | } 92 | -------------------------------------------------------------------------------- /pkg/analysis/commentstart/testdata/src/a/pkg/a.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | type A struct { 4 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 5 | } 6 | 7 | // To embed the same struct multiple times, we need to rename the type. 8 | type Embedded A 9 | type EmbeddedPointer A 10 | -------------------------------------------------------------------------------- /pkg/analysis/commentstart/testdata/src/a/pkg/a.go.golden: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | type A struct { 4 | NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment" 5 | } 6 | 7 | // To embed the same struct multiple times, we need to rename the type. 8 | type Embedded A 9 | type EmbeddedPointer A 10 | -------------------------------------------------------------------------------- /pkg/analysis/conditions/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package conditions_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/conditions" 23 | "sigs.k8s.io/kube-api-linter/pkg/config" 24 | ) 25 | 26 | func TestDefaultConfiguration(t *testing.T) { 27 | testdata := analysistest.TestData() 28 | 29 | a, err := conditions.Initializer().Init(config.LintersConfig{}) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | analysistest.RunWithSuggestedFixes(t, testdata, a, "a") 35 | } 36 | 37 | func TestNotFieldFirst(t *testing.T) { 38 | testdata := analysistest.TestData() 39 | 40 | a, err := conditions.Initializer().Init(config.LintersConfig{ 41 | Conditions: config.ConditionsConfig{ 42 | IsFirstField: config.ConditionsFirstFieldIgnore, 43 | }, 44 | }) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | analysistest.RunWithSuggestedFixes(t, testdata, a, "b") 50 | } 51 | 52 | func TestIgnoreProtobuf(t *testing.T) { 53 | testdata := analysistest.TestData() 54 | 55 | a, err := conditions.Initializer().Init(config.LintersConfig{ 56 | Conditions: config.ConditionsConfig{ 57 | UseProtobuf: config.ConditionsUseProtobufIgnore, 58 | }, 59 | }) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | analysistest.RunWithSuggestedFixes(t, testdata, a, "c") 65 | } 66 | 67 | func TestForbidProtobuf(t *testing.T) { 68 | testdata := analysistest.TestData() 69 | 70 | a, err := conditions.Initializer().Init(config.LintersConfig{ 71 | Conditions: config.ConditionsConfig{ 72 | UseProtobuf: config.ConditionsUseProtobufForbid, 73 | }, 74 | }) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | analysistest.RunWithSuggestedFixes(t, testdata, a, "d") 80 | } 81 | 82 | func TestIgnorePatchStrategy(t *testing.T) { 83 | testdata := analysistest.TestData() 84 | 85 | a, err := conditions.Initializer().Init(config.LintersConfig{ 86 | Conditions: config.ConditionsConfig{ 87 | UsePatchStrategy: config.ConditionsUsePatchStrategyIgnore, 88 | }, 89 | }) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | analysistest.RunWithSuggestedFixes(t, testdata, a, "e") 95 | } 96 | 97 | func TestForbidPatchStrategy(t *testing.T) { 98 | testdata := analysistest.TestData() 99 | 100 | a, err := conditions.Initializer().Init(config.LintersConfig{ 101 | Conditions: config.ConditionsConfig{ 102 | UsePatchStrategy: config.ConditionsUsePatchStrategyForbid, 103 | }, 104 | }) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | analysistest.RunWithSuggestedFixes(t, testdata, a, "f") 110 | } 111 | -------------------------------------------------------------------------------- /pkg/analysis/conditions/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | conditions is a linter that verifies that the conditions field within the struct is correctly defined. 19 | 20 | conditions fields in Kuberenetes API types are expected to be a slice of metav1.Condition. 21 | This linter verifies that the field is a slice of metav1.Condition and that it is correctly annotated with the required markers, 22 | and tags. 23 | 24 | The expected condition field should look like this: 25 | 26 | // +listType=map 27 | // +listMapKey=type 28 | // +patchStrategy=merge 29 | // +patchMergeKey=type 30 | // +optional 31 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` 32 | 33 | Where the tags and markers are incorrect, the linter will suggest fixes to improve the field definition. 34 | 35 | Conditions are also idiomatically the first item in the struct, the linter will highlight when the conditions field is not the first field in the struct. 36 | If this is not a desired behaviour, set the linter config option `isFirstField` to `Ignore`. 37 | 38 | Protobuf tags and patch strategy are required for in-tree API types, but not for CRDs. 39 | When linting CRD based types, set the `useProtobuf` and `usePatchStrategy` config option to `Ignore` or `Forbid`. 40 | */ 41 | package conditions 42 | -------------------------------------------------------------------------------- /pkg/analysis/conditions/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package conditions 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return newAnalyzer(cfg.Conditions), nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/conditions/testdata/src/c/a.go: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | import ( 4 | "go/ast" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type ValidConditions struct { 10 | // conditions is an accurate representation of the desired state of a conditions object. 11 | // +listType=map 12 | // +listMapKey=type 13 | // +patchStrategy=merge 14 | // +patchMergeKey=type 15 | // +optional 16 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` 17 | 18 | // other fields 19 | OtherField string `json:"otherField,omitempty"` 20 | } 21 | 22 | type ValidConditionsMissingProtobuf struct { 23 | // conditions is an accurate representation of the desired state of a conditions object. 24 | // +listType=map 25 | // +listMapKey=type 26 | // +patchStrategy=merge 27 | // +patchMergeKey=type 28 | // +optional 29 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` 30 | 31 | // other fields 32 | OtherField string `json:"otherField,omitempty"` 33 | } 34 | 35 | type ConditionsIncorrectType struct { 36 | // conditions has an incorrect type. 37 | Conditions map[string]metav1.Condition // want "Conditions field must be a slice of metav1.Condition" 38 | } 39 | 40 | type ConditionsIncorrectSliceElement struct { 41 | // conditions has an incorrect type. 42 | Conditions []string // want "Conditions field must be a slice of metav1.Condition" 43 | } 44 | 45 | type ConditionsIncorrectImportedSliceElement struct { 46 | // conditions has an incorrect type. 47 | Conditions []metav1.Time // want "Conditions field must be a slice of metav1.Condition" 48 | } 49 | 50 | type ConditionsIncorrectImportedPackage struct { 51 | // conditions has an incorrect type. 52 | Conditions []ast.Node // want "Conditions field must be a slice of metav1.Condition" 53 | } 54 | 55 | type MissingFieldTag struct { 56 | // conditions is missing the field tag. 57 | // +listType=map 58 | // +listMapKey=type 59 | // +patchStrategy=merge 60 | // +patchMergeKey=type 61 | // +optional 62 | Conditions []metav1.Condition // want "Conditions field is missing tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 63 | } 64 | 65 | type IncorrectFieldTag struct { 66 | // conditions has an incorrect field tag. 67 | // +listType=map 68 | // +listMapKey=type 69 | // +patchStrategy=merge 70 | // +patchMergeKey=type 71 | // +optional 72 | Conditions []metav1.Condition `json:"conditions" patchMergeKey:"type" protobuf:"bytes,3,rep,name=conditions"` // want "Conditions field has incorrect tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 73 | } 74 | -------------------------------------------------------------------------------- /pkg/analysis/conditions/testdata/src/c/a.go.golden: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | import ( 4 | "go/ast" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type ValidConditions struct { 10 | // conditions is an accurate representation of the desired state of a conditions object. 11 | // +listType=map 12 | // +listMapKey=type 13 | // +patchStrategy=merge 14 | // +patchMergeKey=type 15 | // +optional 16 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` 17 | 18 | // other fields 19 | OtherField string `json:"otherField,omitempty"` 20 | } 21 | 22 | type ValidConditionsMissingProtobuf struct { 23 | // conditions is an accurate representation of the desired state of a conditions object. 24 | // +listType=map 25 | // +listMapKey=type 26 | // +patchStrategy=merge 27 | // +patchMergeKey=type 28 | // +optional 29 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` 30 | 31 | // other fields 32 | OtherField string `json:"otherField,omitempty"` 33 | } 34 | 35 | type ConditionsIncorrectType struct { 36 | // conditions has an incorrect type. 37 | Conditions map[string]metav1.Condition // want "Conditions field must be a slice of metav1.Condition" 38 | } 39 | 40 | type ConditionsIncorrectSliceElement struct { 41 | // conditions has an incorrect type. 42 | Conditions []string // want "Conditions field must be a slice of metav1.Condition" 43 | } 44 | 45 | type ConditionsIncorrectImportedSliceElement struct { 46 | // conditions has an incorrect type. 47 | Conditions []metav1.Time // want "Conditions field must be a slice of metav1.Condition" 48 | } 49 | 50 | type ConditionsIncorrectImportedPackage struct { 51 | // conditions has an incorrect type. 52 | Conditions []ast.Node // want "Conditions field must be a slice of metav1.Condition" 53 | } 54 | 55 | type MissingFieldTag struct { 56 | // conditions is missing the field tag. 57 | // +listType=map 58 | // +listMapKey=type 59 | // +patchStrategy=merge 60 | // +patchMergeKey=type 61 | // +optional 62 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // want "Conditions field is missing tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 63 | } 64 | 65 | type IncorrectFieldTag struct { 66 | // conditions has an incorrect field tag. 67 | // +listType=map 68 | // +listMapKey=type 69 | // +patchStrategy=merge 70 | // +patchMergeKey=type 71 | // +optional 72 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // want "Conditions field has incorrect tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 73 | } 74 | -------------------------------------------------------------------------------- /pkg/analysis/conditions/testdata/src/d/a.go: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | import ( 4 | "go/ast" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type ValidConditions struct { 10 | // conditions is an accurate representation of the desired state of a conditions object. 11 | // +listType=map 12 | // +listMapKey=type 13 | // +patchStrategy=merge 14 | // +patchMergeKey=type 15 | // +optional 16 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` // want "Conditions field has incorrect tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 17 | 18 | // other fields 19 | OtherField string `json:"otherField,omitempty"` 20 | } 21 | 22 | type ValidConditionsMissingProtobuf struct { 23 | // conditions is an accurate representation of the desired state of a conditions object. 24 | // +listType=map 25 | // +listMapKey=type 26 | // +patchStrategy=merge 27 | // +patchMergeKey=type 28 | // +optional 29 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` 30 | 31 | // other fields 32 | OtherField string `json:"otherField,omitempty"` 33 | } 34 | 35 | type ConditionsIncorrectType struct { 36 | // conditions has an incorrect type. 37 | Conditions map[string]metav1.Condition // want "Conditions field must be a slice of metav1.Condition" 38 | } 39 | 40 | type ConditionsIncorrectSliceElement struct { 41 | // conditions has an incorrect type. 42 | Conditions []string // want "Conditions field must be a slice of metav1.Condition" 43 | } 44 | 45 | type ConditionsIncorrectImportedSliceElement struct { 46 | // conditions has an incorrect type. 47 | Conditions []metav1.Time // want "Conditions field must be a slice of metav1.Condition" 48 | } 49 | 50 | type ConditionsIncorrectImportedPackage struct { 51 | // conditions has an incorrect type. 52 | Conditions []ast.Node // want "Conditions field must be a slice of metav1.Condition" 53 | } 54 | 55 | type MissingFieldTag struct { 56 | // conditions is missing the field tag. 57 | // +listType=map 58 | // +listMapKey=type 59 | // +patchStrategy=merge 60 | // +patchMergeKey=type 61 | // +optional 62 | Conditions []metav1.Condition // want "Conditions field is missing tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 63 | } 64 | 65 | type IncorrectFieldTag struct { 66 | // conditions has an incorrect field tag. 67 | // +listType=map 68 | // +listMapKey=type 69 | // +patchStrategy=merge 70 | // +patchMergeKey=type 71 | // +optional 72 | Conditions []metav1.Condition `json:"conditions" patchMergeKey:"type" protobuf:"bytes,3,rep,name=conditions"` // want "Conditions field has incorrect tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 73 | } 74 | -------------------------------------------------------------------------------- /pkg/analysis/conditions/testdata/src/d/a.go.golden: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | import ( 4 | "go/ast" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type ValidConditions struct { 10 | // conditions is an accurate representation of the desired state of a conditions object. 11 | // +listType=map 12 | // +listMapKey=type 13 | // +patchStrategy=merge 14 | // +patchMergeKey=type 15 | // +optional 16 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // want "Conditions field has incorrect tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 17 | 18 | // other fields 19 | OtherField string `json:"otherField,omitempty"` 20 | } 21 | 22 | type ValidConditionsMissingProtobuf struct { 23 | // conditions is an accurate representation of the desired state of a conditions object. 24 | // +listType=map 25 | // +listMapKey=type 26 | // +patchStrategy=merge 27 | // +patchMergeKey=type 28 | // +optional 29 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` 30 | 31 | // other fields 32 | OtherField string `json:"otherField,omitempty"` 33 | } 34 | 35 | type ConditionsIncorrectType struct { 36 | // conditions has an incorrect type. 37 | Conditions map[string]metav1.Condition // want "Conditions field must be a slice of metav1.Condition" 38 | } 39 | 40 | type ConditionsIncorrectSliceElement struct { 41 | // conditions has an incorrect type. 42 | Conditions []string // want "Conditions field must be a slice of metav1.Condition" 43 | } 44 | 45 | type ConditionsIncorrectImportedSliceElement struct { 46 | // conditions has an incorrect type. 47 | Conditions []metav1.Time // want "Conditions field must be a slice of metav1.Condition" 48 | } 49 | 50 | type ConditionsIncorrectImportedPackage struct { 51 | // conditions has an incorrect type. 52 | Conditions []ast.Node // want "Conditions field must be a slice of metav1.Condition" 53 | } 54 | 55 | type MissingFieldTag struct { 56 | // conditions is missing the field tag. 57 | // +listType=map 58 | // +listMapKey=type 59 | // +patchStrategy=merge 60 | // +patchMergeKey=type 61 | // +optional 62 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // want "Conditions field is missing tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 63 | } 64 | 65 | type IncorrectFieldTag struct { 66 | // conditions has an incorrect field tag. 67 | // +listType=map 68 | // +listMapKey=type 69 | // +patchStrategy=merge 70 | // +patchMergeKey=type 71 | // +optional 72 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` // want "Conditions field has incorrect tags, should be: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`" 73 | } 74 | -------------------------------------------------------------------------------- /pkg/analysis/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | analysis providers a linter registry and a set of linters that can be used to analyze Go code. 19 | The linters in this package are focused on Kubernetes API types and implmement API conventions 20 | and best practices. 21 | 22 | To use the linters provided by KAL, initialise an instance of the Registry and then initialize 23 | the linters within, by passing the required configuration. 24 | 25 | Example: 26 | 27 | registry := analysis.NewRegistry() 28 | 29 | // Initialize the linters 30 | linters, err := registry.InitLinters( 31 | config.Linters{ 32 | Enabled: []string{ 33 | "commentstart" 34 | "jsontags", 35 | "optionalorrequired", 36 | }, 37 | Disabled: []string{ 38 | ... 39 | }, 40 | }, 41 | config.LintersConfig{ 42 | JSONTags: config.JSONTagsConfig{ 43 | JSONTagRegex: `^[-_a-zA-Z0-9]+$`, 44 | }, 45 | OptionalOrRequired: config.OptionalOrRequiredConfig{ 46 | PreferredOptionalMarker: optionalorrequired.OptionalMarker, 47 | PreferredRequiredMarker: optionalorrequired.RequiredMarker, 48 | }, 49 | }, 50 | ) 51 | 52 | The provided list of analyzers can be used with `multichecker.Main()` from the `golang.org/x/tools/go/analysis/multichecker` package, 53 | or as part of a custom analysis pipeline, eg via the golangci-lint plugin system. 54 | 55 | Linters provided by KAL: 56 | - [commentstart]: Linter to ensure that comments start with the serialized version of the field name. 57 | - [jsontags]: Linter to ensure that JSON tags are present on struct fields, and that they match a given regex. 58 | - [optionalorrequired]: Linter to ensure that all fields are marked as either optional or required. 59 | 60 | When adding new linters, ensure that they are added to the registry in the `NewRegistry` function. 61 | Linters should not depend on other linters, unless that linter has no configuration and is always enabled, 62 | see the helpers package. 63 | 64 | Any common, or shared functionality to extract data from types, should be added as a helper function in the helpers package. 65 | The available helpers are: 66 | - extractjsontags: Extracts JSON tags from struct fields and returns the information in a structured format. 67 | - markers: Extracts marker information from types and returns the information in a structured format. 68 | */ 69 | package analysis 70 | -------------------------------------------------------------------------------- /pkg/analysis/duplicatemarkers/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package duplicatemarkers_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers" 23 | ) 24 | 25 | func Test(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | analysistest.RunWithSuggestedFixes(t, testdata, duplicatemarkers.Analyzer, "a") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/analysis/duplicatemarkers/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | duplicatemarkers is an analyzer that checks for duplicate markers in the API types. 19 | It reports exact matches for marker definitions. 20 | 21 | For example, something like: 22 | 23 | type Foo struct { 24 | // +kubebuilder:validation:MaxLength=10 25 | // +kubebuilder:validation:MaxLength=11 26 | type Bar string 27 | } 28 | 29 | would not be reported while something like: 30 | 31 | type Foo struct { 32 | // +kubebuilder:validation:MaxLength=10 33 | // +kubebuilder:validation:MaxLength=10 34 | type Bar string 35 | } 36 | 37 | would be reported. 38 | 39 | This linter also be able to automatically fix all markers that are exact match to another markers. 40 | If there are duplicates across fields and their underlying type, the marker on the type will be preferred and the marker on the field will be removed. 41 | */ 42 | package duplicatemarkers 43 | -------------------------------------------------------------------------------- /pkg/analysis/duplicatemarkers/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package duplicatemarkers 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return Analyzer, nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | // Duplicated markers are a sign of bad code, and should be avoided. 45 | // This is a good rule to have on by default. 46 | return true 47 | } 48 | -------------------------------------------------------------------------------- /pkg/analysis/duplicatemarkers/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | // +kubebuilder:validation:MaxLength=10 4 | type StringFromAnotherFile string 5 | -------------------------------------------------------------------------------- /pkg/analysis/errors/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package errors 17 | 18 | import "errors" 19 | 20 | var ( 21 | // ErrCouldNotCreateMarkers is returned when the markers could not be created. 22 | ErrCouldNotCreateMarkers = errors.New("could not create markers") 23 | 24 | // ErrCouldNotCreateStructFieldTags is returned when the struct field tags could not be created. 25 | ErrCouldNotCreateStructFieldTags = errors.New("could not create new structFieldTags") 26 | 27 | // ErrCouldNotGetInspector is returned when the inspector could not be retrieved. 28 | ErrCouldNotGetInspector = errors.New("could not get inspector") 29 | 30 | // ErrCouldNotGetMarkers is returned when the markers analyzer could not be retrieved. 31 | ErrCouldNotGetMarkers = errors.New("could not get markers") 32 | 33 | // ErrCouldNotGetJSONTags is returned when the JSON tags could not be retrieved. 34 | ErrCouldNotGetJSONTags = errors.New("could not get json tags") 35 | ) 36 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | helpers contains utility functions that are used by the analysis package. 19 | The helpers are used to extract data from the types, and provide common functionality that is used by multiple linters. 20 | 21 | The available helpers are: 22 | - [extractjsontags]: Extracts JSON tags from struct fields and returns the information in a structured format. 23 | - [markers]: Extracts marker information from types and returns the information in a structured format. 24 | 25 | Helpers should expose an *analysis.Analyzer as a globabl variable. 26 | Other linters will use the `Requires` configuration to ensure that the helper is run before the linter. 27 | The linter `Requires` relies on matching pointers to Analyzers, and therefore the helper cannot be dynamically created. 28 | */ 29 | package helpers 30 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/extractjsontags/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | extractjsontags is a helper package that extracts JSON tags from a struct field. 19 | 20 | It returns data behind the interface [StructFieldTags] which is used to find information about JSON tags on fields within a struct. 21 | 22 | Data about json tags, for a field within a struct can be accessed by calling the `FieldTags` method on the interface. 23 | 24 | Example: 25 | 26 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 27 | jsonTags := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags) 28 | 29 | // Filter to fields so that we can iterate over fields in a struct. 30 | nodeFilter := []ast.Node{ 31 | (*ast.Field)(nil), 32 | } 33 | 34 | inspect.Preorder(nodeFilter, func(n ast.Node) { 35 | field, ok := n.(*ast.Field) 36 | if !ok { 37 | return 38 | } 39 | 40 | tagInfo := jsonTags.FieldTags(field) 41 | 42 | ... 43 | 44 | }) 45 | 46 | For each field, tag information is returned as a [FieldTagInfo] struct. 47 | This can be used to determine the name of the field, as per the json tag, whether the 48 | field is inline, has omitempty or is missing completely. 49 | */ 50 | package extractjsontags 51 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/inspector/analyzer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package inspector 17 | 18 | import ( 19 | "reflect" 20 | 21 | "golang.org/x/tools/go/analysis" 22 | astinspector "golang.org/x/tools/go/ast/inspector" 23 | 24 | kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" 25 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" 26 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" 27 | ) 28 | 29 | const name = "inspector" 30 | 31 | // Analyzer is the analyzer for the inspector package. 32 | // It provides common functionality for analyzers that need to inspect fields and struct. 33 | // Abstracting away filtering of fields that the analyzers should and shouldn't be worrying about. 34 | var Analyzer = &analysis.Analyzer{ 35 | Name: name, 36 | Doc: "Provides common functionality for analyzers that need to inspect fields and struct", 37 | Run: run, 38 | Requires: []*analysis.Analyzer{extractjsontags.Analyzer, markers.Analyzer}, 39 | ResultType: reflect.TypeOf(newInspector(nil, nil, nil)), 40 | } 41 | 42 | func run(pass *analysis.Pass) (any, error) { 43 | astInspector := astinspector.New(pass.Files) 44 | 45 | jsonTags, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags) 46 | if !ok { 47 | return nil, kalerrors.ErrCouldNotGetJSONTags 48 | } 49 | 50 | markersAccess, ok := pass.ResultOf[markers.Analyzer].(markers.Markers) 51 | if !ok { 52 | return nil, kalerrors.ErrCouldNotGetMarkers 53 | } 54 | 55 | return newInspector(astInspector, jsonTags, markersAccess), nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/inspector/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package inspector_test 17 | 18 | import ( 19 | "errors" 20 | "go/ast" 21 | "testing" 22 | 23 | "golang.org/x/tools/go/analysis" 24 | "golang.org/x/tools/go/analysis/analysistest" 25 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" 26 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" 27 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" 28 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 29 | ) 30 | 31 | func TestInspector(t *testing.T) { 32 | testdata := analysistest.TestData() 33 | 34 | analysistest.Run(t, testdata, testAnalyzer, "a") 35 | } 36 | 37 | var errCouldNotGetInspector = errors.New("could not get inspector") 38 | 39 | var testAnalyzer = &analysis.Analyzer{ 40 | Name: "test", 41 | Doc: "tests the inspector analyzer", 42 | Run: run, 43 | Requires: []*analysis.Analyzer{inspector.Analyzer}, 44 | } 45 | 46 | func run(pass *analysis.Pass) (any, error) { 47 | inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) 48 | if !ok { 49 | return nil, errCouldNotGetInspector 50 | } 51 | 52 | inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) { 53 | pass.Reportf(field.Pos(), "field: %v", utils.FieldName(field)) 54 | 55 | if jsonTagInfo.Name != "" { 56 | pass.Reportf(field.Pos(), "json tag: %v", jsonTagInfo.Name) 57 | } 58 | }) 59 | 60 | return nil, nil //nolint:nilnil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/inspector/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | inspector is a helper package that iterates over fields in structs, calling an inspection function on fields 19 | that should be considered for analysis. 20 | 21 | The inspector extracts common logic of iterating and filtering through struct fields, so that analyzers 22 | need not re-implement the same filtering over and over. 23 | 24 | For example, the inspector filters out struct definitions that are not type declarations, and fields that are ignored. 25 | 26 | Example: 27 | 28 | type A struct { 29 | // This field is included in the analysis. 30 | Field string `json:"field"` 31 | 32 | // This field, and the fields within are ignored due to the json tag. 33 | F struct { 34 | Field string `json:"field"` 35 | } `json:"-"` 36 | } 37 | 38 | // Any struct defined within a function is ignored. 39 | func Foo() { 40 | type Bar struct { 41 | Field string 42 | } 43 | } 44 | 45 | // All fields within interface declarations are ignored. 46 | type Bar interface { 47 | Name() string 48 | } 49 | */ 50 | package inspector 51 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/inspector/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package A 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | var ( 8 | String string 9 | ) 10 | 11 | const ( 12 | Int int = 0 13 | ) 14 | 15 | type A struct { 16 | Field string `json:"field"` // want "field: Field" "json tag: field" 17 | 18 | B `json:"b"` // want "field: B" "json tag: b" 19 | 20 | C `json:",inline"` // want "field: C" 21 | 22 | D `json:"-"` 23 | 24 | E struct { // want "field: E" "json tag: e" 25 | Field string `json:"field"` // want "field: Field" "json tag: field" 26 | } `json:"e"` 27 | 28 | F struct { 29 | Field string `json:"field"` 30 | } `json:"-"` 31 | } 32 | 33 | func (A) DoNothing() {} 34 | 35 | type B struct { 36 | Field string `json:"field"` // want "field: Field" "json tag: field" 37 | } 38 | 39 | type ( 40 | C struct { 41 | Field string `json:"field"` // want "field: Field" "json tag: field" 42 | } 43 | 44 | D struct { 45 | Field string `json:"field"` // want "field: Field" "json tag: field" 46 | } 47 | ) 48 | 49 | func Foo() { 50 | type Bar struct { 51 | Field string 52 | } 53 | } 54 | 55 | type Bar interface { 56 | Name() string 57 | } 58 | 59 | var Var = struct { 60 | Field string 61 | }{ 62 | Field: "field", 63 | } 64 | 65 | // AItems is a list of A. 66 | // This represents the Items types in Kubernetes APIs. 67 | // We don't need to lint this type as it doesn't affect the API behaviour in general. 68 | type AItems struct { 69 | metav1.TypeMeta `json:",inline"` 70 | metav1.ListMeta `json:"metadata,omitempty"` 71 | 72 | Items []A `json:"items"` 73 | } 74 | 75 | type NotItems struct { 76 | metav1.TypeMeta `json:",inline"` // want "field: " 77 | metav1.ListMeta `json:"metadata,omitempty"` // want "field: " "json tag: metadata" 78 | 79 | Items A `json:"items"` // want "field: Items" "json tag: items" 80 | } 81 | 82 | type NotItemsWrongMetadata struct { 83 | metav1.TypeMeta `json:",inline"` // want "field: " 84 | metav1.ObjectMeta `json:"metadata,omitempty"` // want "field: " "json tag: metadata" 85 | 86 | Items []A `json:"items"` // want "field: Items" "json tag: items" 87 | } 88 | 89 | type NotItemsWrongTypeMeta struct { 90 | metav1.ObjectMeta `json:",inline"` // want "field: " 91 | metav1.ListMeta `json:"metadata,omitempty"` // want "field: " "json tag: metadata" 92 | 93 | Items []A `json:"items"` // want "field: Items" "json tag: items" 94 | } 95 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/inspector/testdata/src/k8s.io/apimachinery/pkg/types/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | This is a copy of the minimum amount of the original file to be able to test the inspector linter. 3 | */ 4 | package types 5 | 6 | // UID is a type that holds unique ID values, including UUIDs. Because we 7 | // don't ONLY use UUIDs, this is an alias to string. Being a type captures 8 | // intent and helps make sure that UIDs and names do not get conflated. 9 | type UID string 10 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/markers/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package markers 17 | 18 | import ( 19 | "testing" 20 | 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | func TestExtractMarkerIdAndExpressions(t *testing.T) { 25 | type testcase struct { 26 | name string 27 | marker string 28 | expectedID string 29 | expectedExpressions map[string]string 30 | } 31 | 32 | testcases := []testcase{ 33 | { 34 | name: "registered marker with single unnamed expression using '='", 35 | marker: "kubebuilder:object:root=true", 36 | expectedID: "kubebuilder:object:root", 37 | expectedExpressions: map[string]string{ 38 | "": "true", 39 | }, 40 | }, 41 | { 42 | name: "registered marker with single unnamed expression using ':='", 43 | marker: "kubebuilder:object:root:=true", 44 | expectedID: "kubebuilder:object:root", 45 | expectedExpressions: map[string]string{ 46 | "": "true", 47 | }, 48 | }, 49 | { 50 | name: "registered marker with no expressions", 51 | marker: "required", 52 | expectedID: "required", 53 | expectedExpressions: map[string]string{}, 54 | }, 55 | { 56 | name: "registered marker with multiple named expressions", 57 | marker: "kubebuilder:validation:XValidation:rule='has(self.field)',message='must have field!'", 58 | expectedID: "kubebuilder:validation:XValidation", 59 | expectedExpressions: map[string]string{ 60 | "rule": "'has(self.field)'", 61 | "message": "'must have field!'", 62 | }, 63 | }, 64 | { 65 | name: " unregistered marker with expression wrapped in double quotes (\")", 66 | marker: "foo:bar:rule=\"foo\"", 67 | expectedID: "foo:bar:rule", 68 | expectedExpressions: map[string]string{ 69 | "": "\"foo\"", 70 | }, 71 | }, 72 | } 73 | 74 | for _, tc := range testcases { 75 | t.Run(tc.name, func(t *testing.T) { 76 | g := NewWithT(t) 77 | 78 | reg := NewRegistry() 79 | reg.Register("kubebuilder:object:root", "required", "kubebuilder:validation:XValidation") 80 | 81 | id, expressions := extractMarkerIDAndExpressions(reg, tc.marker) 82 | 83 | g.Expect(id).To(Equal(tc.expectedID), "marker", tc.marker) 84 | g.Expect(expressions).To(Equal(tc.expectedExpressions), "marker", tc.marker) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/markers/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | markers is a helper used to extract marker information from types. 19 | A marker is a comment line preceded with `+` that indicates to a generator something about the field or type. 20 | 21 | The package returns a [Markers] interface, which can be used to access markers associated with a struct or a field within a struct. 22 | 23 | Example: 24 | 25 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 26 | markersAccess := pass.ResultOf[markers.Analyzer].(markers.Markers) 27 | 28 | // Filter to structs so that we can iterate over fields in a struct. 29 | nodeFilter := []ast.Node{ 30 | (*ast.StructType)(nil), 31 | } 32 | 33 | inspect.Preorder(nodeFilter, func(n ast.Node) { 34 | sTyp, ok := n.(*ast.StructType) 35 | if !ok { 36 | return 37 | } 38 | 39 | if sTyp.Fields == nil { 40 | return 41 | } 42 | 43 | for _, field := range sTyp.Fields.List { 44 | if field == nil || len(field.Names) == 0 { 45 | continue 46 | } 47 | 48 | structMarkers := markersAccess.StructMarkers(sTyp) 49 | fieldMarkers := markersAccess.FieldMarkers(field) 50 | 51 | ... 52 | } 53 | }) 54 | 55 | The result of StructMarkers or StructFieldMarkers is a [MarkerSet] which can be used to determine the presence of a marker, and the value of the marker. 56 | The MarkerSet is indexed based on the value of the marker, once the prefix `+` is removed. 57 | 58 | Additional information about the marker can be found in the [Marker] struct, for each marker on the field. 59 | 60 | Example: 61 | 62 | fieldMarkers := markersAccess.FieldMarkers(field) 63 | 64 | if fieldMarkers.Has("required") { 65 | requiredMarker := fieldMarkers["required"] 66 | ... 67 | } 68 | */ 69 | package markers 70 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/markers/registry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package markers 17 | 18 | import ( 19 | "strings" 20 | "sync" 21 | 22 | "k8s.io/apimachinery/pkg/util/sets" 23 | ) 24 | 25 | // Registry is a thread-safe set of known marker identifiers. 26 | type Registry interface { 27 | // Register adds the provided identifiers to the Registry. 28 | Register(ids ...string) 29 | 30 | // Match performs a greedy match to determine if an input 31 | // marker string matches a known marker identifier. It returns 32 | // the matched identifier and a boolean representing if a match was 33 | // found. If no match is found, the returned identifier will be an 34 | // empty string. 35 | Match(in string) (string, bool) 36 | } 37 | 38 | var defaultRegistry = NewRegistry() //nolint:gochecknoglobals 39 | 40 | // DefaultRegistry is a global registry for known markers. 41 | // New linters should register the markers they care about during 42 | // an init() function. 43 | func DefaultRegistry() Registry { 44 | return defaultRegistry 45 | } 46 | 47 | // Registry is a thread-safe set of known marker identifiers. 48 | type registry struct { 49 | identifiers sets.Set[string] 50 | mu sync.Mutex 51 | } 52 | 53 | // NewRegistry creates a new Registry. 54 | func NewRegistry() Registry { 55 | return ®istry{ 56 | identifiers: sets.New[string](), 57 | mu: sync.Mutex{}, 58 | } 59 | } 60 | 61 | // Register adds the provided identifiers to the Registry. 62 | func (r *registry) Register(ids ...string) { 63 | r.mu.Lock() 64 | defer r.mu.Unlock() 65 | r.identifiers.Insert(ids...) 66 | } 67 | 68 | // Match performs a greedy match to determine if an input 69 | // marker string matches a known marker identifier. It returns 70 | // the matched identifier and a boolean representing if a match was 71 | // found. If no match is found, the returned identifier will be an 72 | // empty string. 73 | func (r *registry) Match(in string) (string, bool) { 74 | r.mu.Lock() 75 | defer r.mu.Unlock() 76 | 77 | // If there is an exact match, return early. 78 | // This is likely when using markers with no expressions like 79 | // optional, required, kubebuilder:validation:Required, etc. 80 | if ok := r.identifiers.Has(in); ok { 81 | return in, true 82 | } 83 | 84 | // Look for the longest matching known identifier 85 | bestMatch := "" 86 | foundMatch := false 87 | 88 | for _, id := range r.identifiers.UnsortedList() { 89 | if strings.HasPrefix(in, id) { 90 | if len(bestMatch) < len(id) { 91 | bestMatch = id 92 | foundMatch = true 93 | } 94 | } 95 | } 96 | 97 | return bestMatch, foundMatch 98 | } 99 | -------------------------------------------------------------------------------- /pkg/analysis/helpers/markers/registry_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package markers 17 | 18 | import ( 19 | "testing" 20 | 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | func TestRegistryMatch(t *testing.T) { 25 | type testcase struct { 26 | name string 27 | marker string 28 | registeredMarkers []string 29 | expectedID string 30 | expectedMatch bool 31 | } 32 | 33 | testcases := []testcase{ 34 | { 35 | name: "one marker registered, marker matches", 36 | registeredMarkers: []string{ 37 | "kubebuilder:object:root", 38 | }, 39 | marker: "kubebuilder:object:root=true", 40 | expectedID: "kubebuilder:object:root", 41 | expectedMatch: true, 42 | }, 43 | { 44 | name: "multiple markers registered, matches longest registered entry", 45 | registeredMarkers: []string{ 46 | "kubebuilder:validation:XValidation", 47 | "kubebuilder:validation", 48 | }, 49 | marker: "kubebuilder:validation:XValidation:rule='foo'", 50 | expectedID: "kubebuilder:validation:XValidation", 51 | expectedMatch: true, 52 | }, 53 | { 54 | name: "multiple markers registered, no matches", 55 | registeredMarkers: []string{ 56 | "kubebuilder:validation:XValidation", 57 | "kubebuilder:validation", 58 | }, 59 | marker: "kubebuilder:notreal:foo", 60 | expectedID: "", 61 | expectedMatch: false, 62 | }, 63 | { 64 | name: "marker registered, exact match", 65 | registeredMarkers: []string{ 66 | "optional", 67 | }, 68 | marker: "optional", 69 | expectedID: "optional", 70 | expectedMatch: true, 71 | }, 72 | } 73 | 74 | for _, tc := range testcases { 75 | t.Run(tc.name, func(t *testing.T) { 76 | g := NewWithT(t) 77 | registry := NewRegistry() 78 | registry.Register(tc.registeredMarkers...) 79 | 80 | id, ok := registry.Match(tc.marker) 81 | 82 | g.Expect(id).To(Equal(tc.expectedID)) 83 | g.Expect(ok).To(Equal(tc.expectedMatch)) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pkg/analysis/integers/analyzer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package integers 17 | 18 | import ( 19 | "go/ast" 20 | 21 | "golang.org/x/tools/go/analysis" 22 | "golang.org/x/tools/go/analysis/passes/inspect" 23 | "golang.org/x/tools/go/ast/inspector" 24 | kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" 25 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 26 | ) 27 | 28 | const name = "integers" 29 | 30 | // Analyzer is the analyzer for the integers package. 31 | // It checks that no struct fields or type aliases are `int`, or unsigned integers. 32 | var Analyzer = &analysis.Analyzer{ 33 | Name: name, 34 | Doc: "All integers should be explicit about their size, int32 and int64 should be used over plain int. Unsigned ints are not allowed.", 35 | Run: run, 36 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 37 | } 38 | 39 | func run(pass *analysis.Pass) (any, error) { 40 | inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 41 | if !ok { 42 | return nil, kalerrors.ErrCouldNotGetInspector 43 | } 44 | 45 | // Filter to fields so that we can look at fields within structs. 46 | // Filter typespecs so that we can look at type aliases. 47 | nodeFilter := []ast.Node{ 48 | (*ast.StructType)(nil), 49 | (*ast.TypeSpec)(nil), 50 | } 51 | 52 | typeChecker := utils.NewTypeChecker(checkIntegers) 53 | 54 | // Preorder visits all the nodes of the AST in depth-first order. It calls 55 | // f(n) for each node n before it visits n's children. 56 | // 57 | // We use the filter defined above, ensuring we only look at struct fields and type declarations. 58 | inspect.Preorder(nodeFilter, func(n ast.Node) { 59 | typeChecker.CheckNode(pass, n) 60 | }) 61 | 62 | return nil, nil //nolint:nilnil 63 | } 64 | 65 | // checkIntegers looks for known type of integers that do not match the allowed `int32` or `int64` requirements. 66 | func checkIntegers(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { 67 | switch ident.Name { 68 | case "int32", "int64": 69 | // Valid cases 70 | case "int", "int8", "int16": 71 | pass.Reportf(node.Pos(), "%s should not use an int, int8 or int16. Use int32 or int64 depending on bounding requirements", prefix) 72 | case "uint", "uint8", "uint16", "uint32", "uint64": 73 | pass.Reportf(node.Pos(), "%s should not use unsigned integers, use only int32 or int64 and apply validation to ensure the value is positive", prefix) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/analysis/integers/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package integers_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/integers" 23 | ) 24 | 25 | func Test(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | analysistest.Run(t, testdata, integers.Analyzer, "a") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/analysis/integers/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | integers is an analyzer that checks for usage of unsupported integer types. 19 | 20 | According to the API conventions, only int32 and int64 types should be used in Kubernetes APIs. 21 | 22 | int32 is preferred and should be used in most cases, unless the use case requireds representing 23 | values larger than int32. 24 | 25 | It also states that unsigned integers should be replaced with signed integers, and then numeric 26 | lower bounds added to prevent negative integers. 27 | 28 | Succinctly this anaylzer checks for int, int8, int16, uint, uint8, uint16, uint32 and uint64 types 29 | and highlights that they should not be used. 30 | */ 31 | package integers 32 | -------------------------------------------------------------------------------- /pkg/analysis/integers/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package integers 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return Analyzer, nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/integers/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | // IntB is being used to show a type from a different file. 4 | type IntB int // want "type IntB should not use an int, int8 or int16. Use int32 or int64 depending on bounding requirements" 5 | 6 | type InvalidSliceIntAliasB []int // want "type InvalidSliceIntAliasB array element should not use an int, int8 or int16. Use int32 or int64 depending on bounding requirements" 7 | -------------------------------------------------------------------------------- /pkg/analysis/jsontags/analyzer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package jsontags 17 | 18 | import ( 19 | "fmt" 20 | "go/ast" 21 | "regexp" 22 | 23 | kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" 24 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" 25 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" 26 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" 27 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 28 | "sigs.k8s.io/kube-api-linter/pkg/config" 29 | 30 | "golang.org/x/tools/go/analysis" 31 | ) 32 | 33 | const ( 34 | // camelCaseRegex is a regular expression that matches camel case strings. 35 | camelCaseRegex = "^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$" 36 | 37 | name = "jsontags" 38 | ) 39 | 40 | type analyzer struct { 41 | jsonTagRegex *regexp.Regexp 42 | } 43 | 44 | // newAnalyzer creates a new analyzer with the given json tag regex. 45 | func newAnalyzer(cfg config.JSONTagsConfig) (*analysis.Analyzer, error) { 46 | defaultConfig(&cfg) 47 | 48 | jsonTagRegex, err := regexp.Compile(cfg.JSONTagRegex) 49 | if err != nil { 50 | return nil, fmt.Errorf("could not compile json tag regex: %w", err) 51 | } 52 | 53 | a := &analyzer{ 54 | jsonTagRegex: jsonTagRegex, 55 | } 56 | 57 | return &analysis.Analyzer{ 58 | Name: name, 59 | Doc: "Check that all struct fields in an API are tagged with json tags", 60 | Run: a.run, 61 | Requires: []*analysis.Analyzer{inspector.Analyzer}, 62 | }, nil 63 | } 64 | 65 | func (a *analyzer) run(pass *analysis.Pass) (any, error) { 66 | inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) 67 | if !ok { 68 | return nil, kalerrors.ErrCouldNotGetInspector 69 | } 70 | 71 | inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) { 72 | a.checkField(pass, field, jsonTagInfo) 73 | }) 74 | 75 | return nil, nil //nolint:nilnil 76 | } 77 | 78 | func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo) { 79 | prefix := "field %s" 80 | if len(field.Names) == 0 || field.Names[0] == nil { 81 | prefix = "embedded field %s" 82 | } 83 | 84 | prefix = fmt.Sprintf(prefix, utils.FieldName(field)) 85 | 86 | if tagInfo.Missing { 87 | pass.Reportf(field.Pos(), "%s is missing json tag", prefix) 88 | return 89 | } 90 | 91 | if tagInfo.Inline { 92 | return 93 | } 94 | 95 | if tagInfo.Name == "" { 96 | pass.Reportf(field.Pos(), "%s has empty json tag", prefix) 97 | return 98 | } 99 | 100 | matched := a.jsonTagRegex.Match([]byte(tagInfo.Name)) 101 | if !matched { 102 | pass.Reportf(field.Pos(), "%s json tag does not match pattern %q: %s", prefix, a.jsonTagRegex.String(), tagInfo.Name) 103 | } 104 | } 105 | 106 | func defaultConfig(cfg *config.JSONTagsConfig) { 107 | if cfg.JSONTagRegex == "" { 108 | cfg.JSONTagRegex = camelCaseRegex 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/analysis/jsontags/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package jsontags_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags" 23 | "sigs.k8s.io/kube-api-linter/pkg/config" 24 | ) 25 | 26 | func TestDefaultConfiguration(t *testing.T) { 27 | testdata := analysistest.TestData() 28 | 29 | a, err := jsontags.Initializer().Init(config.LintersConfig{}) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | analysistest.Run(t, testdata, a, "a") 35 | } 36 | 37 | func TestAlternativeRegex(t *testing.T) { 38 | testdata := analysistest.TestData() 39 | 40 | a, err := jsontags.Initializer().Init(config.LintersConfig{ 41 | JSONTags: config.JSONTagsConfig{ 42 | JSONTagRegex: "^[a-z][a-z]*(?:[A-Z][a-z0-9]+)*[a-z0-9]?$", 43 | }, 44 | }) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | analysistest.Run(t, testdata, a, "b") 50 | } 51 | -------------------------------------------------------------------------------- /pkg/analysis/jsontags/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | jsontags provides a linter to ensure that JSON tags are present on struct fields, and that they match a given regex. 19 | 20 | Kubernetes API types should have JSON tags on all fields, to ensure that the fields are correctly serialized and deserialized. 21 | The JSON tags should be camelCase, with a lower case first letter, and should match the field name in all but capitalization. 22 | There should be no hyphens or underscores in either the field name or the JSON tag. 23 | 24 | The linter can be configured with a regex to match the JSON tags against. 25 | By default, the regex is `^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$`, which allows for camelCase JSON tags, with consecutive capital letters, 26 | to allow, for example, for fields like `requestTTLSeconds`. 27 | 28 | To disallow consecutive capital letters, the regex can be set to `^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*$`. 29 | The regex can be configured with the JSONTagRegex field in the JSONTagsConfig struct. 30 | */ 31 | package jsontags 32 | -------------------------------------------------------------------------------- /pkg/analysis/jsontags/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package jsontags 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return newAnalyzer(cfg.JSONTags) 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/jsontags/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | type JSONTagTestStruct struct { 8 | NoJSONTag string // want "field NoJSONTag is missing json tag" 9 | EmptyJSONTag string `json:""` // want "field EmptyJSONTag has empty json tag" 10 | NonCamelCaseJSONTag string `json:"non_camel_case_json_tag"` // want "field NonCamelCaseJSONTag json tag does not match pattern \"\\^\\[a-z\\]\\[a-z0-9\\]\\*\\(\\?:\\[A-Z\\]\\[a-z0-9\\]\\*\\)\\*\\$\": non_camel_case_json_tag" 11 | WithHyphensJSONTag string `json:"with-hyphens-json-tag"` // want "field WithHyphensJSONTag json tag does not match pattern \"\\^\\[a-z\\]\\[a-z0-9\\]\\*\\(\\?:\\[A-Z\\]\\[a-z0-9\\]\\*\\)\\*\\$\": with-hyphens-json-tag" 12 | PascalCaseJSONTag string `json:"PascalCaseJSONTag"` // want "field PascalCaseJSONTag json tag does not match pattern \"\\^\\[a-z\\]\\[a-z0-9\\]\\*\\(\\?:\\[A-Z\\]\\[a-z0-9\\]\\*\\)\\*\\$\": PascalCaseJSONTag" 13 | NonTerminatedJSONTag string `json:"nonTerminatedJSONTag` // want "field NonTerminatedJSONTag is missing json tag" 14 | XMLTag string `xml:"xmlTag"` // want "field XMLTag is missing json tag" 15 | InlineJSONTag string `json:",inline"` 16 | ValidJSONTag string `json:"validJsonTag"` 17 | ValidOptionalJSONTag string `json:"validOptionalJsonTag,omitempty"` 18 | JSONTagWithID string `json:"jsonTagWithID"` 19 | JSONTagWithTTL string `json:"jsonTagWithTTL"` 20 | JSONTagWithGiB string `json:"jsonTagWithGiB"` 21 | Ignored string `json:"-"` 22 | 23 | IgnoredAnonymousStruct struct { 24 | // This field should be ignored since the parent field is ignored. 25 | A string `json:""` 26 | } `json:"-"` 27 | 28 | A `json:",inline"` 29 | B `json:"bar,omitempty"` 30 | C // want "embedded field C is missing json tag" 31 | D `json:""` // want "embedded field D has empty json tag" 32 | E `json:"e-"` // want "embedded field E json tag does not match pattern \"\\^\\[a-z\\]\\[a-z0-9\\]\\*\\(\\?:\\[A-Z\\]\\[a-z0-9\\]\\*\\)\\*\\$\": e-" 33 | 34 | metav1.TypeMeta `json:",inline"` 35 | metav1.ObjectMeta `json:"metadata,omitempty"` 36 | } 37 | 38 | type A struct{} 39 | 40 | func (A) DoNothing() {} 41 | 42 | type B struct{} 43 | 44 | type C struct{} 45 | 46 | type D struct{} 47 | 48 | type E struct{} 49 | 50 | type Interface interface { 51 | InaccessibleFunction() string 52 | } 53 | -------------------------------------------------------------------------------- /pkg/analysis/jsontags/testdata/src/b/b.go: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | type JSONTagTestStruct struct { 4 | NoJSONTag string // want "field NoJSONTag is missing json tag" 5 | EmptyJSONTag string `json:""` // want "field EmptyJSONTag has empty json tag" 6 | NonCamelCaseJSONTag string `json:"non_camel_case_json_tag"` // want "field NonCamelCaseJSONTag json tag does not match pattern \"\\^\\[a-z\\]\\[a-z\\]\\*\\(\\?\\:\\[A-Z\\]\\[a-z0-9\\]\\+\\)\\*\\[a-z0-9\\]\\?\\$\": non_camel_case_json_tag" 7 | WithHyphensJSONTag string `json:"with-hyphens-json-tag"` // want "field WithHyphensJSONTag json tag does not match pattern \"\\^\\[a-z\\]\\[a-z\\]\\*\\(\\?\\:\\[A-Z\\]\\[a-z0-9\\]\\+\\)\\*\\[a-z0-9\\]\\?\\$\": with-hyphens-json-tag" 8 | PascalCaseJSONTag string `json:"PascalCaseJSONTag"` // want "field PascalCaseJSONTag json tag does not match pattern \"\\^\\[a-z\\]\\[a-z\\]\\*\\(\\?\\:\\[A-Z\\]\\[a-z0-9\\]\\+\\)\\*\\[a-z0-9\\]\\?\\$\": PascalCaseJSONTag" 9 | NonTerminatedJSONTag string `json:"nonTerminatedJSONTag` // want "field NonTerminatedJSONTag is missing json tag" 10 | XMLTag string `xml:"xmlTag"` // want "field XMLTag is missing json tag" 11 | InlineJSONTag string `json:",inline"` 12 | ValidJSONTag string `json:"validJsonTag"` 13 | ValidOptionalJSONTag string `json:"validOptionalJsonTag,omitempty"` 14 | JSONTagWithID string `json:"jsonTagWithID"` // want "field JSONTagWithID json tag does not match pattern \"\\^\\[a-z\\]\\[a-z\\]\\*\\(\\?\\:\\[A-Z\\]\\[a-z0-9\\]\\+\\)\\*\\[a-z0-9\\]\\?\\$\": jsonTagWithID" 15 | JSONTagWithTTL string `json:"jsonTagWithTTL"` // want "field JSONTagWithTTL json tag does not match pattern \"\\^\\[a-z\\]\\[a-z\\]\\*\\(\\?\\:\\[A-Z\\]\\[a-z0-9\\]\\+\\)\\*\\[a-z0-9\\]\\?\\$\": jsonTagWithTTL" 16 | JSONTagWithGiB string `json:"jsonTagWithGiB"` // want "field JSONTagWithGiB json tag does not match pattern \"\\^\\[a-z\\]\\[a-z\\]\\*\\(\\?\\:\\[A-Z\\]\\[a-z0-9\\]\\+\\)\\*\\[a-z0-9\\]\\?\\$\": jsonTagWithGiB" 17 | } 18 | -------------------------------------------------------------------------------- /pkg/analysis/jsontags/testdata/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | This is a copy of the minimum amount of the original file to be able to test the jsontags linter. 3 | */ 4 | package v1 5 | 6 | type TypeMeta struct{} 7 | 8 | type ObjectMeta struct{} 9 | -------------------------------------------------------------------------------- /pkg/analysis/maxlength/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package maxlength_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/maxlength" 23 | ) 24 | 25 | func TestMaxLength(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | 28 | analysistest.Run(t, testdata, maxlength.Analyzer, "a") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/analysis/maxlength/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | maxlength is an analyzer that checks that all string fields have a maximum length, and that all array fields have a maximum number of items. 19 | 20 | String fields that are not otherwise bound in length, through being an enum or formatted in a certain way, should have a maximum length. 21 | This ensures that CEL validations on the field are not overly costly in terms of time and memory. 22 | 23 | Array fields should have a maximum number of items. 24 | This ensures that any CEL validations on the field are not overly costly in terms of time and memory. 25 | Where arrays are used to represent a list of structures, CEL rules may exist within the array. 26 | Limiting the array length ensures the cardinality of the rules within the array is not unbounded. 27 | 28 | For strings, the maximum length can be set using the `kubebuilder:validation:MaxLength` tag. 29 | For arrays, the maximum number of items can be set using the `kubebuilder:validation:MaxItems` tag. 30 | 31 | For arrays of strings, the maximum length of each string can be set using the `kubebuilder:validation:items:MaxLength` tag, 32 | on the array field itself. 33 | Or, if the array uses a string type alias, the `kubebuilder:validation:MaxLength` tag can be used on the alias. 34 | */ 35 | package maxlength 36 | -------------------------------------------------------------------------------- /pkg/analysis/maxlength/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package maxlength 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return Analyzer, nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return false // For now, CRD only, and so not on by default. 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/maxlength/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | // StringAliasB is a string without a MaxLength. 4 | type StringAliasB string 5 | 6 | // StringAliasWithMaxLengthB is a string with a MaxLength. 7 | // +kubebuilder:validation:MaxLength:=512 8 | type StringAliasWithMaxLengthB string 9 | -------------------------------------------------------------------------------- /pkg/analysis/nobools/analyzer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nobools 17 | 18 | import ( 19 | "go/ast" 20 | 21 | "golang.org/x/tools/go/analysis" 22 | "golang.org/x/tools/go/analysis/passes/inspect" 23 | "golang.org/x/tools/go/ast/inspector" 24 | kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" 25 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 26 | ) 27 | 28 | const name = "nobools" 29 | 30 | // Analyzer is the analyzer for the nobools package. 31 | // It checks that no struct fields are `bool`. 32 | var Analyzer = &analysis.Analyzer{ 33 | Name: name, 34 | Doc: "Boolean values cannot evolve over time, use an enum with meaningful values instead", 35 | Run: run, 36 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 37 | } 38 | 39 | func run(pass *analysis.Pass) (any, error) { 40 | inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 41 | if !ok { 42 | return nil, kalerrors.ErrCouldNotGetInspector 43 | } 44 | 45 | // Filter to fields so that we can look at fields within structs. 46 | // Filter typespecs so that we can look at type aliases. 47 | nodeFilter := []ast.Node{ 48 | (*ast.StructType)(nil), 49 | (*ast.TypeSpec)(nil), 50 | } 51 | 52 | typeChecker := utils.NewTypeChecker(checkBool) 53 | 54 | // Preorder visits all the nodes of the AST in depth-first order. It calls 55 | // f(n) for each node n before it visits n's children. 56 | // 57 | // We use the filter defined above, ensuring we only look at struct fields and type declarations. 58 | inspect.Preorder(nodeFilter, func(n ast.Node) { 59 | typeChecker.CheckNode(pass, n) 60 | }) 61 | 62 | return nil, nil //nolint:nilnil 63 | } 64 | 65 | func checkBool(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { 66 | if ident.Name == "bool" { 67 | pass.Reportf(node.Pos(), "%s should not use a bool. Use a string type with meaningful constant values as an enum.", prefix) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/analysis/nobools/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nobools_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/nobools" 23 | ) 24 | 25 | func Test(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | analysistest.Run(t, testdata, nobools.Analyzer, "a") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/analysis/nobools/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | nobools is an analyzer that checks for usage of bool types. 19 | 20 | Boolean values can only ever have 2 states, true or false. 21 | Over time, needs may change, and with a bool type, there is no way to add additional states. 22 | This problem then leads to pairs of bools, where values of one are only valid given the value of another. 23 | This is confusing and error-prone. 24 | 25 | It is recommended instead to use a string type with a set of constants to represent the different states, 26 | creating an enum. 27 | 28 | By using an enum, not only can you provide meaningul names for the various states of the API, 29 | but you can also add additional states in the future without breaking the API. 30 | */ 31 | package nobools 32 | -------------------------------------------------------------------------------- /pkg/analysis/nobools/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nobools 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return Analyzer, nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | // Bools avoidance in the Kube conventions is not a must. 45 | // Make this opt in depending on the projects own preference. 46 | return false 47 | } 48 | -------------------------------------------------------------------------------- /pkg/analysis/nobools/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type Bools struct { 4 | ValidString string 5 | 6 | ValidMap map[string]string 7 | 8 | ValidInt32 int32 9 | 10 | ValidInt64 int64 11 | 12 | InvalidBool bool // want "field InvalidBool should not use a bool. Use a string type with meaningful constant values as an enum." 13 | 14 | InvalidBoolPtr *bool // want "field InvalidBoolPtr pointer should not use a bool. Use a string type with meaningful constant values as an enum." 15 | 16 | InvalidBoolSlice []bool // want "field InvalidBoolSlice array element should not use a bool. Use a string type with meaningful constant values as an enum." 17 | 18 | InvalidBoolPtrSlice []*bool // want "field InvalidBoolPtrSlice array element pointer should not use a bool. Use a string type with meaningful constant values as an enum." 19 | 20 | InvalidBoolAlias BoolAlias // want "field InvalidBoolAlias type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum." 21 | 22 | InvalidBoolPtrAlias *BoolAlias // want "field InvalidBoolPtrAlias pointer type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum." 23 | 24 | InvalidBoolSliceAlias []BoolAlias // want "field InvalidBoolSliceAlias array element type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum." 25 | 26 | InvalidBoolPtrSliceAlias []*BoolAlias // want "field InvalidBoolPtrSliceAlias array element pointer type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum." 27 | 28 | InvalidMapStringToBool map[string]bool // want "field InvalidMapStringToBool map value should not use a bool. Use a string type with meaningful constant values as an enum." 29 | 30 | InvalidMapStringToBoolPtr map[string]*bool // want "field InvalidMapStringToBoolPtr map value pointer should not use a bool. Use a string type with meaningful constant values as an enum." 31 | 32 | InvalidMapBoolToString map[bool]string // want "field InvalidMapBoolToString map key should not use a bool. Use a string type with meaningful constant values as an enum." 33 | 34 | InvalidMapBoolPtrToString map[*bool]string // want "field InvalidMapBoolPtrToString map key pointer should not use a bool. Use a string type with meaningful constant values as an enum." 35 | 36 | InvalidBoolAliasFromAnotherFile BoolAliasB // want "field InvalidBoolAliasFromAnotherFile type BoolAliasB should not use a bool. Use a string type with meaningful constant values as an enum." 37 | 38 | InvalidBoolPtrAliasFromAnotherFile *BoolAliasB // want "field InvalidBoolPtrAliasFromAnotherFile pointer type BoolAliasB should not use a bool. Use a string type with meaningful constant values as an enum." 39 | } 40 | 41 | // DoNothing is used to check that the analyser doesn't report on methods. 42 | func (Bools) DoNothing(a bool) bool { 43 | return a 44 | } 45 | 46 | type BoolAlias bool // want "type BoolAlias should not use a bool. Use a string type with meaningful constant values as an enum." 47 | 48 | type BoolAliasPtr *bool // want "type BoolAliasPtr pointer should not use a bool. Use a string type with meaningful constant values as an enum." 49 | 50 | type BoolAliasSlice []bool // want "type BoolAliasSlice array element should not use a bool. Use a string type with meaningful constant values as an enum." 51 | 52 | type BoolAliasPtrSlice []*bool // want "type BoolAliasPtrSlice array element pointer should not use a bool. Use a string type with meaningful constant values as an enum." 53 | 54 | type MapStringToBoolAlias map[string]bool // want "type MapStringToBoolAlias map value should not use a bool. Use a string type with meaningful constant values as an enum" 55 | 56 | type MapStringToBoolPtrAlias map[string]*bool //want "type MapStringToBoolPtrAlias map value pointer should not use a bool. Use a string type with meaningful constant values as an enum" 57 | -------------------------------------------------------------------------------- /pkg/analysis/nobools/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type BoolAliasB bool // want "type BoolAliasB should not use a bool. Use a string type with meaningful constant values as an enum." 4 | 5 | type BoolAliasPtrB *bool // want "type BoolAliasPtrB pointer should not use a bool. Use a string type with meaningful constant values as an enum." 6 | -------------------------------------------------------------------------------- /pkg/analysis/nofloats/analyzer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nofloats 17 | 18 | import ( 19 | "go/ast" 20 | 21 | "golang.org/x/tools/go/analysis" 22 | "golang.org/x/tools/go/analysis/passes/inspect" 23 | "golang.org/x/tools/go/ast/inspector" 24 | kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" 25 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 26 | ) 27 | 28 | const name = "nofloats" 29 | 30 | // Analyzer is the analyzer for the nofloats package. 31 | // It checks that no struct fields are `float`. 32 | var Analyzer = &analysis.Analyzer{ 33 | Name: name, 34 | Doc: "Float values cannot be reliably round-tripped without changing and have varying precisions and representations across languages and architectures.", 35 | Run: run, 36 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 37 | } 38 | 39 | func run(pass *analysis.Pass) (any, error) { 40 | inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 41 | if !ok { 42 | return nil, kalerrors.ErrCouldNotGetInspector 43 | } 44 | 45 | // Filter to structs so that we can look at fields within structs. 46 | // Filter typespecs so that we can look at type aliases. 47 | nodeFilter := []ast.Node{ 48 | (*ast.StructType)(nil), 49 | (*ast.TypeSpec)(nil), 50 | } 51 | 52 | typeChecker := utils.NewTypeChecker(checkFloat) 53 | 54 | // Preorder visits all the nodes of the AST in depth-first order. It calls 55 | // f(n) for each node n before it visits n's children. 56 | // 57 | // We use the filter defined above, ensuring we only look at struct fields and type declarations. 58 | inspect.Preorder(nodeFilter, func(n ast.Node) { 59 | typeChecker.CheckNode(pass, n) 60 | }) 61 | 62 | return nil, nil //nolint:nilnil 63 | } 64 | 65 | func checkFloat(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { 66 | if ident.Name == "float32" || ident.Name == "float64" { 67 | pass.Reportf(node.Pos(), "%s should not use a float value because they cannot be reliably round-tripped.", prefix) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/analysis/nofloats/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nofloats_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/nofloats" 23 | ) 24 | 25 | func Test(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | analysistest.Run(t, testdata, nofloats.Analyzer, "a") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/analysis/nofloats/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | nofloats is an analyzer that ensures fields in the API types do not contain a `float32` or `float64` type. 19 | 20 | Floating-point values cannot be reliably round-tripped without changing 21 | and have varying precision and representation across languages and architectures. 22 | Their use should be avoided as much as possible. 23 | They should never be used in spec. 24 | */ 25 | package nofloats 26 | -------------------------------------------------------------------------------- /pkg/analysis/nofloats/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nofloats 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return Analyzer, nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | // Floats avoidance in the Kube conventions is a must for spec fields. 45 | // Doesn't hurt to enforce it widely, uses outside of spec fields should be 46 | // evaluated on an individual basis to determine if it is reasonable to override. 47 | return true 48 | } 49 | -------------------------------------------------------------------------------- /pkg/analysis/nofloats/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type Float32AliasB float32 // want "type Float32AliasB should not use a float value because they cannot be reliably round-tripped." 4 | 5 | type Float32AliasPtrB *float32 // want "type Float32AliasPtrB pointer should not use a float value because they cannot be reliably round-tripped." 6 | -------------------------------------------------------------------------------- /pkg/analysis/nomaps/analyzer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nomaps 17 | 18 | import ( 19 | "fmt" 20 | "go/ast" 21 | "go/token" 22 | "go/types" 23 | 24 | "golang.org/x/tools/go/analysis" 25 | kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" 26 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" 27 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" 28 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" 29 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 30 | "sigs.k8s.io/kube-api-linter/pkg/config" 31 | ) 32 | 33 | const ( 34 | name = "nomaps" 35 | ) 36 | 37 | type analyzer struct { 38 | policy config.NoMapsPolicy 39 | } 40 | 41 | // newAnalyzer creates a new analyzer. 42 | func newAnalyzer(cfg config.NoMapsConfig) *analysis.Analyzer { 43 | defaultConfig(&cfg) 44 | 45 | a := &analyzer{ 46 | policy: cfg.Policy, 47 | } 48 | 49 | return &analysis.Analyzer{ 50 | Name: name, 51 | Doc: "Checks for usage of map types. Maps are discouraged apart from `map[string]string` which is used for labels and annotations. Use a list of named objects instead.", 52 | Run: a.run, 53 | Requires: []*analysis.Analyzer{inspector.Analyzer}, 54 | } 55 | } 56 | 57 | func (a *analyzer) run(pass *analysis.Pass) (any, error) { 58 | inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) 59 | if !ok { 60 | return nil, kalerrors.ErrCouldNotGetInspector 61 | } 62 | 63 | inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) { 64 | a.checkField(pass, field) 65 | }) 66 | 67 | return nil, nil //nolint:nilnil 68 | } 69 | 70 | func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field) { 71 | stringToStringMapType := types.NewMap(types.Typ[types.String], types.Typ[types.String]) 72 | 73 | underlyingType := pass.TypesInfo.TypeOf(field.Type).Underlying() 74 | 75 | if ptr, ok := underlyingType.(*types.Pointer); ok { 76 | underlyingType = ptr.Elem().Underlying() 77 | } 78 | 79 | m, ok := underlyingType.(*types.Map) 80 | if !ok { 81 | return 82 | } 83 | 84 | if a.policy == config.NoMapsEnforce { 85 | report(pass, field.Pos(), utils.FieldName(field)) 86 | return 87 | } 88 | 89 | if a.policy == config.NoMapsAllowStringToStringMaps { 90 | if types.Identical(m, stringToStringMapType) { 91 | return 92 | } 93 | 94 | report(pass, field.Pos(), utils.FieldName(field)) 95 | } 96 | 97 | if a.policy == config.NoMapsIgnore { 98 | key := m.Key().Underlying() 99 | _, ok := key.(*types.Basic) 100 | 101 | elm := m.Elem().Underlying() 102 | _, ok2 := elm.(*types.Basic) 103 | 104 | if ok && ok2 { 105 | return 106 | } 107 | 108 | report(pass, field.Pos(), utils.FieldName(field)) 109 | } 110 | } 111 | 112 | func report(pass *analysis.Pass, pos token.Pos, fieldName string) { 113 | pass.Report(analysis.Diagnostic{ 114 | Pos: pos, 115 | Message: fmt.Sprintf("%s should not use a map type, use a list type with a unique name/identifier instead", fieldName), 116 | }) 117 | } 118 | 119 | func defaultConfig(cfg *config.NoMapsConfig) { 120 | if cfg.Policy == "" { 121 | cfg.Policy = config.NoMapsAllowStringToStringMaps 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pkg/analysis/nomaps/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nomaps_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/nomaps" 23 | "sigs.k8s.io/kube-api-linter/pkg/config" 24 | ) 25 | 26 | func Test(t *testing.T) { 27 | testdata := analysistest.TestData() 28 | 29 | a, err := nomaps.Initializer().Init(config.LintersConfig{}) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | analysistest.Run(t, testdata, a, "a") 35 | } 36 | 37 | func TestWithEnforce(t *testing.T) { 38 | testdata := analysistest.TestData() 39 | 40 | a, err := nomaps.Initializer().Init(config.LintersConfig{ 41 | NoMaps: config.NoMapsConfig{ 42 | Policy: config.NoMapsEnforce, 43 | }, 44 | }) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | analysistest.Run(t, testdata, a, "b") 50 | } 51 | 52 | func TestWithAllowStringToStringMaps(t *testing.T) { 53 | testdata := analysistest.TestData() 54 | 55 | a, err := nomaps.Initializer().Init(config.LintersConfig{ 56 | NoMaps: config.NoMapsConfig{ 57 | Policy: config.NoMapsAllowStringToStringMaps, 58 | }, 59 | }) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | analysistest.Run(t, testdata, a, "c") 65 | } 66 | 67 | func TestWithIgnore(t *testing.T) { 68 | testdata := analysistest.TestData() 69 | 70 | a, err := nomaps.Initializer().Init(config.LintersConfig{ 71 | NoMaps: config.NoMapsConfig{ 72 | Policy: config.NoMapsIgnore, 73 | }, 74 | }) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | analysistest.Run(t, testdata, a, "d") 80 | } 81 | -------------------------------------------------------------------------------- /pkg/analysis/nomaps/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | nomaps provides a linter to ensure that fields do not use map types. 19 | 20 | Maps are discouraged in Kubernetes APIs. It is hard to distinguish between structs and maps in JSON/YAML and as such, lists of named subobjects are preferred over plain map types. 21 | 22 | Instead of 23 | 24 | ports: 25 | www: 26 | containerPort: 80 27 | 28 | use 29 | 30 | ports: 31 | - name: www 32 | containerPort: 80 33 | 34 | Lists should use the `+listType=map` and `+listMapKey=name` markers, or equivalent. 35 | */ 36 | package nomaps 37 | -------------------------------------------------------------------------------- /pkg/analysis/nomaps/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nomaps 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return newAnalyzer(cfg.NoMaps), nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/nomaps/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type ( 4 | MapStringComponentB map[string]Component 5 | PtrMapStringComponentB *map[string]Component 6 | MapStringIntB map[string]int 7 | MapIntStringB map[int]string 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/analysis/nophase/analyzer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nophase 17 | 18 | import ( 19 | "go/ast" 20 | "strings" 21 | 22 | "golang.org/x/tools/go/analysis" 23 | "golang.org/x/tools/go/analysis/passes/inspect" 24 | "golang.org/x/tools/go/ast/inspector" 25 | kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" 26 | "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" 27 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 28 | ) 29 | 30 | const name = "nophase" 31 | 32 | // Analyzer is the analyzer for the nophase package. 33 | // It checks that no struct fields named 'phase', or that contain phase as a 34 | // substring are present. 35 | var Analyzer = &analysis.Analyzer{ 36 | Name: name, 37 | Doc: "phase fields are deprecated and conditions should be preferred, avoid phase like enum fields", 38 | Run: run, 39 | Requires: []*analysis.Analyzer{inspect.Analyzer, extractjsontags.Analyzer}, 40 | } 41 | 42 | func run(pass *analysis.Pass) (any, error) { 43 | inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 44 | if !ok { 45 | return nil, kalerrors.ErrCouldNotGetInspector 46 | } 47 | 48 | jsonTags, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags) 49 | if !ok { 50 | return nil, kalerrors.ErrCouldNotGetJSONTags 51 | } 52 | 53 | // Filter to fields so that we can iterate over fields in a struct. 54 | nodeFilter := []ast.Node{ 55 | (*ast.Field)(nil), 56 | } 57 | 58 | // Preorder visits all the nodes of the AST in depth-first order. It calls 59 | // f(n) for each node n before it visits n's children. 60 | // 61 | // We use the filter defined above, ensuring we only look at struct fields. 62 | inspect.Preorder(nodeFilter, func(n ast.Node) { 63 | field, ok := n.(*ast.Field) 64 | if !ok { 65 | return 66 | } 67 | 68 | if field == nil || len(field.Names) == 0 { 69 | return 70 | } 71 | 72 | fieldName := utils.FieldName(field) 73 | 74 | // First check if the struct field name contains 'phase' 75 | if strings.Contains(strings.ToLower(fieldName), "phase") { 76 | pass.Reportf(field.Pos(), 77 | "field %s: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields", 78 | fieldName, 79 | ) 80 | 81 | return 82 | } 83 | 84 | // Then check if the json serialization of the field contains 'phase' 85 | tagInfo := jsonTags.FieldTags(field) 86 | 87 | if strings.Contains(strings.ToLower(tagInfo.Name), "phase") { 88 | pass.Reportf(field.Pos(), 89 | "field %s: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields", 90 | fieldName, 91 | ) 92 | } 93 | }) 94 | 95 | return nil, nil //nolint:nilnil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/analysis/nophase/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nophase_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/nophase" 23 | ) 24 | 25 | func Test(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | analysistest.RunWithSuggestedFixes(t, testdata, nophase.Analyzer, "a") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/analysis/nophase/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | nophase provides a linter to ensure that structs do not contain a Phase field. 19 | 20 | Phase fields are deprecated and conditions should be preferred. Avoid phase like enum 21 | fields. 22 | 23 | The linter will flag any struct field containing the substring 'phase'. This means both 24 | Phase and FooPhase will be flagged. 25 | */ 26 | package nophase 27 | -------------------------------------------------------------------------------- /pkg/analysis/nophase/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package nophase 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return Analyzer, nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/nophase/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type NoPhaseTestStruct struct { 4 | // +optional 5 | Phase *string `json:"phase,omitempty"` // want "field Phase: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields" 6 | 7 | } 8 | 9 | // DoNothing is used to check that the analyser doesn't report on methods. 10 | func (NoPhaseTestStruct) DoNothing() {} 11 | 12 | type NoSubPhaseTestStruct struct { 13 | // +optional 14 | FooPhase *string `json:"fooPhase,omitempty"` // want "field FooPhase: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields" 15 | 16 | } 17 | 18 | type SerializedPhaseTeststruct struct { 19 | // +optional 20 | FooField *string `json:"fooPhase,omitempty"` // want "field FooField: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields" 21 | 22 | } 23 | -------------------------------------------------------------------------------- /pkg/analysis/optionalfields/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package optionalfields_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | requiredfields "sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields" 23 | "sigs.k8s.io/kube-api-linter/pkg/config" 24 | ) 25 | 26 | func TestDefaultConfiguration(t *testing.T) { 27 | testdata := analysistest.TestData() 28 | 29 | a, err := requiredfields.Initializer().Init(config.LintersConfig{}) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | analysistest.RunWithSuggestedFixes(t, testdata, a, "a") 35 | } 36 | 37 | func TestWhenRequiredPreferenceConfiguration(t *testing.T) { 38 | testdata := analysistest.TestData() 39 | 40 | a, err := requiredfields.Initializer().Init(config.LintersConfig{ 41 | OptionalFields: config.OptionalFieldsConfig{ 42 | Pointers: config.OptionalFieldsPointers{ 43 | Preference: config.OptionalFieldsPointerPreferenceWhenRequired, 44 | }, 45 | }, 46 | }) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | analysistest.RunWithSuggestedFixes(t, testdata, a, "b") 52 | } 53 | 54 | func TestWhenRequiredWithOmitEmptyIgnorePreferenceConfiguration(t *testing.T) { 55 | testdata := analysistest.TestData() 56 | 57 | a, err := requiredfields.Initializer().Init(config.LintersConfig{ 58 | OptionalFields: config.OptionalFieldsConfig{ 59 | Pointers: config.OptionalFieldsPointers{ 60 | Preference: config.OptionalFieldsPointerPreferenceWhenRequired, 61 | }, 62 | OmitEmpty: config.OptionalFieldsOmitEmpty{ 63 | Policy: config.OptionalFieldsOmitEmptyPolicyIgnore, 64 | }, 65 | }, 66 | }) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | analysistest.RunWithSuggestedFixes(t, testdata, a, "c") 72 | } 73 | -------------------------------------------------------------------------------- /pkg/analysis/optionalfields/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | optionalfields is a linter to check that fields that are marked as optional are marshalled properly depending on the configured policies. 19 | 20 | By default, all optional fields should be pointers and have omitempty tags. The exception to this would be arrays and maps where the empty value can be omitted without the need for a pointer. 21 | 22 | However, where the zero value for a field is not a valid value (e.g. the empty string, or 0), the field does not need to be a pointer as the zero value could never be admitted. 23 | 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. 24 | 25 | Structs are also inspected to determine if they require a pointer. 26 | If a struct has any required fields, or a minimum numebr of properties, then fields leveraging the struct should be pointers. 27 | 28 | 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. 29 | */ 30 | package optionalfields 31 | -------------------------------------------------------------------------------- /pkg/analysis/optionalfields/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package optionalfields 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return newAnalyzer(cfg.OptionalFields), nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/optionalfields/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type StringAliasFromAnotherFile string 4 | -------------------------------------------------------------------------------- /pkg/analysis/optionalfields/testdata/src/b/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type StructWithRequiredField struct { 4 | // tsring is a string field. 5 | // +required 6 | String string `json:"string"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/analysis/optionalfields/testdata/src/c/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type StructWithRequiredField struct { 4 | // tsring is a string field. 5 | // +required 6 | String string `json:"string"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/analysis/optionalorrequired/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package optionalorrequired_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired" 23 | "sigs.k8s.io/kube-api-linter/pkg/config" 24 | "sigs.k8s.io/kube-api-linter/pkg/markers" 25 | ) 26 | 27 | func TestDefaultConfiguration(t *testing.T) { 28 | testdata := analysistest.TestData() 29 | 30 | a, err := optionalorrequired.Initializer().Init(config.LintersConfig{}) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | analysistest.RunWithSuggestedFixes(t, testdata, a, "a") 36 | } 37 | 38 | func TestSwappedMarkerPriority(t *testing.T) { 39 | testdata := analysistest.TestData() 40 | 41 | a, err := optionalorrequired.Initializer().Init(config.LintersConfig{ 42 | OptionalOrRequired: config.OptionalOrRequiredConfig{ 43 | PreferredOptionalMarker: markers.KubebuilderOptionalMarker, 44 | PreferredRequiredMarker: markers.KubebuilderRequiredMarker, 45 | }, 46 | }) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | analysistest.RunWithSuggestedFixes(t, testdata, a, "b") 52 | } 53 | 54 | func TestTypeSpec(t *testing.T) { 55 | testdata := analysistest.TestData() 56 | 57 | a, err := optionalorrequired.Initializer().Init(config.LintersConfig{}) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | analysistest.RunWithSuggestedFixes(t, testdata, a, "c") 63 | } 64 | -------------------------------------------------------------------------------- /pkg/analysis/optionalorrequired/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | optionalorrequired is a linter to ensure that all fields are marked as either optional or required. 19 | It also checks for the presence of optional or required markers on type declarations, and forbids this pattern. 20 | 21 | By default, it searches for the `+optional` and `+required` markers, and ensures that all fields are marked 22 | with at least one of these markers. 23 | 24 | The linter can be configured to use different markers, by setting the `PreferredOptionalMarker` and `PreferredRequiredMarker`. 25 | The default values are `+optional` and `+required`, respectively. 26 | The available alternate values for each marker are: 27 | 28 | For PreferredOptionalMarker: 29 | - `+optional`: The standard Kubernetes marker for optional fields. 30 | - `+kubebuilder:validation:Optional`: The Kubebuilder marker for optional fields. 31 | 32 | For PreferredRequiredMarker: 33 | - `+required`: The standard Kubernetes marker for required fields. 34 | - `+kubebuilder:validation:Required`: The Kubebuilder marker for required fields. 35 | 36 | When a field is marked with both the Kubernetes and Kubebuilder markers, the linter will suggest to remove the Kubebuilder marker. 37 | When a field is marked only with the Kubebuilder marker, the linter will suggest to use the Kubernetes marker instead. 38 | This behaviour is reversed when the `PreferredOptionalMarker` and `PreferredRequiredMarker` are set to the Kubebuilder markers. 39 | 40 | Use the linter fix option to automatically apply suggested fixes. 41 | */ 42 | package optionalorrequired 43 | -------------------------------------------------------------------------------- /pkg/analysis/optionalorrequired/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package optionalorrequired 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return newAnalyzer(cfg.OptionalOrRequired), nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/optionalorrequired/testdata/src/b/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type OptionalOrRequiredTestStruct struct { 4 | NoMarkers string // want "field NoMarkers must be marked as kubebuilder:validation:Optional or kubebuilder:validation:Required" 5 | 6 | // noOptionalOrRequiredMarker is a field with no optional or required marker. 7 | // +enum 8 | // +kubebuilder:validation:Enum=Foo;Bar 9 | NoOptionalOrRequiredMarker string // want "field NoOptionalOrRequiredMarker must be marked as kubebuilder:validation:Optional or kubebuilder:validation:Required" 10 | 11 | // +optional 12 | // +required 13 | MarkedOpitonalAndRequired string // want "field MarkedOpitonalAndRequired must not be marked as both optional and required" 14 | 15 | // +optional 16 | // +kubebuilder:validation:Optional 17 | MarkedOptionalAndKubeBuilderOptional string // want "field MarkedOptionalAndKubeBuilderOptional should use only the marker kubebuilder:validation:Optional, optional is not required" 18 | 19 | // +required 20 | // +kubebuilder:validation:Required 21 | MarkedRequiredAndKubeBuilderRequired string // want "field MarkedRequiredAndKubeBuilderRequired should use only the marker kubebuilder:validation:Required, required is not required" 22 | 23 | // +kubebuilder:validation:Optional 24 | KubebuilderOptionalMarker string 25 | 26 | // +kubebuilder:validation:Required 27 | KubebuilderRequiredMarker string 28 | 29 | // +optional 30 | OptionalMarker string // want "field OptionalMarker should use marker kubebuilder:validation:Optional instead of optional" 31 | 32 | // +required 33 | RequiredMarker string // want "field RequiredMarker should use marker kubebuilder:validation:Required instead of required" 34 | 35 | } 36 | -------------------------------------------------------------------------------- /pkg/analysis/optionalorrequired/testdata/src/b/b.go.golden: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type OptionalOrRequiredTestStruct struct { 4 | NoMarkers string // want "field NoMarkers must be marked as kubebuilder:validation:Optional or kubebuilder:validation:Required" 5 | 6 | // noOptionalOrRequiredMarker is a field with no optional or required marker. 7 | // +enum 8 | // +kubebuilder:validation:Enum=Foo;Bar 9 | NoOptionalOrRequiredMarker string // want "field NoOptionalOrRequiredMarker must be marked as kubebuilder:validation:Optional or kubebuilder:validation:Required" 10 | 11 | // +optional 12 | // +required 13 | MarkedOpitonalAndRequired string // want "field MarkedOpitonalAndRequired must not be marked as both optional and required" 14 | 15 | // +kubebuilder:validation:Optional 16 | MarkedOptionalAndKubeBuilderOptional string // want "field MarkedOptionalAndKubeBuilderOptional should use only the marker kubebuilder:validation:Optional, optional is not required" 17 | 18 | // +kubebuilder:validation:Required 19 | MarkedRequiredAndKubeBuilderRequired string // want "field MarkedRequiredAndKubeBuilderRequired should use only the marker kubebuilder:validation:Required, required is not required" 20 | 21 | // +kubebuilder:validation:Optional 22 | KubebuilderOptionalMarker string 23 | 24 | // +kubebuilder:validation:Required 25 | KubebuilderRequiredMarker string 26 | 27 | // +kubebuilder:validation:Optional 28 | OptionalMarker string // want "field OptionalMarker should use marker kubebuilder:validation:Optional instead of optional" 29 | 30 | // +kubebuilder:validation:Required 31 | RequiredMarker string // want "field RequiredMarker should use marker kubebuilder:validation:Required instead of required" 32 | 33 | } 34 | -------------------------------------------------------------------------------- /pkg/analysis/optionalorrequired/testdata/src/c/c.go: -------------------------------------------------------------------------------- 1 | package c 2 | 3 | type OptionalOrRequiredTestStruct struct { 4 | RequiredEnumField RequiredEnum // want "field RequiredEnumField must be marked as optional or required" 5 | 6 | KubebuilderRequiredEnumField KubeBuilderRequiredEnum // want "field KubebuilderRequiredEnumField must be marked as optional or required" 7 | 8 | OptionalEnumField OptionalEnum // want "field OptionalEnumField must be marked as optional or required" 9 | 10 | KubebuilderOptionalEnumField KubeBuilderOptionalEnum // want "field KubebuilderOptionalEnumField must be marked as optional or required" 11 | } 12 | 13 | // +required 14 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 15 | type RequiredEnum string // want "type RequiredEnum should not be marked as required" 16 | 17 | // +kubebuilder:validation:Required 18 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 19 | type KubeBuilderRequiredEnum string // want "type KubeBuilderRequiredEnum should not be marked as kubebuilder:validation:Required" 20 | 21 | // +k8s:required 22 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 23 | type K8sRequiredEnum string // want "type K8sRequiredEnum should not be marked as k8s:required" 24 | 25 | // +optional 26 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 27 | type OptionalEnum string // want "type OptionalEnum should not be marked as optional" 28 | 29 | // +kubebuilder:validation:Optional 30 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 31 | type KubeBuilderOptionalEnum string // want "type KubeBuilderOptionalEnum should not be marked as kubebuilder:validation:Optional" 32 | 33 | // +k8s:optional 34 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 35 | type K8sOptionalEnum string // want "type K8sOptionalEnum should not be marked as k8s:optional" 36 | -------------------------------------------------------------------------------- /pkg/analysis/optionalorrequired/testdata/src/c/c.go.golden: -------------------------------------------------------------------------------- 1 | package c 2 | 3 | type OptionalOrRequiredTestStruct struct { 4 | RequiredEnumField RequiredEnum // want "field RequiredEnumField must be marked as optional or required" 5 | 6 | KubebuilderRequiredEnumField KubeBuilderRequiredEnum // want "field KubebuilderRequiredEnumField must be marked as optional or required" 7 | 8 | OptionalEnumField OptionalEnum // want "field OptionalEnumField must be marked as optional or required" 9 | 10 | KubebuilderOptionalEnumField KubeBuilderOptionalEnum // want "field KubebuilderOptionalEnumField must be marked as optional or required" 11 | } 12 | 13 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 14 | type RequiredEnum string // want "type RequiredEnum should not be marked as required" 15 | 16 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 17 | type KubeBuilderRequiredEnum string // want "type KubeBuilderRequiredEnum should not be marked as kubebuilder:validation:Required" 18 | 19 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 20 | type K8sRequiredEnum string // want "type K8sRequiredEnum should not be marked as k8s:required" 21 | 22 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 23 | type OptionalEnum string // want "type OptionalEnum should not be marked as optional" 24 | 25 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 26 | type KubeBuilderOptionalEnum string // want "type KubeBuilderOptionalEnum should not be marked as kubebuilder:validation:Optional" 27 | 28 | // +kubebuilder:validation:Enum=Foo;Bar;Baz 29 | type K8sOptionalEnum string // want "type K8sOptionalEnum should not be marked as k8s:optional" 30 | -------------------------------------------------------------------------------- /pkg/analysis/requiredfields/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package requiredfields_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/requiredfields" 23 | "sigs.k8s.io/kube-api-linter/pkg/config" 24 | ) 25 | 26 | func TestDefaultConfiguration(t *testing.T) { 27 | testdata := analysistest.TestData() 28 | 29 | a, err := requiredfields.Initializer().Init(config.LintersConfig{}) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | analysistest.RunWithSuggestedFixes(t, testdata, a, "a") 35 | } 36 | 37 | func TestWithPointerPolicyWarn(t *testing.T) { 38 | testdata := analysistest.TestData() 39 | 40 | a, err := requiredfields.Initializer().Init(config.LintersConfig{ 41 | RequiredFields: config.RequiredFieldsConfig{ 42 | PointerPolicy: config.RequiredFieldPointerWarn, 43 | }, 44 | }) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | analysistest.RunWithSuggestedFixes(t, testdata, a, "b") 50 | } 51 | -------------------------------------------------------------------------------- /pkg/analysis/requiredfields/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | requiredFields is a linter to check that fields that are marked as required are not pointers, and do not have the omitempty tag. 19 | The linter will check for fields that are marked as required using the +required marker, or the +kubebuilder:validation:Required marker. 20 | 21 | The linter will suggest to remove the omitempty tag from fields that are marked as required, but have the omitempty tag. 22 | The linter will suggest to remove the pointer type from fields that are marked as required. 23 | 24 | If you have a large, existing codebase, you may not want to automatically fix all of the pointer issues. 25 | In this case, you can configure the linter not to suggest fixing the pointer issues by setting the `pointerPolicy` option to `Warn`. 26 | */ 27 | package requiredfields 28 | -------------------------------------------------------------------------------- /pkg/analysis/requiredfields/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package requiredfields 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return newAnalyzer(cfg.RequiredFields), nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/analysis/requiredfields/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type A struct { 4 | // optional field should not be picked up. 5 | // +optional 6 | OptionalField *string `json:"optionalField,omitempty"` 7 | 8 | // requiredCorrectField should not be picked up. 9 | // +required 10 | RequiredCorrectField string `json:"requiredCorrectField"` 11 | 12 | // requiredOmitEmptyField field should be picked up. 13 | // +required 14 | RequiredOmitEmptyField string `json:"requiredOmitEmptyField,omitempty"` // want "field RequiredOmitEmptyField is marked as required, but has the omitempty tag" 15 | 16 | // requiredPointerField should be picked up. 17 | // +required 18 | RequiredPointerField *string `json:"requiredPointerField"` // want "field RequiredPointerField is marked as required, should not be a pointer" 19 | 20 | // requiredPointerOmitEmptyField should be picked up. 21 | // +required 22 | RequiredPointerOmitEmptyField *string `json:"requiredPointerOmitEmptyField,omitempty"` // want "field RequiredPointerOmitEmptyField is marked as required, but has the omitempty tag" "field RequiredPointerOmitEmptyField is marked as required, should not be a pointer" 23 | 24 | // requiredKubebuilderMarkerField should not be picked up. 25 | // +kubebuilder:validation:Required 26 | RequiredKubebuilderMarkerField string `json:"requiredKubebuilderMarkerField"` 27 | 28 | // requiredKubebuilderMarkerOmitEmptyField should be picked up. 29 | // +kubebuilder:validation:Required 30 | RequiredKubebuilderMarkerOmitEmptyField string `json:"requiredKubebuilderMarkerOmitEmptyField,omitempty"` // want "field RequiredKubebuilderMarkerOmitEmptyField is marked as required, but has the omitempty tag" 31 | 32 | // requiredKubebuilderMarkerPointerField should be picked up. 33 | // +kubebuilder:validation:Required 34 | RequiredKubebuilderMarkerPointerField *string `json:"requiredKubebuilderMarkerPointerField"` // want "field RequiredKubebuilderMarkerPointerField is marked as required, should not be a pointer" 35 | 36 | // requiredKubebuilderMarkerPointerOmitEmptyField should be picked up. 37 | // +kubebuilder:validation:Required 38 | RequiredKubebuilderMarkerPointerOmitEmptyField *string `json:"requiredKubebuilderMarkerPointerOmitEmptyField,omitempty"` // want "field RequiredKubebuilderMarkerPointerOmitEmptyField is marked as required, but has the omitempty tag" "field RequiredKubebuilderMarkerPointerOmitEmptyField is marked as required, should not be a pointer" 39 | } 40 | 41 | // DoNothing is used to check that the analyser doesn't report on methods. 42 | func (A) DoNothing() {} 43 | 44 | type Interface interface { 45 | InaccessibleFunction() string 46 | } 47 | -------------------------------------------------------------------------------- /pkg/analysis/requiredfields/testdata/src/a/a.go.golden: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type A struct { 4 | // optional field should not be picked up. 5 | // +optional 6 | OptionalField *string `json:"optionalField,omitempty"` 7 | 8 | // requiredCorrectField should not be picked up. 9 | // +required 10 | RequiredCorrectField string `json:"requiredCorrectField"` 11 | 12 | // requiredOmitEmptyField field should be picked up. 13 | // +required 14 | RequiredOmitEmptyField string `json:"requiredOmitEmptyField"` // want "field RequiredOmitEmptyField is marked as required, but has the omitempty tag" 15 | 16 | // requiredPointerField should be picked up. 17 | // +required 18 | RequiredPointerField string `json:"requiredPointerField"` // want "field RequiredPointerField is marked as required, should not be a pointer" 19 | 20 | // requiredPointerOmitEmptyField should be picked up. 21 | // +required 22 | RequiredPointerOmitEmptyField string `json:"requiredPointerOmitEmptyField"` // want "field RequiredPointerOmitEmptyField is marked as required, but has the omitempty tag" "field RequiredPointerOmitEmptyField is marked as required, should not be a pointer" 23 | 24 | // requiredKubebuilderMarkerField should not be picked up. 25 | // +kubebuilder:validation:Required 26 | RequiredKubebuilderMarkerField string `json:"requiredKubebuilderMarkerField"` 27 | 28 | // requiredKubebuilderMarkerOmitEmptyField should be picked up. 29 | // +kubebuilder:validation:Required 30 | RequiredKubebuilderMarkerOmitEmptyField string `json:"requiredKubebuilderMarkerOmitEmptyField"` // want "field RequiredKubebuilderMarkerOmitEmptyField is marked as required, but has the omitempty tag" 31 | 32 | // requiredKubebuilderMarkerPointerField should be picked up. 33 | // +kubebuilder:validation:Required 34 | RequiredKubebuilderMarkerPointerField string `json:"requiredKubebuilderMarkerPointerField"` // want "field RequiredKubebuilderMarkerPointerField is marked as required, should not be a pointer" 35 | 36 | // requiredKubebuilderMarkerPointerOmitEmptyField should be picked up. 37 | // +kubebuilder:validation:Required 38 | RequiredKubebuilderMarkerPointerOmitEmptyField string `json:"requiredKubebuilderMarkerPointerOmitEmptyField"` // want "field RequiredKubebuilderMarkerPointerOmitEmptyField is marked as required, but has the omitempty tag" "field RequiredKubebuilderMarkerPointerOmitEmptyField is marked as required, should not be a pointer" 39 | } 40 | 41 | // DoNothing is used to check that the analyser doesn't report on methods. 42 | func (A) DoNothing() {} 43 | 44 | type Interface interface { 45 | InaccessibleFunction() string 46 | } 47 | -------------------------------------------------------------------------------- /pkg/analysis/requiredfields/testdata/src/b/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type A struct { 4 | // optional field should not be picked up. 5 | // +optional 6 | OptionalField *string `json:"optionalField,omitempty"` 7 | 8 | // requiredCorrectField should not be picked up. 9 | // +required 10 | RequiredCorrectField string `json:"requiredCorrectField"` 11 | 12 | // requiredOmitEmptyField field should be picked up. 13 | // +required 14 | RequiredOmitEmptyField string `json:"requiredOmitEmptyField,omitempty"` // want "field RequiredOmitEmptyField is marked as required, but has the omitempty tag" 15 | 16 | // requiredPointerField should be picked up. 17 | // +required 18 | RequiredPointerField *string `json:"requiredPointerField"` // want "field RequiredPointerField is marked as required, should not be a pointer" 19 | 20 | // requiredPointerOmitEmptyField should be picked up. 21 | // +required 22 | RequiredPointerOmitEmptyField *string `json:"requiredPointerOmitEmptyField,omitempty"` // want "field RequiredPointerOmitEmptyField is marked as required, but has the omitempty tag" "field RequiredPointerOmitEmptyField is marked as required, should not be a pointer" 23 | 24 | // requiredKubebuilderMarkerField should not be picked up. 25 | // +kubebuilder:validation:Required 26 | RequiredKubebuilderMarkerField string `json:"requiredKubebuilderMarkerField"` 27 | 28 | // requiredKubebuilderMarkerOmitEmptyField should be picked up. 29 | // +kubebuilder:validation:Required 30 | RequiredKubebuilderMarkerOmitEmptyField string `json:"requiredKubebuilderMarkerOmitEmptyField,omitempty"` // want "field RequiredKubebuilderMarkerOmitEmptyField is marked as required, but has the omitempty tag" 31 | 32 | // requiredKubebuilderMarkerPointerField should be picked up. 33 | // +kubebuilder:validation:Required 34 | RequiredKubebuilderMarkerPointerField *string `json:"requiredKubebuilderMarkerPointerField"` // want "field RequiredKubebuilderMarkerPointerField is marked as required, should not be a pointer" 35 | 36 | // requiredKubebuilderMarkerPointerOmitEmptyField should be picked up. 37 | // +kubebuilder:validation:Required 38 | RequiredKubebuilderMarkerPointerOmitEmptyField *string `json:"requiredKubebuilderMarkerPointerOmitEmptyField,omitempty"` // want "field RequiredKubebuilderMarkerPointerOmitEmptyField is marked as required, but has the omitempty tag" "field RequiredKubebuilderMarkerPointerOmitEmptyField is marked as required, should not be a pointer" 39 | } 40 | -------------------------------------------------------------------------------- /pkg/analysis/requiredfields/testdata/src/b/a.go.golden: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type A struct { 4 | // optional field should not be picked up. 5 | // +optional 6 | OptionalField *string `json:"optionalField,omitempty"` 7 | 8 | // requiredCorrectField should not be picked up. 9 | // +required 10 | RequiredCorrectField string `json:"requiredCorrectField"` 11 | 12 | // requiredOmitEmptyField field should be picked up. 13 | // +required 14 | RequiredOmitEmptyField string `json:"requiredOmitEmptyField"` // want "field RequiredOmitEmptyField is marked as required, but has the omitempty tag" 15 | 16 | // requiredPointerField should be picked up. 17 | // +required 18 | RequiredPointerField *string `json:"requiredPointerField"` // want "field RequiredPointerField is marked as required, should not be a pointer" 19 | 20 | // requiredPointerOmitEmptyField should be picked up. 21 | // +required 22 | RequiredPointerOmitEmptyField *string `json:"requiredPointerOmitEmptyField"` // want "field RequiredPointerOmitEmptyField is marked as required, but has the omitempty tag" "field RequiredPointerOmitEmptyField is marked as required, should not be a pointer" 23 | 24 | // requiredKubebuilderMarkerField should not be picked up. 25 | // +kubebuilder:validation:Required 26 | RequiredKubebuilderMarkerField string `json:"requiredKubebuilderMarkerField"` 27 | 28 | // requiredKubebuilderMarkerOmitEmptyField should be picked up. 29 | // +kubebuilder:validation:Required 30 | RequiredKubebuilderMarkerOmitEmptyField string `json:"requiredKubebuilderMarkerOmitEmptyField"` // want "field RequiredKubebuilderMarkerOmitEmptyField is marked as required, but has the omitempty tag" 31 | 32 | // requiredKubebuilderMarkerPointerField should be picked up. 33 | // +kubebuilder:validation:Required 34 | RequiredKubebuilderMarkerPointerField *string `json:"requiredKubebuilderMarkerPointerField"` // want "field RequiredKubebuilderMarkerPointerField is marked as required, should not be a pointer" 35 | 36 | // requiredKubebuilderMarkerPointerOmitEmptyField should be picked up. 37 | // +kubebuilder:validation:Required 38 | RequiredKubebuilderMarkerPointerOmitEmptyField *string `json:"requiredKubebuilderMarkerPointerOmitEmptyField"` // want "field RequiredKubebuilderMarkerPointerOmitEmptyField is marked as required, but has the omitempty tag" "field RequiredKubebuilderMarkerPointerOmitEmptyField is marked as required, should not be a pointer" 39 | } 40 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package statusoptional 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/markers" 23 | ) 24 | 25 | func Test(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | analysistest.RunWithSuggestedFixes(t, testdata, newAnalyzer(markers.OptionalMarker), "a") 28 | } 29 | 30 | func TestWithKubebuilderOptionalMarker(t *testing.T) { 31 | testdata := analysistest.TestData() 32 | analysistest.RunWithSuggestedFixes(t, testdata, newAnalyzer(markers.KubebuilderOptionalMarker), "b") 33 | } 34 | 35 | func TestWithK8sOptionalMarker(t *testing.T) { 36 | testdata := analysistest.TestData() 37 | analysistest.RunWithSuggestedFixes(t, testdata, newAnalyzer(markers.K8sOptionalMarker), "c") 38 | } 39 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | The statusoptional linter ensures that all first-level children fields within a status struct 19 | are marked as optional. 20 | 21 | This is important because status fields should be optional to allow for partial updates 22 | and backward compatibility. 23 | 24 | This linter checks: 25 | 1. For structs with a JSON tag of "status" 26 | 2. All direct child fields of the status struct 27 | 3. Ensures each child field has an optional marker 28 | 29 | The linter will report an issue if any field in the status struct is not marked as optional 30 | and will suggest a fix to add the appropriate optional marker. 31 | */ 32 | package statusoptional 33 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package statusoptional 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | 21 | "sigs.k8s.io/kube-api-linter/pkg/config" 22 | ) 23 | 24 | // Initializer returns the AnalyzerInitializer for this 25 | // Analyzer so that it can be added to the registry. 26 | func Initializer() initializer { 27 | return initializer{} 28 | } 29 | 30 | // initializer implements the AnalyzerInitializer interface. 31 | type initializer struct{} 32 | 33 | // Name returns the name of the Analyzer. 34 | func (initializer) Name() string { 35 | return name 36 | } 37 | 38 | // Init returns the initialized Analyzer. 39 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 40 | return newAnalyzer(cfg.StatusOptional.PreferredOptionalMarker), nil 41 | } 42 | 43 | // Default determines whether this Analyzer is on by default, or not. 44 | func (initializer) Default() bool { 45 | return true 46 | } 47 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | // Different embedding scenarios 4 | type ResourceWithEmbeddings struct { 5 | Status StatusWithEmbeddings `json:"status"` 6 | } 7 | 8 | type StatusWithEmbeddings struct { 9 | // Regular inlined embed 10 | InlineEmbed `json:",inline"` 11 | 12 | // Non-inlined embed 13 | NonInlineEmbed `json:"nonInlineEmbed"` // want "status field \"NonInlineEmbed\" must be marked as optional" 14 | 15 | // Non-inlined embed with omitempty 16 | NonInlineOmitEmptyEmbed `json:"nonInlineOmitEmpty,omitempty"` // want "status field \"NonInlineOmitEmptyEmbed\" must be marked as optional" 17 | 18 | // Pointer to non-inlined embed 19 | *PointerEmbed `json:"pointerEmbed"` // want "status field \"PointerEmbed\" must be marked as optional" 20 | 21 | // Pointer to non-inlined embed with omitempty 22 | *PointerOmitEmptyEmbed `json:"pointerOmitEmpty,omitempty"` // want "status field \"PointerOmitEmptyEmbed\" must be marked as optional" 23 | 24 | // NonInlinedStructFromAnotherFile imports a type from another file 25 | NonInlinedStructFromAnotherFile StructFromAnotherFile `json:"nonInlinedStructFromAnotherFile"` // want "status field \"NonInlinedStructFromAnotherFile\" must be marked as optional" 26 | 27 | StructFromAnotherFile `json:",inline"` 28 | } 29 | 30 | type InlineEmbed struct { 31 | InlineField string `json:"inlineField"` // want "status field \"InlineField\" must be marked as optional" 32 | } 33 | 34 | type NonInlineEmbed struct { 35 | NonInlineField string `json:"nonInlineField"` 36 | } 37 | 38 | type NonInlineOmitEmptyEmbed struct { 39 | NonInlineOmitEmptyField string `json:"nonInlineOmitEmptyField"` 40 | } 41 | 42 | type PointerEmbed struct { 43 | PointerField string `json:"pointerField"` 44 | } 45 | 46 | type PointerOmitEmptyEmbed struct { 47 | PointerOmitEmptyField string `json:"pointerOmitEmptyField"` 48 | } 49 | 50 | type NonInlineOmitZeroEmbed struct { 51 | NonInlineOmitZeroField string `json:"nonInlineOmitZeroField"` 52 | } 53 | 54 | type PointerOmitZeroEmbed struct { 55 | PointerOmitZeroField string `json:"pointerOmitZeroField"` 56 | } 57 | 58 | type ResourceWithNestedStatus struct { 59 | Status NestedStatusStatus `json:"status"` 60 | } 61 | 62 | type NestedStatusStatus struct { 63 | // +optional 64 | NestedStatus SecondLevelStatus `json:"nestedStatus"` 65 | } 66 | 67 | type SecondLevelStatus struct { 68 | // The required here is ignored because it is not the top-level status field. 69 | // +required 70 | NestedField string `json:"nestedField"` 71 | } 72 | 73 | type ResourceWithStatusMarkedRequired struct { 74 | Status StatusMarkedRequired `json:"status"` 75 | } 76 | 77 | type StatusMarkedRequired struct { 78 | // +required 79 | OneRequiredField string `json:"oneRequiredField"` // want "status field \"OneRequiredField\" must be marked as optional" 80 | 81 | // +required 82 | // +kubebuilder:validation:Required 83 | BothRequiredField string `json:"bothRequiredField"` // want "status field \"BothRequiredField\" must be marked as optional" 84 | } 85 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/testdata/src/a/a.go.golden: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | // Different embedding scenarios 4 | type ResourceWithEmbeddings struct { 5 | Status StatusWithEmbeddings `json:"status"` 6 | } 7 | 8 | type StatusWithEmbeddings struct { 9 | // Regular inlined embed 10 | InlineEmbed `json:",inline"` 11 | 12 | // Non-inlined embed 13 | // +optional 14 | NonInlineEmbed `json:"nonInlineEmbed"` // want "status field \"NonInlineEmbed\" must be marked as optional" 15 | 16 | // Non-inlined embed with omitempty 17 | // +optional 18 | NonInlineOmitEmptyEmbed `json:"nonInlineOmitEmpty,omitempty"` // want "status field \"NonInlineOmitEmptyEmbed\" must be marked as optional" 19 | 20 | // Pointer to non-inlined embed 21 | // +optional 22 | *PointerEmbed `json:"pointerEmbed"` // want "status field \"PointerEmbed\" must be marked as optional" 23 | 24 | // Pointer to non-inlined embed with omitempty 25 | // +optional 26 | *PointerOmitEmptyEmbed `json:"pointerOmitEmpty,omitempty"` // want "status field \"PointerOmitEmptyEmbed\" must be marked as optional" 27 | 28 | // NonInlinedStructFromAnotherFile imports a type from another file 29 | // +optional 30 | NonInlinedStructFromAnotherFile StructFromAnotherFile `json:"nonInlinedStructFromAnotherFile"` // want "status field \"NonInlinedStructFromAnotherFile\" must be marked as optional" 31 | 32 | StructFromAnotherFile `json:",inline"` 33 | } 34 | 35 | type InlineEmbed struct { 36 | // +optional 37 | InlineField string `json:"inlineField"` // want "status field \"InlineField\" must be marked as optional" 38 | } 39 | 40 | type NonInlineEmbed struct { 41 | NonInlineField string `json:"nonInlineField"` 42 | } 43 | 44 | type NonInlineOmitEmptyEmbed struct { 45 | NonInlineOmitEmptyField string `json:"nonInlineOmitEmptyField"` 46 | } 47 | 48 | type PointerEmbed struct { 49 | PointerField string `json:"pointerField"` 50 | } 51 | 52 | type PointerOmitEmptyEmbed struct { 53 | PointerOmitEmptyField string `json:"pointerOmitEmptyField"` 54 | } 55 | 56 | type NonInlineOmitZeroEmbed struct { 57 | NonInlineOmitZeroField string `json:"nonInlineOmitZeroField"` 58 | } 59 | 60 | type PointerOmitZeroEmbed struct { 61 | PointerOmitZeroField string `json:"pointerOmitZeroField"` 62 | } 63 | 64 | type ResourceWithNestedStatus struct { 65 | Status NestedStatusStatus `json:"status"` 66 | } 67 | 68 | type NestedStatusStatus struct { 69 | // +optional 70 | NestedStatus SecondLevelStatus `json:"nestedStatus"` 71 | } 72 | 73 | type SecondLevelStatus struct { 74 | // The required here is ignored because it is not the top-level status field. 75 | // +required 76 | NestedField string `json:"nestedField"` 77 | } 78 | 79 | type ResourceWithStatusMarkedRequired struct { 80 | Status StatusMarkedRequired `json:"status"` 81 | } 82 | 83 | type StatusMarkedRequired struct { 84 | // +optional 85 | OneRequiredField string `json:"oneRequiredField"` // want "status field \"OneRequiredField\" must be marked as optional" 86 | 87 | // +optional 88 | BothRequiredField string `json:"bothRequiredField"` // want "status field \"BothRequiredField\" must be marked as optional" 89 | } 90 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package a 17 | 18 | type StructFromAnotherFile struct { 19 | // OptionalStringField is a string field that is optional. 20 | // +optional 21 | OptionalStringField string `json:"optionalStringField,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/testdata/src/b/b.go: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | // Different embedding scenarios 4 | type ResourceWithEmbeddings struct { 5 | Status StatusWithEmbeddings `json:"status"` 6 | } 7 | 8 | type StatusWithEmbeddings struct { 9 | // Regular inlined embed 10 | InlineEmbed `json:",inline"` 11 | 12 | // Non-inlined embed 13 | NonInlineEmbed `json:"nonInlineEmbed"` // want "status field \"NonInlineEmbed\" must be marked as optional" 14 | 15 | // Non-inlined embed with omitempty 16 | NonInlineOmitEmptyEmbed `json:"nonInlineOmitEmpty,omitempty"` // want "status field \"NonInlineOmitEmptyEmbed\" must be marked as optional" 17 | 18 | // Pointer to non-inlined embed 19 | *PointerEmbed `json:"pointerEmbed"` // want "status field \"PointerEmbed\" must be marked as optional" 20 | 21 | // Pointer to non-inlined embed with omitempty 22 | *PointerOmitEmptyEmbed `json:"pointerOmitEmpty,omitempty"` // want "status field \"PointerOmitEmptyEmbed\" must be marked as optional" 23 | } 24 | 25 | type InlineEmbed struct { 26 | InlineField string `json:"inlineField"` // want "status field \"InlineField\" must be marked as optional" 27 | } 28 | 29 | type NonInlineEmbed struct { 30 | NonInlineField string `json:"nonInlineField"` 31 | } 32 | 33 | type NonInlineOmitEmptyEmbed struct { 34 | NonInlineOmitEmptyField string `json:"nonInlineOmitEmptyField"` 35 | } 36 | 37 | type PointerEmbed struct { 38 | PointerField string `json:"pointerField"` 39 | } 40 | 41 | type PointerOmitEmptyEmbed struct { 42 | PointerOmitEmptyField string `json:"pointerOmitEmptyField"` 43 | } 44 | 45 | type NonInlineOmitZeroEmbed struct { 46 | NonInlineOmitZeroField string `json:"nonInlineOmitZeroField"` 47 | } 48 | 49 | type PointerOmitZeroEmbed struct { 50 | PointerOmitZeroField string `json:"pointerOmitZeroField"` 51 | } 52 | 53 | type ResourceWithNestedStatus struct { 54 | Status NestedStatusStatus `json:"status"` 55 | } 56 | 57 | type NestedStatusStatus struct { 58 | // +kubebuilder:validation:Optional 59 | NestedStatus SecondLevelStatus `json:"nestedStatus"` 60 | } 61 | 62 | type SecondLevelStatus struct { 63 | // The required here is ignored because it is not the top-level status field. 64 | // +required 65 | NestedField string `json:"nestedField"` 66 | } 67 | 68 | type ResourceWithStatusMarkedRequired struct { 69 | Status StatusMarkedRequired `json:"status"` 70 | } 71 | 72 | type StatusMarkedRequired struct { 73 | // +required 74 | OneRequiredField string `json:"oneRequiredField"` // want "status field \"OneRequiredField\" must be marked as optional" 75 | 76 | // +required 77 | // +kubebuilder:validation:Required 78 | BothRequiredField string `json:"bothRequiredField"` // want "status field \"BothRequiredField\" must be marked as optional" 79 | } 80 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/testdata/src/b/b.go.golden: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | // Different embedding scenarios 4 | type ResourceWithEmbeddings struct { 5 | Status StatusWithEmbeddings `json:"status"` 6 | } 7 | 8 | type StatusWithEmbeddings struct { 9 | // Regular inlined embed 10 | InlineEmbed `json:",inline"` 11 | 12 | // Non-inlined embed 13 | // +kubebuilder:validation:Optional 14 | NonInlineEmbed `json:"nonInlineEmbed"` // want "status field \"NonInlineEmbed\" must be marked as optional" 15 | 16 | // Non-inlined embed with omitempty 17 | // +kubebuilder:validation:Optional 18 | NonInlineOmitEmptyEmbed `json:"nonInlineOmitEmpty,omitempty"` // want "status field \"NonInlineOmitEmptyEmbed\" must be marked as optional" 19 | 20 | // Pointer to non-inlined embed 21 | // +kubebuilder:validation:Optional 22 | *PointerEmbed `json:"pointerEmbed"` // want "status field \"PointerEmbed\" must be marked as optional" 23 | 24 | // Pointer to non-inlined embed with omitempty 25 | // +kubebuilder:validation:Optional 26 | *PointerOmitEmptyEmbed `json:"pointerOmitEmpty,omitempty"` // want "status field \"PointerOmitEmptyEmbed\" must be marked as optional" 27 | } 28 | 29 | type InlineEmbed struct { 30 | // +kubebuilder:validation:Optional 31 | InlineField string `json:"inlineField"` // want "status field \"InlineField\" must be marked as optional" 32 | } 33 | 34 | type NonInlineEmbed struct { 35 | NonInlineField string `json:"nonInlineField"` 36 | } 37 | 38 | type NonInlineOmitEmptyEmbed struct { 39 | NonInlineOmitEmptyField string `json:"nonInlineOmitEmptyField"` 40 | } 41 | 42 | type PointerEmbed struct { 43 | PointerField string `json:"pointerField"` 44 | } 45 | 46 | type PointerOmitEmptyEmbed struct { 47 | PointerOmitEmptyField string `json:"pointerOmitEmptyField"` 48 | } 49 | 50 | type NonInlineOmitZeroEmbed struct { 51 | NonInlineOmitZeroField string `json:"nonInlineOmitZeroField"` 52 | } 53 | 54 | type PointerOmitZeroEmbed struct { 55 | PointerOmitZeroField string `json:"pointerOmitZeroField"` 56 | } 57 | 58 | type ResourceWithNestedStatus struct { 59 | Status NestedStatusStatus `json:"status"` 60 | } 61 | 62 | type NestedStatusStatus struct { 63 | // +kubebuilder:validation:Optional 64 | NestedStatus SecondLevelStatus `json:"nestedStatus"` 65 | } 66 | 67 | type SecondLevelStatus struct { 68 | // The required here is ignored because it is not the top-level status field. 69 | // +required 70 | NestedField string `json:"nestedField"` 71 | } 72 | 73 | type ResourceWithStatusMarkedRequired struct { 74 | Status StatusMarkedRequired `json:"status"` 75 | } 76 | 77 | type StatusMarkedRequired struct { 78 | // +kubebuilder:validation:Optional 79 | OneRequiredField string `json:"oneRequiredField"` // want "status field \"OneRequiredField\" must be marked as optional" 80 | 81 | // +kubebuilder:validation:Optional 82 | BothRequiredField string `json:"bothRequiredField"` // want "status field \"BothRequiredField\" must be marked as optional" 83 | } 84 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/testdata/src/c/c.go: -------------------------------------------------------------------------------- 1 | package c 2 | 3 | // Different embedding scenarios 4 | type ResourceWithEmbeddings struct { 5 | Status StatusWithEmbeddings `json:"status"` 6 | } 7 | 8 | type StatusWithEmbeddings struct { 9 | // Regular inlined embed 10 | InlineEmbed `json:",inline"` 11 | 12 | // Non-inlined embed 13 | NonInlineEmbed `json:"nonInlineEmbed"` // want "status field \"NonInlineEmbed\" must be marked as optional" 14 | 15 | // Non-inlined embed with omitempty 16 | NonInlineOmitEmptyEmbed `json:"nonInlineOmitEmpty,omitempty"` // want "status field \"NonInlineOmitEmptyEmbed\" must be marked as optional" 17 | 18 | // Pointer to non-inlined embed 19 | *PointerEmbed `json:"pointerEmbed"` // want "status field \"PointerEmbed\" must be marked as optional" 20 | 21 | // Pointer to non-inlined embed with omitempty 22 | *PointerOmitEmptyEmbed `json:"pointerOmitEmpty,omitempty"` // want "status field \"PointerOmitEmptyEmbed\" must be marked as optional" 23 | } 24 | 25 | type InlineEmbed struct { 26 | InlineField string `json:"inlineField"` // want "status field \"InlineField\" must be marked as optional" 27 | } 28 | 29 | type NonInlineEmbed struct { 30 | NonInlineField string `json:"nonInlineField"` 31 | } 32 | 33 | type NonInlineOmitEmptyEmbed struct { 34 | NonInlineOmitEmptyField string `json:"nonInlineOmitEmptyField"` 35 | } 36 | 37 | type PointerEmbed struct { 38 | PointerField string `json:"pointerField"` 39 | } 40 | 41 | type PointerOmitEmptyEmbed struct { 42 | PointerOmitEmptyField string `json:"pointerOmitEmptyField"` 43 | } 44 | 45 | type NonInlineOmitZeroEmbed struct { 46 | NonInlineOmitZeroField string `json:"nonInlineOmitZeroField"` 47 | } 48 | 49 | type PointerOmitZeroEmbed struct { 50 | PointerOmitZeroField string `json:"pointerOmitZeroField"` 51 | } 52 | 53 | type ResourceWithNestedStatus struct { 54 | Status NestedStatusStatus `json:"status"` 55 | } 56 | 57 | type NestedStatusStatus struct { 58 | // +k8s:optional 59 | NestedStatus SecondLevelStatus `json:"nestedStatus"` 60 | } 61 | 62 | type SecondLevelStatus struct { 63 | // The required here is ignored because it is not the top-level status field. 64 | // +required 65 | NestedField string `json:"nestedField"` 66 | } 67 | 68 | type ResourceWithStatusMarkedRequired struct { 69 | Status StatusMarkedRequired `json:"status"` 70 | } 71 | 72 | type StatusMarkedRequired struct { 73 | // +required 74 | OneRequiredField string `json:"oneRequiredField"` // want "status field \"OneRequiredField\" must be marked as optional" 75 | 76 | // +required 77 | // +kubebuilder:validation:Required 78 | BothRequiredField string `json:"bothRequiredField"` // want "status field \"BothRequiredField\" must be marked as optional" 79 | } 80 | -------------------------------------------------------------------------------- /pkg/analysis/statusoptional/testdata/src/c/c.go.golden: -------------------------------------------------------------------------------- 1 | package c 2 | 3 | // Different embedding scenarios 4 | type ResourceWithEmbeddings struct { 5 | Status StatusWithEmbeddings `json:"status"` 6 | } 7 | 8 | type StatusWithEmbeddings struct { 9 | // Regular inlined embed 10 | InlineEmbed `json:",inline"` 11 | 12 | // Non-inlined embed 13 | // +k8s:optional 14 | NonInlineEmbed `json:"nonInlineEmbed"` // want "status field \"NonInlineEmbed\" must be marked as optional" 15 | 16 | // Non-inlined embed with omitempty 17 | // +k8s:optional 18 | NonInlineOmitEmptyEmbed `json:"nonInlineOmitEmpty,omitempty"` // want "status field \"NonInlineOmitEmptyEmbed\" must be marked as optional" 19 | 20 | // Pointer to non-inlined embed 21 | // +k8s:optional 22 | *PointerEmbed `json:"pointerEmbed"` // want "status field \"PointerEmbed\" must be marked as optional" 23 | 24 | // Pointer to non-inlined embed with omitempty 25 | // +k8s:optional 26 | *PointerOmitEmptyEmbed `json:"pointerOmitEmpty,omitempty"` // want "status field \"PointerOmitEmptyEmbed\" must be marked as optional" 27 | } 28 | 29 | type InlineEmbed struct { 30 | // +k8s:optional 31 | InlineField string `json:"inlineField"` // want "status field \"InlineField\" must be marked as optional" 32 | } 33 | 34 | type NonInlineEmbed struct { 35 | NonInlineField string `json:"nonInlineField"` 36 | } 37 | 38 | type NonInlineOmitEmptyEmbed struct { 39 | NonInlineOmitEmptyField string `json:"nonInlineOmitEmptyField"` 40 | } 41 | 42 | type PointerEmbed struct { 43 | PointerField string `json:"pointerField"` 44 | } 45 | 46 | type PointerOmitEmptyEmbed struct { 47 | PointerOmitEmptyField string `json:"pointerOmitEmptyField"` 48 | } 49 | 50 | type NonInlineOmitZeroEmbed struct { 51 | NonInlineOmitZeroField string `json:"nonInlineOmitZeroField"` 52 | } 53 | 54 | type PointerOmitZeroEmbed struct { 55 | PointerOmitZeroField string `json:"pointerOmitZeroField"` 56 | } 57 | 58 | type ResourceWithNestedStatus struct { 59 | Status NestedStatusStatus `json:"status"` 60 | } 61 | 62 | type NestedStatusStatus struct { 63 | // +k8s:optional 64 | NestedStatus SecondLevelStatus `json:"nestedStatus"` 65 | } 66 | 67 | type SecondLevelStatus struct { 68 | // The required here is ignored because it is not the top-level status field. 69 | // +required 70 | NestedField string `json:"nestedField"` 71 | } 72 | 73 | type ResourceWithStatusMarkedRequired struct { 74 | Status StatusMarkedRequired `json:"status"` 75 | } 76 | 77 | type StatusMarkedRequired struct { 78 | // +k8s:optional 79 | OneRequiredField string `json:"oneRequiredField"` // want "status field \"OneRequiredField\" must be marked as optional" 80 | 81 | // +k8s:optional 82 | BothRequiredField string `json:"bothRequiredField"` // want "status field \"BothRequiredField\" must be marked as optional" 83 | } 84 | -------------------------------------------------------------------------------- /pkg/analysis/statussubresource/analyzer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package statussubresource_test 17 | 18 | import ( 19 | "testing" 20 | 21 | "golang.org/x/tools/go/analysis/analysistest" 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis/statussubresource" 23 | "sigs.k8s.io/kube-api-linter/pkg/config" 24 | ) 25 | 26 | func TestStatusSubresourceAnalyzer(t *testing.T) { 27 | testdata := analysistest.TestData() 28 | initializer := statussubresource.Initializer() 29 | 30 | analyzer, err := initializer.Init(config.LintersConfig{}) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a") 36 | } 37 | -------------------------------------------------------------------------------- /pkg/analysis/statussubresource/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package statussubresource 17 | -------------------------------------------------------------------------------- /pkg/analysis/statussubresource/initializer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package statussubresource 17 | 18 | import ( 19 | "golang.org/x/tools/go/analysis" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // Initializer returns the AnalyzerInitializer for this 24 | // Analyzer so that it can be added to the registry. 25 | func Initializer() initializer { 26 | return initializer{} 27 | } 28 | 29 | // intializer implements the AnalyzerInitializer interface. 30 | type initializer struct{} 31 | 32 | // Name returns the name of the Analyzer. 33 | func (initializer) Name() string { 34 | return name 35 | } 36 | 37 | // Init returns the intialized Analyzer. 38 | func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) { 39 | return newAnalyzer(), nil 40 | } 41 | 42 | // Default determines whether this Analyzer is on by default, or not. 43 | func (initializer) Default() bool { 44 | // This check only applies to CRDs so should not be on by default. 45 | return false 46 | } 47 | -------------------------------------------------------------------------------- /pkg/analysis/statussubresource/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | // +kubebuilder:object:root:=true 4 | type Foo struct { 5 | Spec FooSpec `json:"spec"` 6 | } 7 | 8 | type FooSpec struct { 9 | Name string `json:"name"` 10 | } 11 | 12 | // +kubebuilder:object:root:=true 13 | // +kubebuilder:subresource:status 14 | type Bar struct { // want "root object type \"Bar\" is marked to enable the status subresource with marker \"kubebuilder:subresource:status\" but has no status field" 15 | Spec BarSpec `json:"spec"` 16 | } 17 | 18 | type BarSpec struct { 19 | Name string `json:"name"` 20 | } 21 | 22 | // +kubebuilder:object:root:=true 23 | type Baz struct { // want "root object type \"Baz\" has a status field but does not have the marker \"kubebuilder:subresource:status\" to enable the status subresource" 24 | Spec BazSpec `json:"spec"` 25 | Status BazStatus `json:"status,omitempty"` 26 | } 27 | 28 | type BazSpec struct { 29 | Name string `json:"name"` 30 | } 31 | 32 | type BazStatus struct { 33 | Name string `json:"name"` 34 | } 35 | 36 | // +kubebuilder:object:root:=true 37 | // +kubebuilder:subresource:status 38 | type FooBar struct { 39 | Spec FooBarSpec `json:"spec"` 40 | Status FooBarStatus `json:"status"` 41 | } 42 | 43 | type FooBarSpec struct { 44 | Name string `json:"name"` 45 | } 46 | 47 | type FooBarStatus struct { 48 | Name string `json:"name"` 49 | } 50 | 51 | // Test that it works with 'root=true' as well 52 | // +kubebuilder:object:root=true 53 | // +kubebuilder:subresource:status 54 | type FooBarBaz struct { // want "root object type \"FooBarBaz\" is marked to enable the status subresource with marker \"kubebuilder:subresource:status\" but has no status field" 55 | Spec FooBarBazSpec `json:"spec"` 56 | } 57 | 58 | type FooBarBazSpec struct { 59 | Name string `json:"name"` 60 | } 61 | -------------------------------------------------------------------------------- /pkg/analysis/statussubresource/testdata/src/a/a.go.golden: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | // +kubebuilder:object:root:=true 4 | type Foo struct { 5 | Spec FooSpec `json:"spec"` 6 | } 7 | 8 | type FooSpec struct { 9 | Name string `json:"name"` 10 | } 11 | 12 | // +kubebuilder:object:root:=true 13 | // +kubebuilder:subresource:status 14 | type Bar struct { // want "root object type \"Bar\" is marked to enable the status subresource with marker \"kubebuilder:subresource:status\" but has no status field" 15 | Spec BarSpec `json:"spec"` 16 | } 17 | 18 | type BarSpec struct { 19 | Name string `json:"name"` 20 | } 21 | 22 | // +kubebuilder:object:root:=true 23 | // +kubebuilder:subresource:status 24 | type Baz struct { // want "root object type \"Baz\" has a status field but does not have the marker \"kubebuilder:subresource:status\" to enable the status subresource" 25 | Spec BazSpec `json:"spec"` 26 | Status BazStatus `json:"status,omitempty"` 27 | } 28 | 29 | type BazSpec struct { 30 | Name string `json:"name"` 31 | } 32 | 33 | type BazStatus struct { 34 | Name string `json:"name"` 35 | } 36 | 37 | // +kubebuilder:object:root:=true 38 | // +kubebuilder:subresource:status 39 | type FooBar struct { 40 | Spec FooBarSpec `json:"spec"` 41 | Status FooBarStatus `json:"status"` 42 | } 43 | 44 | type FooBarSpec struct { 45 | Name string `json:"name"` 46 | } 47 | 48 | type FooBarStatus struct { 49 | Name string `json:"name"` 50 | } 51 | 52 | // Test that it works with 'root=true' as well 53 | // +kubebuilder:object:root=true 54 | // +kubebuilder:subresource:status 55 | type FooBarBaz struct { // want "root object type \"FooBarBaz\" is marked to enable the status subresource with marker \"kubebuilder:subresource:status\" but has no status field" 56 | Spec FooBarBazSpec `json:"spec"` 57 | } 58 | 59 | type FooBarBazSpec struct { 60 | Name string `json:"name"` 61 | } 62 | -------------------------------------------------------------------------------- /pkg/analysis/utils/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type Integers struct { 4 | String string // want "field String is a string" 5 | 6 | Map map[string]string // want "field Map map key is a string" "field Map map value is a string" 7 | 8 | MapStringToStringAlias map[string]StringAlias // want "field MapStringToStringAlias map key is a string" "field MapStringToStringAlias map value type StringAlias is a string" 9 | 10 | Int32 int32 11 | 12 | Int64 int64 13 | 14 | Bool bool 15 | 16 | StringPtr *string // want "field StringPtr pointer is a string" 17 | 18 | StringSlice []string // want "field StringSlice array element is a string" 19 | 20 | StringPtrSlice []*string // want "field StringPtrSlice array element pointer is a string" 21 | 22 | StringAlias StringAlias // want "field StringAlias type StringAlias is a string" 23 | 24 | StringAliasPtr *StringAlias // want "field StringAliasPtr pointer type StringAlias is a string" 25 | 26 | StringAliasSlice []StringAlias // want "field StringAliasSlice array element type StringAlias is a string" 27 | 28 | StringAliasPtrSlice []*StringAlias // want "field StringAliasPtrSlice array element pointer type StringAlias is a string" 29 | 30 | StringAliasFromAnotherFile StringAliasB // want "field StringAliasFromAnotherFile type StringAliasB is a string" 31 | } 32 | 33 | type StringAlias string // want "type StringAlias is a string" 34 | 35 | type StringAliasPtr *string // want "type StringAliasPtr pointer is a string" 36 | 37 | type StringAliasSlice []string // want "type StringAliasSlice array element is a string" 38 | 39 | type StringAliasPtrSlice []*string // want "type StringAliasPtrSlice array element pointer is a string" 40 | -------------------------------------------------------------------------------- /pkg/analysis/utils/testdata/src/a/b.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type StringAliasB string // want "type StringAliasB is a string" 4 | -------------------------------------------------------------------------------- /pkg/analysis/utils/type_check_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package utils_test 17 | 18 | import ( 19 | "errors" 20 | "go/ast" 21 | "testing" 22 | 23 | "golang.org/x/tools/go/analysis" 24 | "golang.org/x/tools/go/analysis/analysistest" 25 | "golang.org/x/tools/go/analysis/passes/inspect" 26 | "golang.org/x/tools/go/ast/inspector" 27 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 28 | ) 29 | 30 | var ( 31 | errCouldNotGetInspector = errors.New("could not get inspector") 32 | ) 33 | 34 | func Test(t *testing.T) { 35 | testdata := analysistest.TestData() 36 | analysistest.Run(t, testdata, testAnalyzer(), "a") 37 | } 38 | 39 | func testAnalyzer() *analysis.Analyzer { 40 | return &analysis.Analyzer{ 41 | Name: "test", 42 | Doc: "test", 43 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 44 | Run: func(pass *analysis.Pass) (any, error) { 45 | inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 46 | if !ok { 47 | return nil, errCouldNotGetInspector 48 | } 49 | 50 | // Filter to structs so that we can iterate over fields in a struct. 51 | nodeFilter := []ast.Node{ 52 | (*ast.Field)(nil), 53 | (*ast.TypeSpec)(nil), 54 | } 55 | 56 | typeChecker := utils.NewTypeChecker(func(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) { 57 | if ident.Name == "string" { 58 | pass.Reportf(node.Pos(), "%s is a string", prefix) 59 | } 60 | }) 61 | 62 | inspect.Preorder(nodeFilter, func(n ast.Node) { 63 | typeChecker.CheckNode(pass, n) 64 | }) 65 | 66 | return nil, nil 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/analysis/utils/utils_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package utils_test 17 | 18 | import ( 19 | "testing" 20 | 21 | . "github.com/onsi/ginkgo/v2" 22 | . "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestValidation(t *testing.T) { 26 | RegisterFailHandler(Fail) 27 | RunSpecs(t, "Utils") 28 | } 29 | -------------------------------------------------------------------------------- /pkg/analysis/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package utils_test 17 | 18 | import ( 19 | "go/ast" 20 | 21 | . "github.com/onsi/ginkgo/v2" 22 | . "github.com/onsi/gomega" 23 | 24 | "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" 25 | ) 26 | 27 | var _ = Describe("FieldName", func() { 28 | type fieldNameInput struct { 29 | field *ast.Field 30 | want string 31 | } 32 | 33 | DescribeTable("Should extract the field name", func(in fieldNameInput) { 34 | Expect(utils.FieldName(in.field)).To(Equal(in.want), "expect to match the extracted field name") 35 | }, 36 | Entry("field has Names", fieldNameInput{ 37 | field: &ast.Field{ 38 | Names: []*ast.Ident{ 39 | { 40 | Name: "foo", 41 | }, 42 | }, 43 | }, 44 | want: "foo", 45 | }), 46 | Entry("field has no Names, but is an Ident", fieldNameInput{ 47 | field: &ast.Field{ 48 | Type: &ast.Ident{ 49 | Name: "foo", 50 | }, 51 | }, 52 | want: "foo", 53 | }), 54 | Entry("field has no Names, but is a StarExpr with an Ident", fieldNameInput{ 55 | field: &ast.Field{ 56 | Type: &ast.StarExpr{ 57 | X: &ast.Ident{ 58 | Name: "foo", 59 | }, 60 | }, 61 | }, 62 | want: "foo", 63 | }), 64 | Entry("field has no Names, and is not an Ident or StarExpr", fieldNameInput{ 65 | field: &ast.Field{ 66 | Type: &ast.ArrayType{ 67 | Elt: &ast.Ident{ 68 | Name: "foo", 69 | }, 70 | }, 71 | }, 72 | want: "", 73 | }), 74 | ) 75 | }) 76 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package config 17 | 18 | // GolangCIConfig is the complete configuration for the KAL 19 | // linter when built as an integration into golangci-lint. 20 | type GolangCIConfig struct { 21 | // Linters allows the user to configure which linters should, 22 | // and should not be enabled. 23 | Linters Linters `mapstructure:"linters"` 24 | 25 | // LintersConfig contains configuration for individual linters. 26 | LintersConfig LintersConfig `mapstructure:"lintersConfig"` 27 | } 28 | -------------------------------------------------------------------------------- /pkg/config/linters.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package config 17 | 18 | const ( 19 | // Wildcard is used to imply all linters should be enabled/disabled. 20 | Wildcard = "*" 21 | ) 22 | 23 | // Linters allows the user to configure which linters should, and 24 | // should not be enabled. 25 | type Linters struct { 26 | // Enable is used to enable specific linters. 27 | // Use '*' to enable all known linters. 28 | // When using '*', it should be the only value in the list. 29 | // Values in this list will be added to the linters enabled by default. 30 | // Values should not appear in both 'enable' and 'disable'. 31 | Enable []string `mapstructure:"enable"` 32 | 33 | // Disable is used to disable specific linters. 34 | // Use '*' to disable all known linters. When all linters are disabled, 35 | // only those explicitly called out in 'Enable' will be enabled. 36 | // When using '*', it should be the only value in the list. 37 | // Values in this list will be added to the linters disabled by default. 38 | // Values should not appear in both 'enable' and 'disable'. 39 | Disable []string `mapstructure:"disable"` 40 | } 41 | -------------------------------------------------------------------------------- /pkg/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package main is meant to be compiled as a plugin for golangci-lint, see 18 | // https://golangci-lint.run/plugins/go-plugins/. 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | 24 | "golang.org/x/tools/go/analysis" 25 | kubeapilinter "sigs.k8s.io/kube-api-linter" 26 | ) 27 | 28 | // New API, see https://github.com/golangci/golangci-lint/pull/3887. 29 | func New(pluginSettings any) ([]*analysis.Analyzer, error) { 30 | plugin, err := kubeapilinter.New(pluginSettings) 31 | if err != nil { 32 | return nil, fmt.Errorf("error creating plugin: %w", err) 33 | } 34 | 35 | analyzers, err := plugin.BuildAnalyzers() 36 | if err != nil { 37 | return nil, fmt.Errorf("error building analyzers: %w", err) 38 | } 39 | 40 | return analyzers, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/validation/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package validation 17 | 18 | import ( 19 | "k8s.io/apimachinery/pkg/util/validation/field" 20 | "sigs.k8s.io/kube-api-linter/pkg/config" 21 | ) 22 | 23 | // ValidateGolangCIConfig is used to validate the provided configuration once 24 | // extracted from golangci-lint. 25 | func ValidateGolangCIConfig(g config.GolangCIConfig, fldPath *field.Path) error { 26 | if fldPath == nil { 27 | fldPath = field.NewPath("") 28 | } 29 | 30 | var fieldErrors field.ErrorList 31 | 32 | fieldErrors = append(fieldErrors, ValidateLinters(g.Linters, fldPath.Child("linters"))...) 33 | fieldErrors = append(fieldErrors, ValidateLintersConfig(g.LintersConfig, fldPath.Child("lintersConfig"))...) 34 | 35 | return fieldErrors.ToAggregate() 36 | } 37 | -------------------------------------------------------------------------------- /pkg/validation/linters.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package validation 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | 22 | "sigs.k8s.io/kube-api-linter/pkg/analysis" 23 | "sigs.k8s.io/kube-api-linter/pkg/config" 24 | 25 | "k8s.io/apimachinery/pkg/util/sets" 26 | "k8s.io/apimachinery/pkg/util/validation/field" 27 | ) 28 | 29 | // ValidateLinters is used to validate the configuration in the config.Linters struct. 30 | // 31 | //nolint:cyclop 32 | func ValidateLinters(l config.Linters, fldPath *field.Path) field.ErrorList { 33 | fieldErrors := field.ErrorList{} 34 | 35 | enable := sets.New(l.Enable...) 36 | enablePath := fldPath.Child("enable") 37 | 38 | switch { 39 | case len(enable) != len(l.Enable): 40 | fieldErrors = append(fieldErrors, field.Invalid(enablePath, l.Enable, "values in 'enable' must be unique")) 41 | case enable.Has(config.Wildcard) && enable.Len() != 1: 42 | fieldErrors = append(fieldErrors, field.Invalid(enablePath, l.Enable, "wildcard ('*') must not be specified with other values")) 43 | case !enable.Has(config.Wildcard) && enable.Difference(analysis.NewRegistry().AllLinters()).Len() > 0: 44 | fieldErrors = append(fieldErrors, field.Invalid(enablePath, l.Enable, fmt.Sprintf("unknown linters: %s", strings.Join(enable.Difference(analysis.NewRegistry().AllLinters()).UnsortedList(), ",")))) 45 | } 46 | 47 | disable := sets.New(l.Disable...) 48 | disablePath := fldPath.Child("disable") 49 | 50 | switch { 51 | case len(disable) != len(l.Disable): 52 | fieldErrors = append(fieldErrors, field.Invalid(disablePath, l.Disable, "values in 'disable' must be unique")) 53 | case disable.Has(config.Wildcard) && disable.Len() != 1: 54 | fieldErrors = append(fieldErrors, field.Invalid(disablePath, l.Disable, "wildcard ('*') must not be specified with other values")) 55 | case !disable.Has(config.Wildcard) && disable.Difference(analysis.NewRegistry().AllLinters()).Len() > 0: 56 | fieldErrors = append(fieldErrors, field.Invalid(disablePath, l.Disable, fmt.Sprintf("unknown linters: %s", strings.Join(disable.Difference(analysis.NewRegistry().AllLinters()).UnsortedList(), ",")))) 57 | } 58 | 59 | if enable.Intersection(disable).Len() > 0 { 60 | fieldErrors = append(fieldErrors, field.Invalid(fldPath, l, fmt.Sprintf("values in 'enable' and 'disable may not overlap, overlapping values: %s", strings.Join(enable.Intersection(disable).UnsortedList(), ",")))) 61 | } 62 | 63 | return fieldErrors 64 | } 65 | -------------------------------------------------------------------------------- /pkg/validation/validation_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package validation_test 17 | 18 | import ( 19 | "testing" 20 | 21 | . "github.com/onsi/ginkgo/v2" 22 | . "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestValidation(t *testing.T) { 26 | RegisterFailHandler(Fail) 27 | RunSpecs(t, "Validation") 28 | } 29 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package kubeapilinter is a golangci-lint plugin for the Kube API Linter (KAL). 18 | // It is built as a module to be used with golangci-lint. 19 | // See https://golangci-lint.run/plugins/module-plugins/ for more information. 20 | package kubeapilinter 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/golangci/plugin-module-register/register" 26 | "golang.org/x/tools/go/analysis" 27 | "k8s.io/apimachinery/pkg/util/validation/field" 28 | kalanalysis "sigs.k8s.io/kube-api-linter/pkg/analysis" 29 | "sigs.k8s.io/kube-api-linter/pkg/config" 30 | "sigs.k8s.io/kube-api-linter/pkg/validation" 31 | ) 32 | 33 | func init() { 34 | register.Plugin("kubeapilinter", New) 35 | } 36 | 37 | // New creates a new golangci-lint plugin based on the KAL analyzers. 38 | func New(settings any) (register.LinterPlugin, error) { 39 | s, err := register.DecodeSettings[config.GolangCIConfig](settings) 40 | if err != nil { 41 | return nil, fmt.Errorf("error decoding settings: %w", err) 42 | } 43 | 44 | return &GolangCIPlugin{config: s}, nil 45 | } 46 | 47 | // GolangCIPlugin constructs a new plugin for the golangci-lint 48 | // plugin pattern. 49 | // This allows golangci-lint to build a version of itself, containing 50 | // all of the anaylzers included in KAL. 51 | type GolangCIPlugin struct { 52 | config config.GolangCIConfig 53 | } 54 | 55 | // BuildAnalyzers returns all of the analyzers to run, based on the configuration. 56 | func (f *GolangCIPlugin) BuildAnalyzers() ([]*analysis.Analyzer, error) { 57 | if err := validation.ValidateGolangCIConfig(f.config, field.NewPath("")); err != nil { 58 | return nil, fmt.Errorf("error in KAL configuration: %w", err) 59 | } 60 | 61 | registry := kalanalysis.NewRegistry() 62 | 63 | analyzers, err := registry.InitializeLinters(f.config.Linters, f.config.LintersConfig) 64 | if err != nil { 65 | return nil, fmt.Errorf("error initializing analyzers: %w", err) 66 | } 67 | 68 | return analyzers, nil 69 | } 70 | 71 | // GetLoadMode implements the golangci-lint plugin interface. 72 | func (f *GolangCIPlugin) GetLoadMode() string { 73 | return register.LoadModeTypesInfo 74 | } 75 | --------------------------------------------------------------------------------