package analyzers import ( "context" "fmt" "go/ast" "go/parser" "go/token" "os" "path/filepath" "strings" "github.com/yourorg/devour/internal/quality" ) type LargeFileDetector struct { *quality.BaseDetector maxLOC int } func NewLargeFileDetector(finder quality.FileFinder) *LargeFileDetector { return &LargeFileDetector{ BaseDetector: quality.NewBaseDetector("large_file", quality.SeverityT3, finder), maxLOC: 500, } } func (d *LargeFileDetector) Name() string { return "large_file" } func (d *LargeFileDetector) Severity() quality.Severity { return quality.SeverityT3 } func (d *LargeFileDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { files, err := d.FindFiles(path, "go") if err != nil { return nil, err } var findings []quality.Finding for _, file := range files { loc, err := countLines(file) if err != nil { findings = append(findings, quality.Finding{ ID: fmt.Sprintf("detector_read_error::large_file::%s", file), Type: "detector_error", Title: "Large file detector could not read file", Description: fmt.Sprintf("Failed to count lines in %s: %v", filepath.Base(file), err), File: file, Line: 1, Severity: quality.SeverityT2, Score: 0, Status: quality.StatusOpen, Metadata: map[string]string{ "detector": "large_file", "error": err.Error(), }, }) continue } if loc > d.maxLOC { finding := quality.Finding{ ID: fmt.Sprintf("large_file::%s", file), Type: "large_file", Title: fmt.Sprintf("Large file detected: %d lines", loc), Description: fmt.Sprintf("File '%s' has %d lines (max: %d). Consider splitting into smaller, focused files.", filepath.Base(file), loc, d.maxLOC), File: file, Line: 1, Severity: quality.SeverityT3, Score: (loc - d.maxLOC) / 50, Status: quality.StatusOpen, Metadata: map[string]string{ "loc": fmt.Sprintf("%d", loc), "max_loc": fmt.Sprintf("%d", d.maxLOC), }, } findings = append(findings, finding) } } return findings, nil } type GodStructDetector struct { *quality.BaseDetector maxFields int maxMethods int } func NewGodStructDetector(finder quality.FileFinder) *GodStructDetector { return &GodStructDetector{ BaseDetector: quality.NewBaseDetector("god_struct", quality.SeverityT3, finder), maxFields: 15, maxMethods: 20, } } func (d *GodStructDetector) Name() string { return "god_struct" } func (d *GodStructDetector) Severity() quality.Severity { return quality.SeverityT3 } func (d *GodStructDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { files, err := d.FindFiles(path, "go") if err != nil { return nil, err } var findings []quality.Finding for _, file := range files { fileFindings, err := d.analyzeFile(file) if err != nil { return nil, fmt.Errorf("analyze god struct in %q: %w", file, err) } findings = append(findings, fileFindings...) } return findings, nil } func (d *GodStructDetector) analyzeFile(path string) ([]quality.Finding, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, path, nil, 0) if err != nil { return nil, fmt.Errorf("parse %s: %w", path, err) } methodCounts := make(map[string]int) for _, decl := range node.Decls { if fn, ok := decl.(*ast.FuncDecl); ok && fn.Recv != nil { for _, field := range fn.Recv.List { for _, name := range field.Names { methodCounts[name.Name]++ } if len(field.Names) == 0 { if star, ok := field.Type.(*ast.StarExpr); ok { if ident, ok := star.X.(*ast.Ident); ok { methodCounts[ident.Name]++ } } else if ident, ok := field.Type.(*ast.Ident); ok { methodCounts[ident.Name]++ } } } } } var findings []quality.Finding for _, decl := range node.Decls { gen, ok := decl.(*ast.GenDecl) if !ok || gen.Tok != token.TYPE { continue } for _, spec := range gen.Specs { typeSpec, ok := spec.(*ast.TypeSpec) if !ok { continue } structType, ok := typeSpec.Type.(*ast.StructType) if !ok { continue } fieldCount := len(structType.Fields.List) methodCount := methodCounts[typeSpec.Name.Name] if fieldCount > d.maxFields { pos := fset.Position(typeSpec.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("god_struct::%s::%s", path, typeSpec.Name.Name), Type: "god_struct", Title: fmt.Sprintf("God struct detected: %s", typeSpec.Name.Name), Description: fmt.Sprintf("Struct '%s' has %d fields (max: %d). Consider breaking it into smaller, focused structs.", typeSpec.Name.Name, fieldCount, d.maxFields), File: path, Line: pos.Line, Severity: quality.SeverityT3, Score: (fieldCount - d.maxFields) * 2, Status: quality.StatusOpen, Metadata: map[string]string{ "struct_name": typeSpec.Name.Name, "field_count": fmt.Sprintf("%d", fieldCount), "max_fields": fmt.Sprintf("%d", d.maxFields), }, } findings = append(findings, finding) } if methodCount > d.maxMethods { pos := fset.Position(typeSpec.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("god_struct_methods::%s::%s", path, typeSpec.Name.Name), Type: "god_struct", Title: fmt.Sprintf("God struct (methods): %s", typeSpec.Name.Name), Description: fmt.Sprintf("Struct '%s' has %d methods (max: %d). Consider splitting responsibilities.", typeSpec.Name.Name, methodCount, d.maxMethods), File: path, Line: pos.Line, Severity: quality.SeverityT3, Score: (methodCount - d.maxMethods) * 2, Status: quality.StatusOpen, Metadata: map[string]string{ "struct_name": typeSpec.Name.Name, "method_count": fmt.Sprintf("%d", methodCount), "max_methods": fmt.Sprintf("%d", d.maxMethods), }, } findings = append(findings, finding) } } } return findings, nil } type DebugLogDetector struct { *quality.BaseDetector } func NewDebugLogDetector(finder quality.FileFinder) *DebugLogDetector { return &DebugLogDetector{ BaseDetector: quality.NewBaseDetector("debug_log", quality.SeverityT1, finder), } } func (d *DebugLogDetector) Name() string { return "debug_log" } func (d *DebugLogDetector) Severity() quality.Severity { return quality.SeverityT1 } func (d *DebugLogDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { files, err := d.FindFiles(path, "go") if err != nil { return nil, err } var findings []quality.Finding for _, file := range files { fileFindings, err := d.analyzeFile(file) if err != nil { return nil, fmt.Errorf("analyze debug logs in %q: %w", file, err) } findings = append(findings, fileFindings...) } return findings, nil } func (d *DebugLogDetector) analyzeFile(path string) ([]quality.Finding, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, path, nil, 0) if err != nil { return nil, fmt.Errorf("parse %s: %w", path, err) } normPath := filepath.ToSlash(path) if strings.Contains(normPath, "internal/ui/") || strings.Contains(normPath, "examples/") { return nil, nil } debugPatterns := []string{ "log.Print", "log.Println", "log.Printf", "log.Fatal", "log.Fatalf", "log.Fatalln", } cliPatterns := []string{ "fmt.Print", "fmt.Println", "fmt.Printf", } var findings []quality.Finding ast.Inspect(node, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } callStr := exprToString(call.Fun) for _, pattern := range debugPatterns { if callStr == pattern || strings.HasPrefix(callStr, pattern) { if strings.HasSuffix(normPath, "_test.go") || strings.HasPrefix(normPath, "cmd/") || strings.Contains(normPath, "/cmd/") { return true } pos := fset.Position(call.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("debug_log::%s::%d", path, pos.Line), Type: "debug_log", Title: "Debug log statement detected", Description: fmt.Sprintf("Found '%s' statement. Consider using structured logging instead.", callStr), File: path, Line: pos.Line, Severity: quality.SeverityT1, Score: 2, Status: quality.StatusOpen, Metadata: map[string]string{ "call": callStr, }, } findings = append(findings, finding) break } } if strings.HasPrefix(normPath, "cmd/") || strings.Contains(normPath, "/cmd/") { return true } for _, pattern := range cliPatterns { if callStr == pattern || strings.HasPrefix(callStr, pattern) { pos := fset.Position(call.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("debug_log::%s::%d", path, pos.Line), Type: "debug_log", Title: "Potential debug print in non-CLI code", Description: fmt.Sprintf("Found '%s' in library code. Consider using structured logging or returning errors.", callStr), File: path, Line: pos.Line, Severity: quality.SeverityT1, Score: 2, Status: quality.StatusOpen, Metadata: map[string]string{ "call": callStr, }, } findings = append(findings, finding) break } } return true }) return findings, nil } type GodFunctionDetector struct { *quality.BaseDetector maxLOC int maxParams int maxReturns int maxNesting int } func NewGodFunctionDetector(finder quality.FileFinder) *GodFunctionDetector { return &GodFunctionDetector{ BaseDetector: quality.NewBaseDetector("god_function", quality.SeverityT3, finder), maxLOC: 50, maxParams: 5, maxReturns: 3, maxNesting: 4, } } func (d *GodFunctionDetector) Name() string { return "god_function" } func (d *GodFunctionDetector) Severity() quality.Severity { return quality.SeverityT3 } func (d *GodFunctionDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { files, err := d.FindFiles(path, "go") if err != nil { return nil, err } var findings []quality.Finding for _, file := range files { fileFindings := d.analyzeFile(file) findings = append(findings, fileFindings...) } return findings, nil } func (d *GodFunctionDetector) analyzeFile(path string) []quality.Finding { fset := token.NewFileSet() node, err := parser.ParseFile(fset, path, nil, 0) if err != nil { return nil } var findings []quality.Finding for _, decl := range node.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok { continue } startLine := fset.Position(fn.Pos()).Line endLine := fset.Position(fn.End()).Line loc := endLine - startLine + 1 paramCount := 0 if fn.Type.Params != nil { for _, field := range fn.Type.Params.List { paramCount += len(field.Names) if len(field.Names) == 0 { paramCount++ } } } returnCount := 0 if fn.Type.Results != nil { returnCount = len(fn.Type.Results.List) } nestingDepth := d.calculateNesting(fn) var issues []string if loc > d.maxLOC { issues = append(issues, fmt.Sprintf("%d lines (max %d)", loc, d.maxLOC)) } if paramCount > d.maxParams { issues = append(issues, fmt.Sprintf("%d params (max %d)", paramCount, d.maxParams)) } if returnCount > d.maxReturns { issues = append(issues, fmt.Sprintf("%d returns (max %d)", returnCount, d.maxReturns)) } if nestingDepth > d.maxNesting { issues = append(issues, fmt.Sprintf("nesting depth %d (max %d)", nestingDepth, d.maxNesting)) } if len(issues) > 0 { finding := quality.Finding{ ID: fmt.Sprintf("god_function::%s::%s", path, fn.Name.Name), Type: "god_function", Title: fmt.Sprintf("God function: %s", fn.Name.Name), Description: fmt.Sprintf("Function '%s' has issues: %s", fn.Name.Name, strings.Join(issues, ", ")), File: path, Line: startLine, Severity: quality.SeverityT3, Score: len(issues) * 3, Status: quality.StatusOpen, Metadata: map[string]string{ "function": fn.Name.Name, "loc": fmt.Sprintf("%d", loc), "params": fmt.Sprintf("%d", paramCount), "returns": fmt.Sprintf("%d", returnCount), "nesting_depth": fmt.Sprintf("%d", nestingDepth), }, } findings = append(findings, finding) } } return findings } func (d *GodFunctionDetector) calculateNesting(fn *ast.FuncDecl) int { maxDepth := 0 var visit func(n ast.Node, depth int) visit = func(n ast.Node, depth int) { if depth > maxDepth { maxDepth = depth } switch stmt := n.(type) { case *ast.IfStmt: visit(stmt.Body, depth+1) if stmt.Else != nil { visit(stmt.Else, depth+1) } case *ast.ForStmt: visit(stmt.Body, depth+1) case *ast.RangeStmt: visit(stmt.Body, depth+1) case *ast.SwitchStmt: visit(stmt.Body, depth+1) case *ast.SelectStmt: visit(stmt.Body, depth+1) case *ast.BlockStmt: for _, s := range stmt.List { visit(s, depth) } case *ast.CaseClause: for _, s := range stmt.Body { visit(s, depth) } } } if fn.Body != nil { visit(fn.Body, 0) } return maxDepth } func exprToString(expr ast.Expr) string { switch e := expr.(type) { case *ast.Ident: return e.Name case *ast.SelectorExpr: return exprToString(e.X) + "." + e.Sel.Name default: return "" } } func countLines(path string) (int, error) { data, err := os.ReadFile(path) if err != nil { return 0, err } return strings.Count(string(data), "\n") + 1, nil }