@@ -39,12 +39,8 @@ type analyzer struct {
3939 config * Config
4040}
4141
42- // newAnalyzer creates a new analysis.Analyzer for the enums linter based on the provided config.
4342func newAnalyzer (cfg * Config ) * analysis.Analyzer {
44- a := & analyzer {
45- config : cfg ,
46- }
47-
43+ a := & analyzer {config : cfg }
4844 return & analysis.Analyzer {
4945 Name : name ,
5046 Doc : "Enforces that enumerated fields use type aliases with +enum marker and have PascalCase values" ,
@@ -68,10 +64,7 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) {
6864 inspect .InspectTypeSpec (func (typeSpec * ast.TypeSpec , markersAccess markershelper.Markers ) {
6965 a .checkTypeSpec (pass , typeSpec , markersAccess )
7066 })
71-
72- // Check const values for PascalCase
7367 a .checkConstValues (pass , inspect )
74-
7568 return nil , nil //nolint:nilnil
7669}
7770
@@ -86,41 +79,41 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce
8679 if ! ok {
8780 return
8881 }
82+ prefix := buildFieldPrefix (fieldName , isArray )
83+ if ident .Name == "string" && utils .IsBasicType (pass , ident ) {
84+ a .checkPlainStringField (pass , field , markersAccess , prefix )
85+ return
86+ }
87+ a .checkTypeAliasField (pass , field , ident , markersAccess , prefix )
88+ }
8989
90- // Build appropriate prefix for error messages
91- prefix := fmt .Sprintf ("field %s" , fieldName )
90+ func buildFieldPrefix (fieldName string , isArray bool ) string {
9291 if isArray {
93- prefix = fmt .Sprintf ("field %s array element" , fieldName )
92+ return fmt .Sprintf ("field %s array element" , fieldName )
9493 }
94+ return fmt .Sprintf ("field %s" , fieldName )
95+ }
9596
96- // Check if it's a basic string type
97- if ident .Name == "string" && utils .IsBasicType (pass , ident ) {
98- // Check if the field has an enum marker directly
99- fieldMarkers := markersAccess .FieldMarkers (field )
100- if ! hasEnumMarker (fieldMarkers ) {
101- pass .Reportf (field .Pos (),
102- "%s uses plain string without +enum marker. Enumerated fields should use a type alias with +enum marker" ,
103- prefix )
104- }
97+ func (a * analyzer ) checkPlainStringField (pass * analysis.Pass , field * ast.Field , markersAccess markershelper.Markers , prefix string ) {
98+ if ! hasEnumMarker (markersAccess .FieldMarkers (field )) {
99+ pass .Reportf (field .Pos (),
100+ "%s uses plain string without +enum marker. Enumerated fields should use a type alias with +enum marker" ,
101+ prefix )
102+ }
103+ }
104+
105+ func (a * analyzer ) checkTypeAliasField (pass * analysis.Pass , field * ast.Field , ident * ast.Ident , markersAccess markershelper.Markers , prefix string ) {
106+ if utils .IsBasicType (pass , ident ) {
105107 return
106108 }
107- // If it's a type alias, check that the alias has an enum marker
108- if ! utils .IsBasicType (pass , ident ) {
109- typeSpec , ok := utils .LookupTypeSpec (pass , ident )
110- if ! ok {
111- return
112- }
113- // Check if the underlying type is string
114- if ! isStringTypeAlias (pass , typeSpec ) {
115- return
116- }
117- // Check if the type has an enum marker
118- typeMarkers := markersAccess .TypeMarkers (typeSpec )
119- if ! hasEnumMarker (typeMarkers ) {
120- pass .Reportf (field .Pos (),
121- "%s uses type %s which appears to be an enum but is missing +enum marker (kubebuilder:validation:Enum)" ,
122- prefix , typeSpec .Name .Name )
123- }
109+ typeSpec , ok := utils .LookupTypeSpec (pass , ident )
110+ if ! ok || ! isStringTypeAlias (pass , typeSpec ) {
111+ return
112+ }
113+ if ! hasEnumMarker (markersAccess .TypeMarkers (typeSpec )) {
114+ pass .Reportf (field .Pos (),
115+ "%s uses type %s which appears to be an enum but is missing +enum marker (kubebuilder:validation:Enum)" ,
116+ prefix , typeSpec .Name .Name )
124117 }
125118}
126119
@@ -132,8 +125,6 @@ func (a *analyzer) checkTypeSpec(pass *analysis.Pass, typeSpec *ast.TypeSpec, ma
132125 if ! hasEnumMarker (typeMarkers ) {
133126 return
134127 }
135-
136- // Has +enum marker, verify it's a string type
137128 if ! isStringTypeAlias (pass , typeSpec ) {
138129 pass .Reportf (typeSpec .Pos (),
139130 "type %s has +enum marker but underlying type is not string" ,
@@ -142,81 +133,64 @@ func (a *analyzer) checkTypeSpec(pass *analysis.Pass, typeSpec *ast.TypeSpec, ma
142133}
143134
144135func (a * analyzer ) checkConstValues (pass * analysis.Pass , inspect inspector.Inspector ) {
145- // We need to check const declarations, but the inspector helper doesn't
146- // have a method for this, so we'll iterate through files manually
147136 for _ , file := range pass .Files {
148137 for _ , decl := range file .Decls {
149138 genDecl , ok := decl .(* ast.GenDecl )
150139 if ! ok || genDecl .Tok .String () != "const" {
151140 continue
152141 }
153142 for _ , spec := range genDecl .Specs {
154- valueSpec , ok := spec .(* ast.ValueSpec )
155- if ! ok {
156- continue
143+ if valueSpec , ok := spec .(* ast.ValueSpec ); ok {
144+ a .checkConstSpec (pass , valueSpec )
157145 }
158- a .checkConstSpec (pass , valueSpec )
159146 }
160147 }
161148 }
162149}
163150
164151func (a * analyzer ) checkConstSpec (pass * analysis.Pass , valueSpec * ast.ValueSpec ) {
165152 for i , name := range valueSpec .Names {
166- if name == nil {
167- continue
168- }
169- // Get the type of the constant
170- obj := pass .TypesInfo .ObjectOf (name )
171- if obj == nil {
172- continue
173- }
174- constObj , ok := obj .(* types.Const )
175- if ! ok {
176- continue
177- }
178- // Check if the type is a named type (potential enum)
179- namedType , ok := constObj .Type ().(* types.Named )
180- if ! ok {
181- continue
182- }
183- // Check if the type is in the current package
184- if namedType .Obj ().Pkg () == nil || namedType .Obj ().Pkg () != pass .Pkg {
185- continue
186- }
187- // Find the type spec for this named type
188- typeSpec := findTypeSpecByName (pass , namedType .Obj ().Name ())
189- if typeSpec == nil {
190- continue
191- }
192- // Check if this type has an enum marker
193- if ! hasEnumMarkerOnTypeSpec (pass , typeSpec ) {
194- continue
195- }
196- // This is an enum constant, validate the value
197- if i >= len (valueSpec .Values ) {
198- continue
199- }
200- value := valueSpec .Values [i ]
201- basicLit , ok := value .(* ast.BasicLit )
202- if ! ok {
203- continue
204- }
205- // Extract the string value (remove quotes)
206- strValue := strings .Trim (basicLit .Value , `"` )
207- // Check if it's in the allowlist
208- if a .isInAllowlist (strValue ) {
209- continue
210- }
211- // Validate PascalCase
212- if ! isPascalCase (strValue ) {
213- pass .Reportf (basicLit .Pos (),
214- "enum value %q should be PascalCase (e.g., \" PhasePending\" , \" StateRunning\" )" ,
215- strValue )
216- }
153+ a .validateEnumConstant (pass , name , valueSpec , i )
154+ }
155+ }
156+
157+ func (a * analyzer ) validateEnumConstant (pass * analysis.Pass , name * ast.Ident , valueSpec * ast.ValueSpec , index int ) {
158+ if name == nil || index >= len (valueSpec .Values ) {
159+ return
160+ }
161+ typeSpec := a .getEnumTypeSpec (pass , name )
162+ if typeSpec == nil {
163+ return
164+ }
165+ // Extract and validate the enum value
166+ basicLit , ok := valueSpec .Values [index ].(* ast.BasicLit )
167+ if ! ok {
168+ return
169+ }
170+ strValue := strings .Trim (basicLit .Value , `"` )
171+ if ! a .isInAllowlist (strValue ) && ! isPascalCase (strValue ) {
172+ pass .Reportf (basicLit .Pos (),
173+ "enum value %q should be PascalCase (e.g., \" PhasePending\" , \" StateRunning\" )" ,
174+ strValue )
217175 }
218176}
219177
178+ func (a * analyzer ) getEnumTypeSpec (pass * analysis.Pass , name * ast.Ident ) * ast.TypeSpec {
179+ constObj , ok := pass .TypesInfo .ObjectOf (name ).(* types.Const )
180+ if ! ok {
181+ return nil
182+ }
183+ namedType , ok := constObj .Type ().(* types.Named )
184+ if ! ok || namedType .Obj ().Pkg () == nil || namedType .Obj ().Pkg () != pass .Pkg {
185+ return nil
186+ }
187+ typeSpec := findTypeSpecByName (pass , namedType .Obj ().Name ())
188+ if typeSpec == nil || ! hasEnumMarkerOnTypeSpec (pass , typeSpec ) {
189+ return nil
190+ }
191+ return typeSpec
192+ }
193+
220194// unwrapType removes pointer and array wrappers to get the underlying type
221195func unwrapType (expr ast.Expr ) ast.Expr {
222196 switch t := expr .(type ) {
@@ -246,7 +220,6 @@ func unwrapTypeWithArrayTracking(expr ast.Expr) (ast.Expr, bool) {
246220 }
247221}
248222
249- // isStringTypeAlias checks if a type spec is an alias for string
250223func isStringTypeAlias (pass * analysis.Pass , typeSpec * ast.TypeSpec ) bool {
251224 underlyingType := unwrapType (typeSpec .Type )
252225 ident , ok := underlyingType .(* ast.Ident )
@@ -256,34 +229,44 @@ func isStringTypeAlias(pass *analysis.Pass, typeSpec *ast.TypeSpec) bool {
256229 return ident .Name == "string" && utils .IsBasicType (pass , ident )
257230}
258231
259- // hasEnumMarker checks if a marker set contains an enum marker
260232func hasEnumMarker (markerSet markershelper.MarkerSet ) bool {
261233 return markerSet .Has (markers .KubebuilderEnumMarker ) || markerSet .Has (markers .K8sEnumMarker )
262234}
263235
264- // hasEnumMarkerOnTypeSpec checks if a type spec has an enum marker by checking its doc comments
265236func hasEnumMarkerOnTypeSpec (pass * analysis.Pass , typeSpec * ast.TypeSpec ) bool {
266237 for _ , file := range pass .Files {
267- for _ , decl := range file .Decls {
268- genDecl , ok := decl .(* ast.GenDecl )
269- if ! ok {
270- continue
271- }
272- for _ , spec := range genDecl .Specs {
273- if spec == typeSpec {
274- if genDecl .Doc != nil {
275- for _ , comment := range genDecl .Doc .List {
276- if strings .Contains (comment .Text , markers .KubebuilderEnumMarker ) ||
277- strings .Contains (comment .Text , markers .K8sEnumMarker ) {
278- return true
279- }
280- }
281- }
282- return false
283- }
238+ if genDecl := findGenDeclForSpec (file , typeSpec ); genDecl != nil {
239+ return hasEnumMarkerInDoc (genDecl .Doc )
240+ }
241+ }
242+ return false
243+ }
244+
245+ func findGenDeclForSpec (file * ast.File , typeSpec * ast.TypeSpec ) * ast.GenDecl {
246+ for _ , decl := range file .Decls {
247+ genDecl , ok := decl .(* ast.GenDecl )
248+ if ! ok {
249+ continue
250+ }
251+ for _ , spec := range genDecl .Specs {
252+ if spec == typeSpec {
253+ return genDecl
284254 }
285255 }
286256 }
257+ return nil
258+ }
259+
260+ func hasEnumMarkerInDoc (doc * ast.CommentGroup ) bool {
261+ if doc == nil {
262+ return false
263+ }
264+ for _ , comment := range doc .List {
265+ text := comment .Text
266+ if strings .Contains (text , markers .KubebuilderEnumMarker ) || strings .Contains (text , markers .K8sEnumMarker ) {
267+ return true
268+ }
269+ }
287270 return false
288271}
289272
@@ -300,7 +283,6 @@ func (a *analyzer) isInAllowlist(value string) bool {
300283 return false
301284}
302285
303- // findTypeSpecByName searches through the AST files to find a TypeSpec by name
304286func findTypeSpecByName (pass * analysis.Pass , typeName string ) * ast.TypeSpec {
305287 for _ , file := range pass .Files {
306288 for _ , decl := range file .Decls {
@@ -322,36 +304,24 @@ func findTypeSpecByName(pass *analysis.Pass, typeName string) *ast.TypeSpec {
322304 return nil
323305}
324306
325- // isPascalCase validates that a string follows PascalCase convention
326- // PascalCase: FirstLetterUpperCase, no underscores, no hyphens
327307func isPascalCase (s string ) bool {
328308 if len (s ) == 0 {
329309 return false
330310 }
331- // First character must be uppercase
332311 if ! unicode .IsUpper (rune (s [0 ])) {
333312 return false
334313 }
335- // Check for invalid characters (underscores, hyphens)
314+ hasLower := false
336315 for _ , r := range s {
337316 if r == '_' || r == '-' {
338317 return false
339318 }
340- // Only allow letters and digits
341319 if ! unicode .IsLetter (r ) && ! unicode .IsDigit (r ) {
342320 return false
343321 }
344- }
345- // Should not be all uppercase (that's SCREAMING_SNAKE_CASE or similar)
346- allUpper := true
347- for _ , r := range s [1 :] {
348322 if unicode .IsLower (r ) {
349- allUpper = false
350- break
323+ hasLower = true
351324 }
352325 }
353- if allUpper && len (s ) > 1 {
354- return false
355- }
356- return true
326+ return len (s ) == 1 || hasLower
357327}
0 commit comments