Skip to content

Commit a76ac80

Browse files
Simplified loops, switches, and conditionals. Cleaned comments
1 parent 9ebc8bf commit a76ac80

File tree

1 file changed

+102
-132
lines changed

1 file changed

+102
-132
lines changed

pkg/analysis/enums/analyzer.go

Lines changed: 102 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
4342
func 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

144135
func (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

164151
func (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
221195
func 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
250223
func 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
260232
func 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
265236
func 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
304286
func 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
327307
func 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

Comments
 (0)