From f2d8b7ec2ccfbcf548ace509b18dd8d3fa08525e Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Wed, 25 Jun 2025 08:20:23 -0400 Subject: [PATCH] Add linter to report on unsorted feature gates Signed-off-by: Davanum Srinivas --- hack/golangci-hints.yaml | 16 + hack/golangci.yaml | 16 + hack/golangci.yaml.in | 8 + hack/tools/golangci-lint/go.mod | 21 +- hack/tools/golangci-lint/go.sum | 36 +- .../golangci-lint/sortedfeatures/README.md | 137 ++++ .../golangci-lint/sortedfeatures/config.yaml | 9 + .../sortedfeatures/example.golangci.yml | 25 + .../golangci-lint/sortedfeatures/main.go | 27 + .../sortedfeatures/pkg/sortedfeatures.go | 395 +++++++++ .../sortedfeatures/pkg/sortedfeatures_test.go | 762 ++++++++++++++++++ .../sortedfeatures/plugin/plugin.go | 96 +++ .../testdata/src/testdata/sorted.go | 17 + .../testdata/src/testdata/unsorted.go | 17 + hack/verify-golangci-lint.sh | 3 +- hack/verify-test-featuregates.sh | 4 +- 16 files changed, 1559 insertions(+), 30 deletions(-) create mode 100644 hack/tools/golangci-lint/sortedfeatures/README.md create mode 100644 hack/tools/golangci-lint/sortedfeatures/config.yaml create mode 100644 hack/tools/golangci-lint/sortedfeatures/example.golangci.yml create mode 100644 hack/tools/golangci-lint/sortedfeatures/main.go create mode 100644 hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures.go create mode 100644 hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures_test.go create mode 100644 hack/tools/golangci-lint/sortedfeatures/plugin/plugin.go create mode 100644 hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/sorted.go create mode 100644 hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/unsorted.go diff --git a/hack/golangci-hints.yaml b/hack/golangci-hints.yaml index 856c8b63e21..f05356fbe6d 100644 --- a/hack/golangci-hints.yaml +++ b/hack/golangci-hints.yaml @@ -131,6 +131,7 @@ linters: - kubeapilinter - logcheck - revive + - sortedfeatures - staticcheck - testifylint - unused @@ -210,6 +211,21 @@ linters: # this restriction. Whether we then do a global search/replace remains # to be decided. with-helpers .* + sortedfeatures: + # Installed there by hack/verify-golangci-lint.sh. + path: _output/local/bin/sortedfeatures.so + description: check if feature gates are sorted + original-url: k8s.io/kubernetes/hack/tools/golangci-lint/sortedfeatures + settings: + files: + - cmd/kubeadm/app/features/features.go + - pkg/features/kube_features.go + - staging/src/k8s.io/apiserver/pkg/features/kube_features.go + - staging/src/k8s.io/client-go/features/known_features.go + - staging/src/k8s.io/controller-manager/pkg/features/kube_features.go + - staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go + - test/e2e/feature/feature.go + - test/e2e/environment/environment.go kubeapilinter: path: _output/local/bin/kube-api-linter.so description: kube-api-linter and lints Kube like APIs based on API conventions and best practices. diff --git a/hack/golangci.yaml b/hack/golangci.yaml index 6235bf8225f..26c59de3b8b 100644 --- a/hack/golangci.yaml +++ b/hack/golangci.yaml @@ -146,6 +146,7 @@ linters: - kubeapilinter - logcheck - revive + - sortedfeatures - staticcheck - testifylint - unused @@ -224,6 +225,21 @@ linters: # this restriction. Whether we then do a global search/replace remains # to be decided. with-helpers .* + sortedfeatures: + # Installed there by hack/verify-golangci-lint.sh. + path: _output/local/bin/sortedfeatures.so + description: check if feature gates are sorted + original-url: k8s.io/kubernetes/hack/tools/golangci-lint/sortedfeatures + settings: + files: + - cmd/kubeadm/app/features/features.go + - pkg/features/kube_features.go + - staging/src/k8s.io/apiserver/pkg/features/kube_features.go + - staging/src/k8s.io/client-go/features/known_features.go + - staging/src/k8s.io/controller-manager/pkg/features/kube_features.go + - staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go + - test/e2e/feature/feature.go + - test/e2e/environment/environment.go kubeapilinter: path: _output/local/bin/kube-api-linter.so description: kube-api-linter and lints Kube like APIs based on API conventions and best practices. diff --git a/hack/golangci.yaml.in b/hack/golangci.yaml.in index 754ab67cb47..a3cc4fd873c 100644 --- a/hack/golangci.yaml.in +++ b/hack/golangci.yaml.in @@ -165,6 +165,7 @@ linters: - kubeapilinter - logcheck - revive + - sortedfeatures - staticcheck - testifylint - unused @@ -182,6 +183,13 @@ linters: settings: config: | {{include "hack/logcheck.conf" | indent 12 | trim}} + sortedfeatures: + # Installed there by hack/verify-golangci-lint.sh. + path: _output/local/bin/sortedfeatures.so + description: check if feature gates are sorted + original-url: k8s.io/kubernetes/hack/tools/golangci-lint/sortedfeatures + settings: + {{include "hack/tools/golangci-lint/sortedfeatures/config.yaml" | indent 10 | trim}} kubeapilinter: path: _output/local/bin/kube-api-linter.so description: kube-api-linter and lints Kube like APIs based on API conventions and best practices. diff --git a/hack/tools/golangci-lint/go.mod b/hack/tools/golangci-lint/go.mod index d186b63580a..db516bcef25 100644 --- a/hack/tools/golangci-lint/go.mod +++ b/hack/tools/golangci-lint/go.mod @@ -8,6 +8,11 @@ tool ( sigs.k8s.io/logtools/logcheck ) +require ( + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 + golang.org/x/tools v0.34.0 +) + require ( 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect @@ -21,7 +26,7 @@ require ( github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect - github.com/alecthomas/chroma/v2 v2.16.0 // indirect + github.com/alecthomas/chroma/v2 v2.17.2 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect @@ -78,7 +83,7 @@ require ( github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/go-printf-func-name v0.1.0 // indirect github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect - github.com/golangci/golangci-lint/v2 v2.1.5 // indirect + github.com/golangci/golangci-lint/v2 v2.1.6 // indirect github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect github.com/golangci/misspell v0.6.0 // indirect github.com/golangci/plugin-module-register v0.1.1 // indirect @@ -135,7 +140,6 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polyfloyd/go-errorlint v1.8.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -172,7 +176,7 @@ require ( github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/tdakkota/asciicheck v0.4.1 // indirect - github.com/tetafro/godot v1.5.0 // indirect + github.com/tetafro/godot v1.5.1 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect @@ -187,7 +191,7 @@ require ( github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.13.0 // indirect + go-simpler.org/musttag v0.13.1 // indirect go-simpler.org/sloglint v0.11.0 // indirect go.augendre.info/fatcontext v0.8.0 // indirect go.uber.org/atomic v1.7.0 // indirect @@ -196,11 +200,10 @@ require ( go.uber.org/zap v1.24.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.24.0 // indirect - golang.org/x/tools v0.32.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/hack/tools/golangci-lint/go.sum b/hack/tools/golangci-lint/go.sum index e285ce1ef5b..35af3581957 100644 --- a/hack/tools/golangci-lint/go.sum +++ b/hack/tools/golangci-lint/go.sum @@ -59,8 +59,8 @@ github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsu github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA= -github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= +github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= +github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= @@ -252,8 +252,8 @@ github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUP github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= -github.com/golangci/golangci-lint/v2 v2.1.5 h1:zDcxV8s7kgQW3cpQiVA633CZJnKN/0iEXibPDWO8sZo= -github.com/golangci/golangci-lint/v2 v2.1.5/go.mod h1:RGcjZLyl9fSVLqxdKMrknPlspC3TYETLoKXyRG06RDo= +github.com/golangci/golangci-lint/v2 v2.1.6 h1:LXqShFfAGM5BDzEOWD2SL1IzJAgUOqES/HRBsfKjI+w= +github.com/golangci/golangci-lint/v2 v2.1.6/go.mod h1:EPj+fgv4TeeBq3TcqaKZb3vkiV5dP4hHHKhXhEhzci8= github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8= github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= @@ -569,8 +569,8 @@ github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw= -github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4= +github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= @@ -608,8 +608,8 @@ gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= -go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= -go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= +go-simpler.org/musttag v0.13.1 h1:lw2sJyu7S1X8lc8zWUAdH42y+afdcCnHhWpnkWvd6vU= +go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k= go-simpler.org/sloglint v0.11.0 h1:JlR1X4jkbeaffiyjLtymeqmGDKBDO1ikC6rjiuFAOco= go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkyeW2Pvw= go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk= @@ -683,8 +683,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -725,8 +725,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -748,8 +748,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -802,8 +802,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -885,8 +885,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/hack/tools/golangci-lint/sortedfeatures/README.md b/hack/tools/golangci-lint/sortedfeatures/README.md new file mode 100644 index 00000000000..c5821410aaa --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/README.md @@ -0,0 +1,137 @@ +# SortedFeatures Linter + +This linter checks if feature gates in Kubernetes code are sorted alphabetically in const and var blocks. + +## Purpose + +In Kubernetes, feature gates should be listed in alphabetical, case-sensitive (upper before any lower case character) +order to reduce the risk of code conflicts and improve readability. This linter enforces this convention by checking +if feature gates are properly sorted. + +## How It Works + +The linter analyzes const and var blocks in specified files, extracts feature declarations, and checks if they are +sorted alphabetically by name. If they are not sorted, it reports an error with a detailed diff showing the current +order versus the expected order. + +NOTE: the linter only works for the following scenario where a `const` or a `var` block contains feature gates: +```go +const ( + FeatureA featuregate.Feature = "FeatureA" + FeatureB featuregate.Feature = "FeatureB" +) +``` +it will not work for cases where feature gates are defined in a different way, such as: +```go +const FeatureA featuregate.Feature = "FeatureA" +const FeatureB featuregate.Feature = "FeatureB" +``` + +## Installation + +### As a golangci-lint plugin + +1. Build the plugin: + +```bash +cd hack/tools/golangci-lint/sortedfeatures +go build -buildmode=plugin -o sortedfeatures.so ./plugin/example.go +``` + +2. Add the plugin to your `.golangci.yml` configuration: + +```yaml +linters: + settings: + custom: + sortedfeatures: + path: /path/to/sortedfeatures.so + description: Checks if feature gates are sorted alphabetically + original-url: k8s.io/kubernetes/hack/tools/golangci-lint/sortedfeatures + settings: + debug: false + files: + - path/to/additional/file.go +``` + +## Configuration Options + +The linter supports the following configuration options: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| debug | bool | false | Enable debug logging | +| files | []string | [] | Files to check for feature gate sorting. If specified, only these files will be checked (default files will be ignored) | + +You can specify files to check in your configuration: + +```yaml +# Using files +settings: + files: + - path/to/file.go + - another/path/file.go +``` + +Note: If `files` is specified, only those files will be checked and the default files will be ignored. If no files are +specified, the default set of Kubernetes feature gate files will be checked. + +## Usage + +The linter will check all const and var blocks in your code to ensure that feature gates are sorted alphabetically. + If they are not sorted, it will report an error with a detailed diff showing the current order versus the expected + order. + +### Enabling the linter + +Custom linters are enabled by default, but abide by the same rules as other linters. + +If the disable all option is specified either on command line or in `.golangci.yml` files `linters.disable-all: true`, +custom linters will be disabled; they can be re-enabled by adding them to the `linters.enable` list, +or providing the enabled option on the command line, `golangci-lint run -Esortedfeatures`. + +## Example + +```go +const ( + // These are properly sorted + FeatureA featuregate.Feature = "FeatureA" + FeatureB featuregate.Feature = "FeatureB" + FeatureC featuregate.Feature = "FeatureC" +) + +const ( + // These are NOT properly sorted and will trigger a linter error + FeatureB featuregate.Feature = "FeatureB" + FeatureA featuregate.Feature = "FeatureA" + FeatureC featuregate.Feature = "FeatureC" +) +``` + +## Files Checked + +By default, this linter checks the following files in the Kubernetes codebase: + +- `pkg/features/kube_features.go` +- `staging/src/k8s.io/apiserver/pkg/features/kube_features.go` +- `staging/src/k8s.io/client-go/features/known_features.go` +- `staging/src/k8s.io/controller-manager/pkg/features/kube_features.go` +- `staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go` +- `test/e2e/feature/feature.go` +- `test/e2e/environment/environment.go` + +## Integration with CI + +This linter is part of the Kubernetes CI pipeline and helps ensure that all feature gates are properly sorted +across the codebase. It's recommended to run this linter locally before submitting pull requests that modify +feature gates. + +## Troubleshooting + +If you encounter issues with the linter: + +1. Enable debug mode in your configuration +2. Check that the plugin is correctly built and referenced in your golangci-lint configuration +3. Verify that the files you want to check are either in the default list or explicitly specified in your configuration + +For more information on custom linters in golangci-lint, refer to the [official documentation](https://golangci-lint.run/contributing/new-linters/). diff --git a/hack/tools/golangci-lint/sortedfeatures/config.yaml b/hack/tools/golangci-lint/sortedfeatures/config.yaml new file mode 100644 index 00000000000..a7de0d880e4 --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/config.yaml @@ -0,0 +1,9 @@ +files: + - cmd/kubeadm/app/features/features.go + - pkg/features/kube_features.go + - staging/src/k8s.io/apiserver/pkg/features/kube_features.go + - staging/src/k8s.io/client-go/features/known_features.go + - staging/src/k8s.io/controller-manager/pkg/features/kube_features.go + - staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go + - test/e2e/feature/feature.go + - test/e2e/environment/environment.go diff --git a/hack/tools/golangci-lint/sortedfeatures/example.golangci.yml b/hack/tools/golangci-lint/sortedfeatures/example.golangci.yml new file mode 100644 index 00000000000..e099027b1e9 --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/example.golangci.yml @@ -0,0 +1,25 @@ +linters: + settings: + custom: + sortedfeatures: + path: /path/to/sortedfeatures.so + description: Checks if feature gates are sorted alphabetically + original-url: k8s.io/kubernetes/hack/tools/golangci-lint/sortedfeatures + settings: + debug: false + # Specify files to check (will ignore default files) + files: + - path/to/feature_gates.go + - another/path/to/feature_gates.go + +run: + # These paths control which files golangci-lint will analyze + # This is separate from the sortedfeatures plugin's files setting + paths: + - staging/src/k8s.io/apiserver/pkg/features/kube_features.go + - pkg/features/kube_features.go + - staging/src/k8s.io/client-go/features/known_features.go + - staging/src/k8s.io/controller-manager/pkg/features/kube_features.go + - staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go + - test/e2e/feature/feature.go + - test/e2e/environment/environment.go diff --git a/hack/tools/golangci-lint/sortedfeatures/main.go b/hack/tools/golangci-lint/sortedfeatures/main.go new file mode 100644 index 00000000000..b300577399c --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/main.go @@ -0,0 +1,27 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "golang.org/x/tools/go/analysis/singlechecker" + + "k8s.io/kubernetes/hack/tools/golangci-lint/sortedfeatures/pkg" +) + +func main() { + singlechecker.Main(pkg.NewAnalyzer()) +} diff --git a/hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures.go b/hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures.go new file mode 100644 index 00000000000..f606567376e --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures.go @@ -0,0 +1,395 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package sortedfeatures implements a linter that checks if feature gates are sorted alphabetically. +package pkg + +import ( + "bufio" + "fmt" + "go/ast" + "go/token" + "sort" + "strings" + + "github.com/pmezard/go-difflib/difflib" + "golang.org/x/tools/go/analysis" +) + +// Config holds the configuration for the sortedfeatures analyzer +type Config struct { + // Files contains files to check. If specified, only these files will be checked. + Files []string + // Debug enables debug logging + Debug bool +} + +// NewAnalyzer returns a new sortedfeatures analyzer. +func NewAnalyzer() *analysis.Analyzer { + return NewAnalyzerWithConfig(Config{Debug: true}) +} + +// NewAnalyzerWithConfig returns a new sortedfeatures analyzer with the given configuration. +func NewAnalyzerWithConfig(config Config) *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "sortedfeatures", + Doc: "Checks if feature gates are sorted alphabetically in const and var blocks", + Run: func(pass *analysis.Pass) (interface{}, error) { + if config.Debug { + fmt.Printf("Processing...\n") + } + return run(pass, config) + }, + } +} + +func isTargetFile(configFiles []string, filename string) bool { + for _, item := range configFiles { + if strings.HasSuffix(filename, item) { + return true + } + } + return false +} + +func run(pass *analysis.Pass, config Config) (interface{}, error) { + for _, file := range pass.Files { + filename := pass.Fset.File(file.Pos()).Name() + + if !isTargetFile(config.Files, filename) { + continue + } + + // Check all declarations in the file + for _, decl := range file.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + + // Process var and const blocks for regular feature gates + if (genDecl.Tok == token.VAR || genDecl.Tok == token.CONST) && len(genDecl.Specs) > 1 { + // Extract features with their comments + features := extractFeatures(genDecl, file.Comments) + + // Skip if no features were found + if len(features) > 1 { + // Sort features + sortedFeatures := sortFeatures(features) + + // Check if the order has changed + orderChanged := hasOrderChanged(features, sortedFeatures) + + if orderChanged { + // Generate a diff to show what's wrong + reportSortingIssue(pass, genDecl, features, sortedFeatures) + } + } + } + + // Check for maps with feature gates as keys + if genDecl.Tok == token.VAR { + checkFeatureGateMaps(pass, genDecl) + } + } + } + return nil, nil +} + +// checkFeatureGateMaps checks if maps with feature gates as keys have their keys sorted alphabetically +func checkFeatureGateMaps(pass *analysis.Pass, genDecl *ast.GenDecl) { + for _, spec := range genDecl.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok || len(valueSpec.Names) == 0 || len(valueSpec.Values) == 0 { + continue + } + + // Check each value to see if it's a map with feature gates + for _, value := range valueSpec.Values { + compositeLit, ok := value.(*ast.CompositeLit) + if !ok { + continue + } + + // Check if this is a map type + mapType, ok := compositeLit.Type.(*ast.MapType) + if !ok { + continue + } + + // Check if the key type is featuregate.Feature or contains "Feature" + isFeatureGateMap := false + + // Check for SelectorExpr (e.g., featuregate.Feature) + if selectorExpr, ok := mapType.Key.(*ast.SelectorExpr); ok { + if selectorExpr.Sel.Name == "Feature" { + isFeatureGateMap = true + } + } + + // Check for Ident (e.g., Feature) + if ident, ok := mapType.Key.(*ast.Ident); ok { + if ident.Name == "Feature" { + isFeatureGateMap = true + } + } + + if !isFeatureGateMap { + continue + } + + // This is a map with feature gates as keys + var features []Feature + for _, elt := range compositeLit.Elts { + keyValueExpr, ok := elt.(*ast.KeyValueExpr) + if !ok { + continue + } + + // Get the key, which should be a feature gate identifier + var featureName string + + // Handle different types of keys + switch key := keyValueExpr.Key.(type) { + case *ast.Ident: + featureName = key.Name + case *ast.SelectorExpr: + // For selector expressions like genericfeatures.APIServerIdentity + if x, ok := key.X.(*ast.Ident); ok { + featureName = x.Name + "." + key.Sel.Name + } else { + continue + } + default: + continue + } + + features = append(features, Feature{ + Name: featureName, + Comments: []string{}, // No comments for map keys + }) + } + + if len(features) <= 1 { + continue + } + + // Sort features + sortedFeatures := sortFeatures(features) + + // Check if the order has changed + orderChanged := hasOrderChanged(features, sortedFeatures) + + if orderChanged { + // Generate a diff to show what's wrong + reportMapSortingIssue(pass, genDecl, valueSpec.Names[0].Name, features, sortedFeatures) + } + } + } +} + +// Feature represents a feature declaration with its associated comments +type Feature struct { + Name string // Name of the feature + Comments []string // Comments associated with the feature +} + +// extractFeatures extracts features from a GenDecl +func extractFeatures(decl *ast.GenDecl, comments []*ast.CommentGroup) []Feature { + var features []Feature + + for _, spec := range decl.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok || len(valueSpec.Names) == 0 { + continue + } + + // Get the name of the feature + name := valueSpec.Names[0].Name + + // Get comments for this feature + var featureComments []string + + // Check for doc comments directly on the value spec + if valueSpec.Doc != nil { + for _, comment := range valueSpec.Doc.List { + featureComments = append(featureComments, comment.Text) + } + } else { + // Look for comments before this spec + for _, cg := range comments { + if cg.End()+1 == valueSpec.Pos() { + for _, comment := range cg.List { + featureComments = append(featureComments, comment.Text) + } + } + } + } + + features = append(features, Feature{ + Name: name, + Comments: featureComments, + }) + } + + return features +} + +// sortFeatures sorts features alphabetically by name +func sortFeatures(features []Feature) []Feature { + sorted := make([]Feature, len(features)) + copy(sorted, features) + + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].Name < sorted[j].Name + }) + + return sorted +} + +// hasOrderChanged checks if the order of features has changed +func hasOrderChanged(original, sorted []Feature) bool { + if len(original) != len(sorted) { + return true + } + + for i := range original { + if original[i].Name != sorted[i].Name { + return true + } + } + + return false +} + +// reportSortingIssue reports a linting issue with a diff showing the correct order +func reportSortingIssue(pass *analysis.Pass, decl *ast.GenDecl, current, sorted []Feature) { + // Generate the original source code + originalSource := generateSourceCode(decl.Tok, current) + + // Generate the sorted source code + sortedSource := generateSourceCode(decl.Tok, sorted) + + // Create a unified diff between the original and sorted source + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(originalSource), + B: difflib.SplitLines(sortedSource), + FromFile: "Current", + ToFile: "Expected", + Context: 3, + } + + diffText, err := difflib.GetUnifiedDiffString(diff) + if err != nil { + pass.Reportf(decl.Pos(), "not sorted alphabetically (error creating diff: %v)", err) + return + } + + // Report the issue with the diff + pass.Reportf(decl.Pos(), "not sorted alphabetically:\n%s\n", stripHeader(diffText, 3)) +} + +func stripHeader(input string, n int) string { + scanner := bufio.NewScanner(strings.NewReader(input)) + var result strings.Builder + lineCount := 0 + + for scanner.Scan() { + lineCount++ + if lineCount > n { + result.WriteString(scanner.Text() + "\n") + } + } + + return strings.TrimSuffix(result.String(), "\n") +} + +// reportMapSortingIssue reports a linting issue for unsorted map keys +func reportMapSortingIssue(pass *analysis.Pass, decl *ast.GenDecl, mapName string, current, sorted []Feature) { + // Generate the original source code + originalSource := generateMapSourceCode(current) + + // Generate the sorted source code + sortedSource := generateMapSourceCode(sorted) + + // Create a unified diff between the original and sorted source + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(originalSource), + B: difflib.SplitLines(sortedSource), + FromFile: "Current Map Keys", + ToFile: "Expected Map Keys", + Context: 3, + } + + diffText, err := difflib.GetUnifiedDiffString(diff) + if err != nil { + pass.Reportf(decl.Pos(), "map '%s' keys not sorted alphabetically (error creating diff: %v)", mapName, err) + return + } + + // Report the issue with the diff + pass.Reportf(decl.Pos(), "map '%s' keys not sorted alphabetically:\n%s\n", mapName, stripHeader(diffText, 3)) +} + +// generateMapSourceCode recreates the source code for map keys +func generateMapSourceCode(features []Feature) string { + var sb strings.Builder + + sb.WriteString("map[featuregate.Feature]featuregate.VersionedSpecs{\n") + + // Add each feature key + for _, feature := range features { + sb.WriteString("\t") + sb.WriteString(feature.Name) + sb.WriteString(": ...,\n") + } + + sb.WriteString("}") + + return sb.String() +} + +// generateSourceCode recreates the source code from features +func generateSourceCode(tokenType token.Token, features []Feature) string { + var sb strings.Builder + + // Start the block with the token type (var or const) + sb.WriteString(tokenType.String()) + sb.WriteString(" (\n") + + // Add each feature with its comments + for _, feature := range features { + // Add comments + for _, comment := range feature.Comments { + sb.WriteString("\t") + sb.WriteString(comment) + sb.WriteString("\n") + } + + // Add the feature declaration + sb.WriteString("\t") + sb.WriteString(feature.Name) + sb.WriteString(" = ") + // Since we don't have the actual value, we'll use a placeholder + sb.WriteString("value") + sb.WriteString("\n\n") + } + + // Close the block + sb.WriteString(")") + + return sb.String() +} diff --git a/hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures_test.go b/hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures_test.go new file mode 100644 index 00000000000..9181808a5c6 --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures_test.go @@ -0,0 +1,762 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pkg + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "strings" + "testing" + + "github.com/pmezard/go-difflib/difflib" + "golang.org/x/tools/go/analysis" +) + +func TestNewAnalyzer(t *testing.T) { + analyzer := NewAnalyzer() + if analyzer.Name != "sortedfeatures" { + t.Errorf("Expected analyzer name to be 'sortedfeatures', got '%s'", analyzer.Name) + } +} + +func TestNewAnalyzerWithConfig(t *testing.T) { + config := Config{ + Debug: true, + Files: []string{"test.go"}, + } + analyzer := NewAnalyzerWithConfig(config) + if analyzer.Name != "sortedfeatures" { + t.Errorf("Expected analyzer name to be 'sortedfeatures', got '%s'", analyzer.Name) + } +} + +func TestRunWithEmptyFileList(t *testing.T) { + // Test that the analyzer handles empty file list gracefully + pass := &analysis.Pass{ + Fset: token.NewFileSet(), + Files: []*ast.File{}, + ResultOf: make(map[*analysis.Analyzer]interface{}), + Report: func(d analysis.Diagnostic) { t.Errorf("Report should not be called") }, + } + + // Run the analyzer's run function directly + result, err := run(pass, Config{}) + if err != nil { + t.Errorf("run returned an error: %v", err) + } + if result != nil { + t.Errorf("Expected nil result, got: %v", result) + } +} + +func TestRunWithDefaultFiles(t *testing.T) { + // Test that the analyzer works with default files + // Create a mock analysis.Pass + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "pkg/features/kube_features.go", ` +package features + +type Feature string + +const ( + FeatureA Feature = "FeatureA" + FeatureB Feature = "FeatureB" +) +`, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + pass := &analysis.Pass{ + Fset: fset, + Files: []*ast.File{f}, + ResultOf: make(map[*analysis.Analyzer]interface{}), + Report: func(d analysis.Diagnostic) {}, + } + + // Run the analyzer's run function directly + _, err = run(pass, Config{}) + if err != nil { + t.Errorf("run returned an error: %v", err) + } +} + +func TestRunWithNonTargetFile(t *testing.T) { + // Test that the analyzer ignores non-target files + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "some/random/file.go", ` +package features + +type Feature string + +const ( + FeatureB Feature = "FeatureB" + FeatureA Feature = "FeatureA" // Unsorted, but should be ignored +) +`, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + var reportCalled bool + pass := &analysis.Pass{ + Fset: fset, + Files: []*ast.File{f}, + ResultOf: make(map[*analysis.Analyzer]interface{}), + Report: func(d analysis.Diagnostic) { reportCalled = true }, + } + + // Run the analyzer's run function directly + _, err = run(pass, Config{}) + if err != nil { + t.Errorf("run returned an error: %v", err) + } + + // Check that Report was NOT called for non-target files + if reportCalled { + t.Errorf("Report should not be called for non-target files") + } +} + +func TestRunWithSpecifiedFiles(t *testing.T) { + // Create a temporary file for testing + tmpFile, err := os.CreateTemp("", "custom_file_*.go") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Write test content to the file + testContent := ` +package features + +type Feature string + +const ( + FeatureA Feature = "FeatureA" + FeatureB Feature = "FeatureB" +) +` + if _, err := tmpFile.Write([]byte(testContent)); err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + if err := tmpFile.Close(); err != nil { + t.Fatalf("Failed to close temporary file: %v", err) + } + + // Test that the analyzer works with specified files + config := Config{ + Files: []string{tmpFile.Name()}, + } + + // Create a mock analysis.Pass + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, tmpFile.Name(), testContent, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + pass := &analysis.Pass{ + Fset: fset, + Files: []*ast.File{f}, + ResultOf: make(map[*analysis.Analyzer]interface{}), + Report: func(d analysis.Diagnostic) {}, + } + + // Run the analyzer's run function directly + _, err = run(pass, config) + if err != nil { + t.Errorf("run returned an error: %v", err) + } +} + +func TestExtractFeatures(t *testing.T) { + src := ` +package test + +type Feature string + +const ( + // Comment for FeatureA + FeatureA Feature = "FeatureA" + + // Comment for FeatureB + FeatureB Feature = "FeatureB" +) +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + var foundDecl *ast.GenDecl + ast.Inspect(f, func(n ast.Node) bool { + if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.CONST { + foundDecl = decl + return false + } + return true + }) + + if foundDecl == nil { + t.Fatalf("Failed to find const declaration") + } + + features := extractFeatures(foundDecl, f.Comments) + + if len(features) != 2 { + t.Errorf("Expected 2 features, got %d", len(features)) + } + + if features[0].Name != "FeatureA" { + t.Errorf("Expected first feature to be FeatureA, got %s", features[0].Name) + } + + if features[1].Name != "FeatureB" { + t.Errorf("Expected second feature to be FeatureB, got %s", features[1].Name) + } + + if len(features[0].Comments) == 0 { + t.Errorf("Expected comments for FeatureA, got none") + } +} + +func TestSortFeatures(t *testing.T) { + features := []Feature{ + {Name: "FeatureB"}, + {Name: "FeatureA"}, + {Name: "FeatureC"}, + } + + sorted := sortFeatures(features) + + if sorted[0].Name != "FeatureA" { + t.Errorf("Expected first feature to be FeatureA, got %s", sorted[0].Name) + } + + if sorted[1].Name != "FeatureB" { + t.Errorf("Expected second feature to be FeatureB, got %s", sorted[1].Name) + } + + if sorted[2].Name != "FeatureC" { + t.Errorf("Expected third feature to be FeatureC, got %s", sorted[2].Name) + } +} + +func TestHasOrderChanged(t *testing.T) { + original := []Feature{ + {Name: "FeatureB"}, + {Name: "FeatureA"}, + {Name: "FeatureC"}, + } + + sorted := []Feature{ + {Name: "FeatureA"}, + {Name: "FeatureB"}, + {Name: "FeatureC"}, + } + + if !hasOrderChanged(original, sorted) { + t.Errorf("Expected hasOrderChanged to return true for different orders") + } + + if hasOrderChanged(sorted, sorted) { + t.Errorf("Expected hasOrderChanged to return false for same order") + } +} + +func TestReportSortingIssue(t *testing.T) { + current := []Feature{ + {Name: "FeatureB", Comments: []string{"// Comment for FeatureB"}}, + {Name: "FeatureA", Comments: []string{"// Comment for FeatureA"}}, + {Name: "FeatureC", Comments: []string{"// Comment for FeatureC"}}, + } + + sorted := []Feature{ + {Name: "FeatureA", Comments: []string{"// Comment for FeatureA"}}, + {Name: "FeatureB", Comments: []string{"// Comment for FeatureB"}}, + {Name: "FeatureC", Comments: []string{"// Comment for FeatureC"}}, + } + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", ` +package test + +const ( + FeatureB = "FeatureB" + FeatureA = "FeatureA" + FeatureC = "FeatureC" +) +`, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + var foundDecl *ast.GenDecl + ast.Inspect(f, func(n ast.Node) bool { + if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.CONST { + foundDecl = decl + return false + } + return true + }) + + if foundDecl == nil { + t.Fatalf("Failed to find const declaration") + } + + var reportMessage string + pass := &analysis.Pass{ + Fset: fset, + Files: []*ast.File{f}, + ResultOf: make(map[*analysis.Analyzer]interface{}), + Report: func(d analysis.Diagnostic) { reportMessage = d.Message }, + } + + reportSortingIssue(pass, foundDecl, current, sorted) + + if reportMessage == "" { + t.Errorf("Expected Report to be called") + } + + // Check that the diff shows the features in the correct order + if !strings.Contains(reportMessage, "FeatureB") || !strings.Contains(reportMessage, "FeatureA") || !strings.Contains(reportMessage, "FeatureC") { + t.Errorf("Expected diff to contain all feature names, got: %s", reportMessage) + } + + // Check that the diff contains the expected content + expectedContent := "const (" + if !strings.Contains(reportMessage, expectedContent) { + t.Errorf("Expected diff to contain %q, got: %s", expectedContent, reportMessage) + } +} + +func TestSortedFeatures(t *testing.T) { + // Test with properly sorted features + src := ` +package test + +type Feature string + +const ( + // These are properly sorted + FeatureA Feature = "FeatureA" + FeatureB Feature = "FeatureB" + FeatureC Feature = "FeatureC" +) +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + var foundDecl *ast.GenDecl + ast.Inspect(f, func(n ast.Node) bool { + if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.CONST { + foundDecl = decl + return false + } + return true + }) + + if foundDecl == nil { + t.Fatalf("Failed to find const declaration") + } + + // Extract features + features := extractFeatures(foundDecl, f.Comments) + + // Sort features + sortedFeatures := sortFeatures(features) + + // Check if order changed + if hasOrderChanged(features, sortedFeatures) { + t.Errorf("Expected features to be already sorted") + } +} + +func TestUnsortedFeatures(t *testing.T) { + // Test with unsorted features + src := ` +package test + +type Feature string + +const ( + // These are NOT properly sorted + FeatureB Feature = "FeatureB" + FeatureA Feature = "FeatureA" + FeatureC Feature = "FeatureC" +) +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + var foundDecl *ast.GenDecl + ast.Inspect(f, func(n ast.Node) bool { + if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.CONST { + foundDecl = decl + return false + } + return true + }) + + if foundDecl == nil { + t.Fatalf("Failed to find const declaration") + } + + // Extract features + features := extractFeatures(foundDecl, f.Comments) + + // Sort features + sortedFeatures := sortFeatures(features) + + // Check if order changed + if !hasOrderChanged(features, sortedFeatures) { + t.Errorf("Expected features to be unsorted") + } +} + +func TestVarBlockFeatures(t *testing.T) { + // Test with var block features (like in test/e2e/feature/feature.go) + src := ` +package test + +import "k8s.io/kubernetes/test/e2e/framework" + +var ( + // These are properly sorted + FeatureA = framework.WithFeature(framework.ValidFeatures.Add("FeatureA")) + FeatureB = framework.WithFeature(framework.ValidFeatures.Add("FeatureB")) + FeatureC = framework.WithFeature(framework.ValidFeatures.Add("FeatureC")) +) +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + var foundDecl *ast.GenDecl + ast.Inspect(f, func(n ast.Node) bool { + if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.VAR { + foundDecl = decl + return false + } + return true + }) + + if foundDecl == nil { + t.Fatalf("Failed to find var declaration") + } + + // Extract features + features := extractFeatures(foundDecl, f.Comments) + + // Sort features + sortedFeatures := sortFeatures(features) + + // Check if order changed + if hasOrderChanged(features, sortedFeatures) { + t.Errorf("Expected features to be already sorted") + } +} + +func TestSingleItemBlock(t *testing.T) { + // Test with a block containing only one item + src := ` +package test + +type Feature string + +const ( + // Single item, should be skipped + FeatureA Feature = "FeatureA" +) +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + var foundDecl *ast.GenDecl + ast.Inspect(f, func(n ast.Node) bool { + if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.CONST { + foundDecl = decl + return false + } + return true + }) + + if foundDecl == nil { + t.Fatalf("Failed to find const declaration") + } + + // Extract features + features := extractFeatures(foundDecl, f.Comments) + + if len(features) != 1 { + t.Errorf("Expected 1 feature, got %d", len(features)) + } +} + +func TestNonParenthesizedDeclarationsNotProcessed(t *testing.T) { + // Test with non-parenthesized declarations + src := ` +package test + +type Feature string + +// First feature +const FeatureA Feature = "FeatureA" + +// Second feature +const FeatureB Feature = "FeatureB" + +// Third feature +const FeatureC Feature = "FeatureC" +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + // Create a mock analysis.Pass + var reportCalled bool + pass := &analysis.Pass{ + Fset: fset, + Files: []*ast.File{f}, + ResultOf: make(map[*analysis.Analyzer]interface{}), + Report: func(d analysis.Diagnostic) { reportCalled = true }, + } + + // Configure the analyzer to treat test.go as a target file + config := Config{ + Files: []string{"test.go"}, + } + + // Run the analyzer + _, err = run(pass, config) + if err != nil { + t.Errorf("run returned an error: %v", err) + } + + // Check that Report was NOT called, since non-parenthesized declarations are skipped + if reportCalled { + t.Errorf("Expected Report not to be called for non-parenthesized declarations") + } +} + +// TestAnalyzerRunSimulatingGolangciLint is a test that simulates how golangci-lint would run the analyzer +// by creating a mock analysis.Pass and calling the Run method directly. If you run this test from the root +// of the repository, it will check all default target files defined in the analyzer's config without needing +// to run golangci-lint itself. If run from anywhere else, it will skip the test as the files won't be found. +func TestAnalyzerRunSimulatingGolangciLint(t *testing.T) { + defaultTargetFiles := []string{ + "pkg/features/kube_features.go", + "staging/src/k8s.io/apiserver/pkg/features/kube_features.go", + "staging/src/k8s.io/client-go/features/known_features.go", + "staging/src/k8s.io/controller-manager/pkg/features/kube_features.go", + "staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go", + "test/e2e/feature/feature.go", + "test/e2e/environment/environment.go", + } + for _, filename := range defaultTargetFiles { + t.Run(filename, func(t *testing.T) { + + // Check if file exists + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Skip("File not found; skipping test") + } + + // Create a file set and parse a simple source + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + // Create a mock analysis.Pass similar to what golangci-lint would create + var diagnostics []analysis.Diagnostic + pass := &analysis.Pass{ + Fset: fset, + Files: []*ast.File{f}, + ResultOf: make(map[*analysis.Analyzer]interface{}), + Report: func(d analysis.Diagnostic) { + diagnostics = append(diagnostics, d) + }, + } + + // Get the analyzer + analyzer := NewAnalyzerWithConfig(Config{Debug: true, Files: defaultTargetFiles}) + + // Call the Run method directly as golangci-lint would + result, err := analyzer.Run(pass) + + // Check that no error was returned + if err != nil { + t.Errorf("Analyzer Run returned an error: %v", err) + } + + // Check that result is nil (our analyzer doesn't return any result) + if result != nil { + t.Errorf("Expected nil result for %s, got: %v", filename, result) + } + + // Check that no diagnostics were reported for properly sorted features + if len(diagnostics) > 0 { + t.Errorf("Expected no diagnostics for %s, got: %v", filename, diagnostics) + } + }) + } +} + +func TestDebugLogging(t *testing.T) { + // This test doesn't actually verify the debug output, but ensures the code path is covered + config := Config{ + Debug: true, + } + + // Create a mock analysis.Pass + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "pkg/features/kube_features.go", ` +package features + +type Feature string + +const ( + FeatureA Feature = "FeatureA" + FeatureB Feature = "FeatureB" +) +`, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + + pass := &analysis.Pass{ + Fset: fset, + Files: []*ast.File{f}, + ResultOf: make(map[*analysis.Analyzer]interface{}), + Report: func(d analysis.Diagnostic) {}, + } + + // Run the analyzer's run function directly with debug enabled + _, err = run(pass, config) + if err != nil { + t.Errorf("run returned an error: %v", err) + } +} + +func TestGenerateSourceCode(t *testing.T) { + features := []Feature{ + {Name: "FeatureB", Comments: []string{"// Comment for FeatureB"}}, + {Name: "FeatureA", Comments: []string{"// Comment for FeatureA"}}, + } + + // Test const block generation + constSource := generateSourceCode(token.CONST, features) + expectedConstSource := `const ( + // Comment for FeatureB + FeatureB = value + + // Comment for FeatureA + FeatureA = value + +)` + + if constSource != expectedConstSource { + t.Errorf("Expected const source:\n%s\n\nGot:\n%s", expectedConstSource, constSource) + } + + // Test var block generation + varSource := generateSourceCode(token.VAR, features) + expectedVarSource := `var ( + // Comment for FeatureB + FeatureB = value + + // Comment for FeatureA + FeatureA = value + +)` + + if varSource != expectedVarSource { + t.Errorf("Expected var source:\n%s\n\nGot:\n%s", expectedVarSource, varSource) + } +} + +func TestDiffGeneration(t *testing.T) { + // Create unsorted features + unsorted := []Feature{ + {Name: "FeatureC", Comments: []string{"// Comment for FeatureC"}}, + {Name: "FeatureA", Comments: []string{"// Comment for FeatureA"}}, + {Name: "FeatureB", Comments: []string{"// Comment for FeatureB"}}, + } + + // Create sorted features + sorted := []Feature{ + {Name: "FeatureA", Comments: []string{"// Comment for FeatureA"}}, + {Name: "FeatureB", Comments: []string{"// Comment for FeatureB"}}, + {Name: "FeatureC", Comments: []string{"// Comment for FeatureC"}}, + } + + // Generate source code for both + originalSource := generateSourceCode(token.CONST, unsorted) + sortedSource := generateSourceCode(token.CONST, sorted) + + // Create a unified diff + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(originalSource), + B: difflib.SplitLines(sortedSource), + FromFile: "Current", + ToFile: "Expected", + Context: 3, + } + + diffText, err := difflib.GetUnifiedDiffString(diff) + if err != nil { + t.Fatalf("Failed to generate diff: %v", err) + } + + // Strip header to match the actual implementation + diffText = stripHeader(diffText, 3) + + // Check that the diff contains expected content + expectedLines := []string{ + "-\t// Comment for FeatureC", + "-\tFeatureC = value", + } + + for _, line := range expectedLines { + if !strings.Contains(diffText, line) { + t.Errorf("Expected diff to contain line: %q, but it was not found in:\n%s", line, diffText) + } + } + + // Check that the diff shows the correct order change + if !strings.Contains(diffText, "FeatureA") || !strings.Contains(diffText, "FeatureB") || !strings.Contains(diffText, "FeatureC") { + t.Errorf("Expected diff to contain all feature names, got: %s", diffText) + } +} diff --git a/hack/tools/golangci-lint/sortedfeatures/plugin/plugin.go b/hack/tools/golangci-lint/sortedfeatures/plugin/plugin.go new file mode 100644 index 00000000000..929ec009447 --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/plugin/plugin.go @@ -0,0 +1,96 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This must be package main +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "golang.org/x/tools/go/analysis" + + "k8s.io/kubernetes/hack/tools/golangci-lint/sortedfeatures/pkg" +) + +type analyzerPlugin struct{} + +func (*analyzerPlugin) GetAnalyzers() []*analysis.Analyzer { + return []*analysis.Analyzer{pkg.NewAnalyzer()} +} + +// AnalyzerPlugin is the entry point for golangci-lint. +var AnalyzerPlugin analyzerPlugin + +// settings defines the configuration options for the sortedfeatures linter +type settings struct { + // Debug enables debug logging + Debug bool `json:"debug"` + // Files specifies which files to check + Files []string `json:"files"` +} + +// List of default files to check for feature gate sorting +var defaultTargetFiles = []string{ + "pkg/features/kube_features.go", + "staging/src/k8s.io/apiserver/pkg/features/kube_features.go", + "staging/src/k8s.io/client-go/features/known_features.go", + "staging/src/k8s.io/controller-manager/pkg/features/kube_features.go", + "staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go", + "test/e2e/feature/feature.go", + "test/e2e/environment/environment.go", +} + +// New is the entry point for golangci-lint plugin system +func New(pluginSettings interface{}) ([]*analysis.Analyzer, error) { + // Create default config + config := pkg.Config{} + + // Parse settings if provided + if pluginSettings != nil { + var s settings + // Convert settings to JSON and back to our struct for easier handling + var buffer bytes.Buffer + if err := json.NewEncoder(&buffer).Encode(pluginSettings); err != nil { + return nil, fmt.Errorf("encoding settings as internal JSON buffer: %v", err) + } + + decoder := json.NewDecoder(&buffer) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&s); err != nil { + return nil, fmt.Errorf("decoding settings from internal JSON buffer: %v", err) + } + + // Apply settings to config + config.Debug = s.Debug + config.Files = append(config.Files, s.Files...) + if len(config.Files) == 0 { + // If no files are specified, use the default target files + config.Files = defaultTargetFiles + } + + if config.Debug { + fmt.Printf("sortedfeatures settings: %+v\n", s) + fmt.Printf("final config: %+v\n", config) + } + } + + // Get the analyzer with config + analyzer := pkg.NewAnalyzerWithConfig(config) + + // Return the analyzer + return []*analysis.Analyzer{analyzer}, nil +} diff --git a/hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/sorted.go b/hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/sorted.go new file mode 100644 index 00000000000..005583c1caf --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/sorted.go @@ -0,0 +1,17 @@ +package testdata + +type Feature string + +const ( + // These are properly sorted + FeatureA Feature = "FeatureA" + FeatureB Feature = "FeatureB" + FeatureC Feature = "FeatureC" +) + +var ( + // These are properly sorted + VarFeatureA Feature = "VarFeatureA" + VarFeatureB Feature = "VarFeatureB" + VarFeatureC Feature = "VarFeatureC" +) diff --git a/hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/unsorted.go b/hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/unsorted.go new file mode 100644 index 00000000000..5259f988d90 --- /dev/null +++ b/hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/unsorted.go @@ -0,0 +1,17 @@ +package testdata + +type UnsortedFeature string + +const ( + // These are NOT properly sorted and will trigger a linter error + UnsortedFeatureB UnsortedFeature = "UnsortedFeatureB" + UnsortedFeatureA UnsortedFeature = "UnsortedFeatureA" + UnsortedFeatureC UnsortedFeature = "UnsortedFeatureC" +) + +var ( + // These are NOT properly sorted and will trigger a linter error + UnsortedVarFeatureC UnsortedFeature = "VarUnsortedFeatureC" + UnsortedVarFeatureA UnsortedFeature = "VarUnsortedFeatureA" + UnsortedVarFeatureB UnsortedFeature = "VarUnsortedFeatureB" +) diff --git a/hack/verify-golangci-lint.sh b/hack/verify-golangci-lint.sh index 893e09c2709..34f409b88a7 100755 --- a/hack/verify-golangci-lint.sh +++ b/hack/verify-golangci-lint.sh @@ -129,7 +129,7 @@ kube::util::ensure-temp-dir # Installing from source (https://golangci-lint.run/welcome/install/#install-from-sources) # is not recommended, but for Kubernetes we prefer it because it avoids the need for # pre-built binaries for different platforms and gives more insights on dependencies. -echo "installing golangci-lint, logcheck and kube-api-linter plugins from hack/tools/golangci-lint into ${GOBIN}" +echo "installing golangci-lint, logcheck kube-api-linter and sortedfeatures plugins from hack/tools/golangci-lint into ${GOBIN}" GOTOOLCHAIN="$(kube::golang::hack_tools_gotoolchain)" go -C "${KUBE_ROOT}/hack/tools/golangci-lint" install github.com/golangci/golangci-lint/v2/cmd/golangci-lint if [ "${golangci_config}" ]; then # Plugins cannot be used without a config. @@ -137,6 +137,7 @@ if [ "${golangci_config}" ]; then # (on purpose: https://github.com/golang/go/issues/64964). GOTOOLCHAIN="$(kube::golang::hack_tools_gotoolchain)" go -C "${KUBE_ROOT}/hack/tools/golangci-lint" build -o "${GOBIN}/logcheck.so" -buildmode=plugin sigs.k8s.io/logtools/logcheck/plugin GOTOOLCHAIN="$(kube::golang::hack_tools_gotoolchain)" go -C "${KUBE_ROOT}/hack/tools/golangci-lint" build -o "${GOBIN}/kube-api-linter.so" -buildmode=plugin sigs.k8s.io/kube-api-linter/pkg/plugin + GOTOOLCHAIN="$(kube::golang::hack_tools_gotoolchain)" go -C "${KUBE_ROOT}/hack/tools/golangci-lint" build -o "${GOBIN}/sortedfeatures.so" -buildmode=plugin k8s.io/kubernetes/hack/tools/golangci-lint/sortedfeatures/plugin fi # Verify that the given config is valid. "golangci-lint run" does not diff --git a/hack/verify-test-featuregates.sh b/hack/verify-test-featuregates.sh index bd1f1b12375..80f7af87ac2 100755 --- a/hack/verify-test-featuregates.sh +++ b/hack/verify-test-featuregates.sh @@ -41,10 +41,10 @@ if [[ -n "${direct_sets}" ]]; then rc=1 fi - +export LC_ALL=C # ensure all generic features are added in alphabetic order lines=$(git grep 'genericfeatures[.].*:' -- pkg/features/kube_features.go) -sorted_lines=$(echo "$lines" | sort -f) +sorted_lines=$(echo "$lines" | sort) if [[ "$lines" != "$sorted_lines" ]]; then echo "Generic features in pkg/features/kube_features.go not sorted" >&2 echo >&2