@@ -19,6 +19,7 @@ import (
1919 "fmt"
2020 "go/ast"
2121 "go/types"
22+ "slices"
2223 "strings"
2324 "unicode"
2425
@@ -41,6 +42,7 @@ type analyzer struct {
4142
4243func newAnalyzer (cfg * Config ) * analysis.Analyzer {
4344 a := & analyzer {config : cfg }
45+
4446 return & analysis.Analyzer {
4547 Name : name ,
4648 Doc : "Enforces that enumerated fields use type aliases with +enum marker and have PascalCase values" ,
@@ -56,15 +58,16 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) {
5658 }
5759
5860 // Check struct fields for proper enum usage
59- inspect .InspectFields (func (field * ast.Field , _ extractjsontags.FieldTagInfo , markersAccess markershelper.Markers ) {
61+ inspect .InspectFields (func (field * ast.Field , _ extractjsontags.FieldTagInfo , markersAccess markershelper.Markers , _ string ) {
6062 a .checkField (pass , field , markersAccess )
6163 })
6264
6365 // Check type declarations for +enum markers
6466 inspect .InspectTypeSpec (func (typeSpec * ast.TypeSpec , markersAccess markershelper.Markers ) {
6567 a .checkTypeSpec (pass , typeSpec , markersAccess )
6668 })
67- a .checkConstValues (pass , inspect )
69+ a .checkConstValues (pass )
70+
6871 return nil , nil //nolint:nilnil
6972}
7073
@@ -75,22 +78,29 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce
7578 }
7679 // Get the underlying type, unwrapping pointers and arrays
7780 fieldType , isArray := unwrapTypeWithArrayTracking (field .Type )
81+
7882 ident , ok := fieldType .(* ast.Ident )
83+
7984 if ! ok {
8085 return
8186 }
87+
8288 prefix := buildFieldPrefix (fieldName , isArray )
89+
8390 if ident .Name == "string" && utils .IsBasicType (pass , ident ) {
8491 a .checkPlainStringField (pass , field , markersAccess , prefix )
92+
8593 return
8694 }
95+
8796 a .checkTypeAliasField (pass , field , ident , markersAccess , prefix )
8897}
8998
9099func buildFieldPrefix (fieldName string , isArray bool ) string {
91100 if isArray {
92101 return fmt .Sprintf ("field %s array element" , fieldName )
93102 }
103+
94104 return fmt .Sprintf ("field %s" , fieldName )
95105}
96106
@@ -106,10 +116,13 @@ func (a *analyzer) checkTypeAliasField(pass *analysis.Pass, field *ast.Field, id
106116 if utils .IsBasicType (pass , ident ) {
107117 return
108118 }
119+
109120 typeSpec , ok := utils .LookupTypeSpec (pass , ident )
121+
110122 if ! ok || ! isStringTypeAlias (pass , typeSpec ) {
111123 return
112124 }
125+
113126 if ! hasEnumMarker (markersAccess .TypeMarkers (typeSpec )) {
114127 pass .Reportf (field .Pos (),
115128 "%s uses type %s which appears to be an enum but is missing +enum marker (kubebuilder:validation:Enum)" ,
@@ -121,24 +134,28 @@ func (a *analyzer) checkTypeSpec(pass *analysis.Pass, typeSpec *ast.TypeSpec, ma
121134 if typeSpec .Name == nil {
122135 return
123136 }
137+
124138 typeMarkers := markersAccess .TypeMarkers (typeSpec )
139+
125140 if ! hasEnumMarker (typeMarkers ) {
126141 return
127142 }
143+
128144 if ! isStringTypeAlias (pass , typeSpec ) {
129145 pass .Reportf (typeSpec .Pos (),
130146 "type %s has +enum marker but underlying type is not string" ,
131147 typeSpec .Name .Name )
132148 }
133149}
134150
135- func (a * analyzer ) checkConstValues (pass * analysis.Pass , inspect inspector. Inspector ) {
151+ func (a * analyzer ) checkConstValues (pass * analysis.Pass ) {
136152 for _ , file := range pass .Files {
137153 for _ , decl := range file .Decls {
138154 genDecl , ok := decl .(* ast.GenDecl )
139155 if ! ok || genDecl .Tok .String () != "const" {
140156 continue
141157 }
158+
142159 for _ , spec := range genDecl .Specs {
143160 if valueSpec , ok := spec .(* ast.ValueSpec ); ok {
144161 a .checkConstSpec (pass , valueSpec )
@@ -158,16 +175,20 @@ func (a *analyzer) validateEnumConstant(pass *analysis.Pass, name *ast.Ident, va
158175 if name == nil || index >= len (valueSpec .Values ) {
159176 return
160177 }
178+
161179 typeSpec := a .getEnumTypeSpec (pass , name )
162180 if typeSpec == nil {
163181 return
164182 }
183+
165184 // Extract and validate the enum value
166185 basicLit , ok := valueSpec .Values [index ].(* ast.BasicLit )
167186 if ! ok {
168187 return
169188 }
189+
170190 strValue := strings .Trim (basicLit .Value , `"` )
191+
171192 if ! a .isInAllowlist (strValue ) && ! isPascalCase (strValue ) {
172193 pass .Reportf (basicLit .Pos (),
173194 "enum value %q should be PascalCase (e.g., \" PhasePending\" , \" StateRunning\" )" ,
@@ -180,18 +201,22 @@ func (a *analyzer) getEnumTypeSpec(pass *analysis.Pass, name *ast.Ident) *ast.Ty
180201 if ! ok {
181202 return nil
182203 }
204+
183205 namedType , ok := constObj .Type ().(* types.Named )
184206 if ! ok || namedType .Obj ().Pkg () == nil || namedType .Obj ().Pkg () != pass .Pkg {
185207 return nil
186208 }
209+
187210 typeSpec := findTypeSpecByName (pass , namedType .Obj ().Name ())
211+
188212 if typeSpec == nil || ! hasEnumMarkerOnTypeSpec (pass , typeSpec ) {
189213 return nil
190214 }
215+
191216 return typeSpec
192217}
193218
194- // unwrapType removes pointer and array wrappers to get the underlying type
219+ // unwrapType removes pointer and array wrappers to get the underlying type.
195220func unwrapType (expr ast.Expr ) ast.Expr {
196221 switch t := expr .(type ) {
197222 case * ast.StarExpr :
@@ -207,6 +232,7 @@ func unwrapType(expr ast.Expr) ast.Expr {
207232// and tracks whether an array was encountered during unwrapping.
208233func unwrapTypeWithArrayTracking (expr ast.Expr ) (ast.Expr , bool ) {
209234 isArray := false
235+
210236 for {
211237 switch t := expr .(type ) {
212238 case * ast.StarExpr :
@@ -222,10 +248,13 @@ func unwrapTypeWithArrayTracking(expr ast.Expr) (ast.Expr, bool) {
222248
223249func isStringTypeAlias (pass * analysis.Pass , typeSpec * ast.TypeSpec ) bool {
224250 underlyingType := unwrapType (typeSpec .Type )
251+
225252 ident , ok := underlyingType .(* ast.Ident )
253+
226254 if ! ok {
227255 return false
228256 }
257+
229258 return ident .Name == "string" && utils .IsBasicType (pass , ident )
230259}
231260
@@ -239,6 +268,7 @@ func hasEnumMarkerOnTypeSpec(pass *analysis.Pass, typeSpec *ast.TypeSpec) bool {
239268 return hasEnumMarkerInDoc (genDecl .Doc )
240269 }
241270 }
271+
242272 return false
243273}
244274
@@ -248,39 +278,39 @@ func findGenDeclForSpec(file *ast.File, typeSpec *ast.TypeSpec) *ast.GenDecl {
248278 if ! ok {
249279 continue
250280 }
281+
251282 for _ , spec := range genDecl .Specs {
252283 if spec == typeSpec {
253284 return genDecl
254285 }
255286 }
256287 }
288+
257289 return nil
258290}
259291
260292func hasEnumMarkerInDoc (doc * ast.CommentGroup ) bool {
261293 if doc == nil {
262294 return false
263295 }
296+
264297 for _ , comment := range doc .List {
265298 text := comment .Text
266299 if strings .Contains (text , markers .KubebuilderEnumMarker ) || strings .Contains (text , markers .K8sEnumMarker ) {
267300 return true
268301 }
269302 }
303+
270304 return false
271305}
272306
273- // isInAllowlist checks if a value is in the configured allowlist
307+ // isInAllowlist checks if a value is in the configured allowlist.
274308func (a * analyzer ) isInAllowlist (value string ) bool {
275309 if a .config == nil {
276310 return false
277311 }
278- for _ , allowed := range a .config .Allowlist {
279- if value == allowed {
280- return true
281- }
282- }
283- return false
312+
313+ return slices .Contains (a .config .Allowlist , value )
284314}
285315
286316func findTypeSpecByName (pass * analysis.Pass , typeName string ) * ast.TypeSpec {
@@ -290,38 +320,47 @@ func findTypeSpecByName(pass *analysis.Pass, typeName string) *ast.TypeSpec {
290320 if ! ok {
291321 continue
292322 }
323+
293324 for _ , spec := range genDecl .Specs {
294325 typeSpec , ok := spec .(* ast.TypeSpec )
295326 if ! ok {
296327 continue
297328 }
329+
298330 if typeSpec .Name != nil && typeSpec .Name .Name == typeName {
299331 return typeSpec
300332 }
301333 }
302334 }
303335 }
336+
304337 return nil
305338}
306339
307340func isPascalCase (s string ) bool {
308341 if len (s ) == 0 {
309342 return false
310343 }
344+
311345 if ! unicode .IsUpper (rune (s [0 ])) {
312346 return false
313347 }
348+
314349 hasLower := false
350+
315351 for _ , r := range s {
316352 if r == '_' || r == '-' {
317353 return false
318354 }
355+
319356 if ! unicode .IsLetter (r ) && ! unicode .IsDigit (r ) {
320357 return false
321358 }
359+
322360 if unicode .IsLower (r ) {
323361 hasLower = true
324362 }
325363 }
364+
326365 return len (s ) == 1 || hasLower
327366}
0 commit comments