Files
gocp/tool_analyze_naming_conventions.go
2025-06-27 22:54:45 -07:00

418 lines
11 KiB
Go

package main
import (
"go/ast"
"go/token"
"strings"
"unicode"
)
type NamingViolation struct {
Name string `json:"name"`
Type string `json:"type"` // "function", "variable", "constant", "type", "package"
Issue string `json:"issue"`
Suggestion string `json:"suggestion,omitempty"`
Position Position `json:"position"`
}
type NamingAnalysis struct {
Violations []NamingViolation `json:"violations"`
Statistics NamingStats `json:"statistics"`
}
type NamingStats struct {
TotalSymbols int `json:"total_symbols"`
ExportedSymbols int `json:"exported_symbols"`
UnexportedSymbols int `json:"unexported_symbols"`
ViolationCount int `json:"violation_count"`
}
func analyzeNamingConventions(dir string) (*NamingAnalysis, error) {
analysis := &NamingAnalysis{
Violations: []NamingViolation{},
Statistics: NamingStats{},
}
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
// Check package name
checkPackageName(file, fset, analysis)
ast.Inspect(file, func(n ast.Node) bool {
switch node := n.(type) {
case *ast.FuncDecl:
analysis.Statistics.TotalSymbols++
checkFunctionName(node, fset, analysis)
case *ast.GenDecl:
for _, spec := range node.Specs {
switch s := spec.(type) {
case *ast.TypeSpec:
analysis.Statistics.TotalSymbols++
checkTypeName(s, node, fset, analysis)
case *ast.ValueSpec:
for _, name := range s.Names {
analysis.Statistics.TotalSymbols++
if node.Tok == token.CONST {
checkConstantName(name, fset, analysis)
} else {
checkVariableName(name, fset, analysis)
}
}
}
}
}
return true
})
return nil
})
analysis.Statistics.ViolationCount = len(analysis.Violations)
return analysis, err
}
func checkPackageName(file *ast.File, fset *token.FileSet, analysis *NamingAnalysis) {
name := file.Name.Name
pos := fset.Position(file.Name.Pos())
// Package names should be lowercase
if !isAllLowercase(name) {
violation := NamingViolation{
Name: name,
Type: "package",
Issue: "Package name should be lowercase",
Suggestion: strings.ToLower(name),
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
// Check for underscores
if strings.Contains(name, "_") && name != "main" {
violation := NamingViolation{
Name: name,
Type: "package",
Issue: "Package name should not contain underscores",
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
}
func checkFunctionName(fn *ast.FuncDecl, fset *token.FileSet, analysis *NamingAnalysis) {
name := fn.Name.Name
pos := fset.Position(fn.Name.Pos())
isExported := ast.IsExported(name)
if isExported {
analysis.Statistics.ExportedSymbols++
} else {
analysis.Statistics.UnexportedSymbols++
}
// Check receiver naming
if fn.Recv != nil && len(fn.Recv.List) > 0 {
for _, recv := range fn.Recv.List {
for _, recvName := range recv.Names {
checkReceiverName(recvName, recv.Type, fset, analysis)
}
}
}
// Check CamelCase
if !isCamelCase(name) && !isSpecialFunction(name) {
violation := NamingViolation{
Name: name,
Type: "function",
Issue: "Function name should be in CamelCase",
Suggestion: toCamelCase(name),
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
// Check exported function starts with capital
if isExported && !unicode.IsUpper(rune(name[0])) {
violation := NamingViolation{
Name: name,
Type: "function",
Issue: "Exported function should start with capital letter",
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
// Check for Get prefix on getters
if strings.HasPrefix(name, "Get") && fn.Recv != nil && !returnsError(fn) {
violation := NamingViolation{
Name: name,
Type: "function",
Issue: "Getter methods should not use Get prefix",
Suggestion: name[3:], // Remove "Get" prefix
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
}
func checkTypeName(typeSpec *ast.TypeSpec, genDecl *ast.GenDecl, fset *token.FileSet, analysis *NamingAnalysis) {
name := typeSpec.Name.Name
pos := fset.Position(typeSpec.Name.Pos())
isExported := ast.IsExported(name)
if isExported {
analysis.Statistics.ExportedSymbols++
} else {
analysis.Statistics.UnexportedSymbols++
}
// Check CamelCase
if !isCamelCase(name) {
violation := NamingViolation{
Name: name,
Type: "type",
Issue: "Type name should be in CamelCase",
Suggestion: toCamelCase(name),
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
// Check interface naming
if _, ok := typeSpec.Type.(*ast.InterfaceType); ok {
if isExported && !strings.HasSuffix(name, "er") && !isWellKnownInterface(name) {
// Only suggest for single-method interfaces
if iface, ok := typeSpec.Type.(*ast.InterfaceType); ok && len(iface.Methods.List) == 1 {
violation := NamingViolation{
Name: name,
Type: "type",
Issue: "Single-method interface should end with 'er'",
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
}
}
}
func checkConstantName(name *ast.Ident, fset *token.FileSet, analysis *NamingAnalysis) {
pos := fset.Position(name.Pos())
isExported := ast.IsExported(name.Name)
if isExported {
analysis.Statistics.ExportedSymbols++
} else {
analysis.Statistics.UnexportedSymbols++
}
// Constants can be CamelCase or ALL_CAPS
if !isCamelCase(name.Name) && !isAllCaps(name.Name) {
violation := NamingViolation{
Name: name.Name,
Type: "constant",
Issue: "Constant should be in CamelCase or ALL_CAPS",
Suggestion: toCamelCase(name.Name),
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
}
func checkVariableName(name *ast.Ident, fset *token.FileSet, analysis *NamingAnalysis) {
pos := fset.Position(name.Pos())
isExported := ast.IsExported(name.Name)
if isExported {
analysis.Statistics.ExportedSymbols++
} else {
analysis.Statistics.UnexportedSymbols++
}
// Skip blank identifier
if name.Name == "_" {
return
}
// Check for single letter names (except common ones)
if len(name.Name) == 1 && !isCommonSingleLetter(name.Name) {
violation := NamingViolation{
Name: name.Name,
Type: "variable",
Issue: "Single letter variable names should be avoided except for common cases (i, j, k for loops)",
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
// Check CamelCase
if !isCamelCase(name.Name) && len(name.Name) > 1 {
violation := NamingViolation{
Name: name.Name,
Type: "variable",
Issue: "Variable name should be in camelCase",
Suggestion: toCamelCase(name.Name),
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
}
func checkReceiverName(name *ast.Ident, recvType ast.Expr, fset *token.FileSet, analysis *NamingAnalysis) {
pos := fset.Position(name.Pos())
// Receiver names should be short
if len(name.Name) > 3 {
typeName := extractReceiverTypeName(recvType)
suggestion := ""
if typeName != "" && len(typeName) > 0 {
suggestion = strings.ToLower(string(typeName[0]))
}
violation := NamingViolation{
Name: name.Name,
Type: "receiver",
Issue: "Receiver name should be a short, typically one-letter abbreviation",
Suggestion: suggestion,
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
// Check for "self" or "this"
if name.Name == "self" || name.Name == "this" {
violation := NamingViolation{
Name: name.Name,
Type: "receiver",
Issue: "Avoid 'self' or 'this' for receiver names",
Position: newPosition(pos),
}
analysis.Violations = append(analysis.Violations, violation)
}
}
func extractReceiverTypeName(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return extractReceiverTypeName(t.X)
}
return ""
}
func isCamelCase(name string) bool {
if len(name) == 0 {
return false
}
// Check for underscores
if strings.Contains(name, "_") {
return false
}
// Allow all lowercase for short names (like "i", "ok", "err")
if len(name) <= 3 && isAllLowercase(name) {
return true
}
// Check for proper camelCase/PascalCase
hasUpper := false
hasLower := false
for _, r := range name {
if unicode.IsUpper(r) {
hasUpper = true
} else if unicode.IsLower(r) {
hasLower = true
}
}
// Single case is ok for short names
return len(name) <= 3 || (hasUpper && hasLower) || isAllLowercase(name)
}
func isAllLowercase(s string) bool {
for _, r := range s {
if unicode.IsLetter(r) && !unicode.IsLower(r) {
return false
}
}
return true
}
func isAllCaps(s string) bool {
for _, r := range s {
if unicode.IsLetter(r) && !unicode.IsUpper(r) {
return false
}
}
return true
}
func toCamelCase(s string) string {
words := strings.FieldsFunc(s, func(r rune) bool {
return r == '_' || r == '-'
})
if len(words) == 0 {
return s
}
// First word stays lowercase for camelCase
result := strings.ToLower(words[0])
// Capitalize first letter of subsequent words
for i := 1; i < len(words); i++ {
if len(words[i]) > 0 {
result += strings.ToUpper(words[i][:1]) + strings.ToLower(words[i][1:])
}
}
return result
}
func isSpecialFunction(name string) bool {
// Special functions that don't follow normal naming
special := []string{"init", "main", "String", "Error", "MarshalJSON", "UnmarshalJSON"}
for _, s := range special {
if name == s {
return true
}
}
return false
}
func isWellKnownInterface(name string) bool {
// Well-known interfaces that don't end in 'er'
known := []string{"Interface", "Handler", "ResponseWriter", "Context", "Value"}
for _, k := range known {
if name == k {
return true
}
}
return false
}
func isCommonSingleLetter(name string) bool {
// Common single letter variables that are acceptable
common := []string{"i", "j", "k", "n", "m", "x", "y", "z", "s", "b", "r", "w", "t"}
for _, c := range common {
if name == c {
return true
}
}
return false
}
func returnsError(fn *ast.FuncDecl) bool {
if fn.Type.Results == nil {
return false
}
for _, result := range fn.Type.Results.List {
if ident, ok := result.Type.(*ast.Ident); ok && ident.Name == "error" {
return true
}
}
return false
}