mirror of
https://github.com/outbackdingo/kubernetes.git
synced 2026-01-27 18:19:28 +00:00
Add linter to report on unsorted feature gates
Signed-off-by: Davanum Srinivas <davanum@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
137
hack/tools/golangci-lint/sortedfeatures/README.md
Normal file
137
hack/tools/golangci-lint/sortedfeatures/README.md
Normal file
@@ -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/).
|
||||
9
hack/tools/golangci-lint/sortedfeatures/config.yaml
Normal file
9
hack/tools/golangci-lint/sortedfeatures/config.yaml
Normal file
@@ -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
|
||||
25
hack/tools/golangci-lint/sortedfeatures/example.golangci.yml
Normal file
25
hack/tools/golangci-lint/sortedfeatures/example.golangci.yml
Normal file
@@ -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
|
||||
27
hack/tools/golangci-lint/sortedfeatures/main.go
Normal file
27
hack/tools/golangci-lint/sortedfeatures/main.go
Normal file
@@ -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())
|
||||
}
|
||||
395
hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures.go
Normal file
395
hack/tools/golangci-lint/sortedfeatures/pkg/sortedfeatures.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
96
hack/tools/golangci-lint/sortedfeatures/plugin/plugin.go
Normal file
96
hack/tools/golangci-lint/sortedfeatures/plugin/plugin.go
Normal file
@@ -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
|
||||
}
|
||||
17
hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/sorted.go
vendored
Normal file
17
hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/sorted.go
vendored
Normal file
@@ -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"
|
||||
)
|
||||
17
hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/unsorted.go
vendored
Normal file
17
hack/tools/golangci-lint/sortedfeatures/testdata/src/testdata/unsorted.go
vendored
Normal file
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user