package analyzers import ( "context" "fmt" "go/ast" "go/parser" "go/token" "go/types" "path/filepath" "strings" "github.com/yourorg/devour/internal/quality" "golang.org/x/tools/go/packages" ) type SingleUseDetector struct { *quality.BaseDetector minLOC int } func NewSingleUseDetector(finder quality.FileFinder) *SingleUseDetector { return &SingleUseDetector{ BaseDetector: quality.NewBaseDetector("single_use", quality.SeverityT3, finder), minLOC: 10, } } func (d *SingleUseDetector) Name() string { return "single_use" } func (d *SingleUseDetector) Severity() quality.Severity { return quality.SeverityT3 } func (d *SingleUseDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedSyntax, Dir: path, } pkgs, err := packages.Load(cfg, "./...") if err != nil { return nil, fmt.Errorf("failed to load packages: %w", err) } callCounts := make(map[string]int) funcDefs := make(map[string]FuncDef) typeUsages := make(map[string]int) typeDefs := make(map[string]TypeDef) for _, pkg := range pkgs { for _, obj := range pkg.TypesInfo.Uses { if obj == nil { continue } switch obj := obj.(type) { case *types.Func: key := obj.Pkg().Path() + "." + obj.Name() callCounts[key]++ case *types.TypeName: if obj.Pkg() != nil { key := obj.Pkg().Path() + "." + obj.Name() typeUsages[key]++ } } } for _, obj := range pkg.TypesInfo.Defs { if obj == nil { continue } switch obj := obj.(type) { case *types.Func: if obj.Pkg() != nil { key := obj.Pkg().Path() + "." + obj.Name() pos := pkg.Fset.Position(obj.Pos()) funcDefs[key] = FuncDef{ Name: obj.Name(), File: pos.Filename, Line: pos.Line, Package: obj.Pkg().Path(), Exported: obj.Exported(), Signature: obj.Type().String(), } } case *types.TypeName: if obj.Pkg() != nil { key := obj.Pkg().Path() + "." + obj.Name() pos := pkg.Fset.Position(obj.Pos()) typeDefs[key] = TypeDef{ Name: obj.Name(), File: pos.Filename, Line: pos.Line, Package: obj.Pkg().Path(), Exported: obj.Exported(), Underlying: obj.Type().Underlying().String(), } } } } } entryPoints := d.findEntryPoints(pkgs) var findings []quality.Finding for key, def := range funcDefs { if strings.HasSuffix(def.Name, "Test") || strings.HasPrefix(def.Name, "Test") { continue } if strings.HasSuffix(def.Name, "Handler") || strings.HasSuffix(def.Name, "Middleware") { continue } count := callCounts[key] if count == 1 && !d.isEntryPoint(def.Name, entryPoints) { loc, _ := d.getFuncLOC(def.File, def.Line) if loc >= d.minLOC { finding := quality.Finding{ ID: fmt.Sprintf("single_use_func::%s::%s", def.File, def.Name), Type: "single_use", Title: fmt.Sprintf("Single-use function: %s", def.Name), Description: fmt.Sprintf("Function '%s' is only used once. Consider inlining it or documenting its purpose.", def.Name), File: def.File, Line: def.Line, Severity: quality.SeverityT3, Score: 3, Status: quality.StatusOpen, Metadata: map[string]string{ "name": def.Name, "usage_count": fmt.Sprintf("%d", count), "loc": fmt.Sprintf("%d", loc), "exported": fmt.Sprintf("%v", def.Exported), }, } findings = append(findings, finding) } } } for key, def := range typeDefs { if strings.HasSuffix(def.Name, "Error") || strings.HasSuffix(def.Name, "Options") { continue } count := typeUsages[key] if count == 1 { finding := quality.Finding{ ID: fmt.Sprintf("single_use_type::%s::%s", def.File, def.Name), Type: "single_use", Title: fmt.Sprintf("Single-use type: %s", def.Name), Description: fmt.Sprintf("Type '%s' is only used once. Consider if this abstraction is necessary.", def.Name), File: def.File, Line: def.Line, Severity: quality.SeverityT3, Score: 4, Status: quality.StatusOpen, Metadata: map[string]string{ "name": def.Name, "usage_count": fmt.Sprintf("%d", count), "exported": fmt.Sprintf("%v", def.Exported), "underlying": def.Underlying, }, } findings = append(findings, finding) } } return findings, nil } func (d *SingleUseDetector) findEntryPoints(pkgs []*packages.Package) map[string]bool { entryPoints := make(map[string]bool) for _, pkg := range pkgs { for _, file := range pkg.Syntax { ast.Inspect(file, func(n ast.Node) bool { switch node := n.(type) { case *ast.FuncDecl: if node.Name.Name == "main" { entryPoints[pkg.PkgPath+".main"] = true } if node.Name.Name == "init" { entryPoints[pkg.PkgPath+".init"] = true } if node.Recv == nil { for _, decl := range node.Type.Params.List { if d.isHTTPHandlerType(decl.Type) { entryPoints[pkg.PkgPath+"."+node.Name.Name] = true } } } } return true }) } } return entryPoints } func (d *SingleUseDetector) isHTTPHandlerType(expr ast.Expr) bool { if sel, ok := expr.(*ast.SelectorExpr); ok { if ident, ok := sel.X.(*ast.Ident); ok { return (ident.Name == "http" && (sel.Sel.Name == "Handler" || sel.Sel.Name == "HandlerFunc" || sel.Sel.Name == "ResponseWriter")) } } if star, ok := expr.(*ast.StarExpr); ok { return d.isHTTPHandlerType(star.X) } return false } func (d *SingleUseDetector) isEntryPoint(name string, entryPoints map[string]bool) bool { return entryPoints[name] || name == "main" || name == "init" } func (d *SingleUseDetector) getFuncLOC(file string, startLine int) (int, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, file, nil, 0) if err != nil { return 0, err } loc := 0 ast.Inspect(node, func(n ast.Node) bool { if fn, ok := n.(*ast.FuncDecl); ok { pos := fset.Position(fn.Pos()) if pos.Line == startLine { end := fset.Position(fn.End()) loc = end.Line - pos.Line + 1 return false } } return true }) return loc, nil } type FuncDef struct { Name string File string Line int Package string Exported bool Signature string } type TypeDef struct { Name string File string Line int Package string Exported bool Underlying string } type CouplingDetector struct { *quality.BaseDetector maxFanOut int } func NewCouplingDetector(finder quality.FileFinder) *CouplingDetector { return &CouplingDetector{ BaseDetector: quality.NewBaseDetector("coupling", quality.SeverityT3, finder), maxFanOut: 10, } } func (d *CouplingDetector) Name() string { return "coupling" } func (d *CouplingDetector) Severity() quality.Severity { return quality.SeverityT3 } func (d *CouplingDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedImports | packages.NeedFiles, Dir: path, } pkgs, err := packages.Load(cfg, "./...") if err != nil { return nil, fmt.Errorf("failed to load packages: %w", err) } pkgImports := make(map[string][]string) pkgImportedBy := make(map[string][]string) pkgFiles := make(map[string]int) for _, pkg := range pkgs { pkgFiles[pkg.PkgPath] = len(pkg.GoFiles) for _, imp := range pkg.Imports { pkgImports[pkg.PkgPath] = append(pkgImports[pkg.PkgPath], imp.PkgPath) pkgImportedBy[imp.PkgPath] = append(pkgImportedBy[imp.PkgPath], pkg.PkgPath) } } var findings []quality.Finding for pkg, imports := range pkgImports { fanOut := len(imports) if fanOut > d.maxFanOut { finding := quality.Finding{ ID: fmt.Sprintf("coupling_fanout::%s", pkg), Type: "coupling", Title: fmt.Sprintf("High fan-out coupling: %s", filepath.Base(pkg)), Description: fmt.Sprintf("Package '%s' imports %d packages (max: %d). Consider reducing dependencies.", pkg, fanOut, d.maxFanOut), File: pkg, Line: 1, Severity: quality.SeverityT3, Score: fanOut - d.maxFanOut, Status: quality.StatusOpen, Metadata: map[string]string{ "package": pkg, "fan_out": fmt.Sprintf("%d", fanOut), "imports": strings.Join(imports, ","), }, } findings = append(findings, finding) } } for pkg, importedBy := range pkgImportedBy { fanIn := len(importedBy) if fanIn > d.maxFanOut*2 { finding := quality.Finding{ ID: fmt.Sprintf("coupling_fanin::%s", pkg), Type: "coupling", Title: fmt.Sprintf("High fan-in coupling: %s", filepath.Base(pkg)), Description: fmt.Sprintf("Package '%s' is imported by %d packages. Ensure it's stable and well-documented.", pkg, fanIn), File: pkg, Line: 1, Severity: quality.SeverityT2, Score: fanIn/5 - d.maxFanOut/5, Status: quality.StatusOpen, Metadata: map[string]string{ "package": pkg, "fan_in": fmt.Sprintf("%d", fanIn), "imported_by": strings.Join(importedBy, ","), }, } findings = append(findings, finding) } } findings = append(findings, d.detectHubPackages(pkgImports, pkgImportedBy)...) return findings, nil } func (d *CouplingDetector) detectHubPackages(pkgImports, pkgImportedBy map[string][]string) []quality.Finding { var findings []quality.Finding for pkg, imports := range pkgImports { importedBy := pkgImportedBy[pkg] centrality := len(imports) + len(importedBy) if centrality > d.maxFanOut*3 { finding := quality.Finding{ ID: fmt.Sprintf("coupling_hub::%s", pkg), Type: "coupling", Title: fmt.Sprintf("Hub package detected: %s", filepath.Base(pkg)), Description: fmt.Sprintf("Package '%s' is a coupling hub with %d connections (%d imports, %d imported by). Consider splitting.", pkg, centrality, len(imports), len(importedBy)), File: pkg, Line: 1, Severity: quality.SeverityT4, Score: centrality / 5, Status: quality.StatusOpen, Metadata: map[string]string{ "package": pkg, "centrality": fmt.Sprintf("%d", centrality), "fan_out": fmt.Sprintf("%d", len(imports)), "fan_in": fmt.Sprintf("%d", len(importedBy)), }, } findings = append(findings, finding) } } return findings } type EnhancedDeadCodeDetector struct { *quality.BaseDetector } func NewEnhancedDeadCodeDetector(finder quality.FileFinder) *EnhancedDeadCodeDetector { return &EnhancedDeadCodeDetector{ BaseDetector: quality.NewBaseDetector("dead_code_enhanced", quality.SeverityT2, finder), } } func (d *EnhancedDeadCodeDetector) Name() string { return "dead_code_enhanced" } func (d *EnhancedDeadCodeDetector) Severity() quality.Severity { return quality.SeverityT2 } func (d *EnhancedDeadCodeDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedSyntax, Dir: path, } pkgs, err := packages.Load(cfg, "./...") if err != nil { return nil, fmt.Errorf("failed to load packages: %w", err) } used := make(map[string]bool) defs := make(map[string]ObjInfo) entryPoints := make(map[string]bool) for _, pkg := range pkgs { if pkg.Name == "main" { for _, file := range pkg.Syntax { ast.Inspect(file, func(n ast.Node) bool { if fn, ok := n.(*ast.FuncDecl); ok { if fn.Name.Name == "main" || fn.Name.Name == "init" { entryPoints[pkg.PkgPath+"."+fn.Name.Name] = true } } return true }) } } for _, obj := range pkg.TypesInfo.Uses { if obj != nil && obj.Pkg() != nil { used[obj.Pkg().Path()+"."+obj.Name()] = true } } for _, obj := range pkg.TypesInfo.Defs { if obj == nil || obj.Pkg() == nil { continue } key := obj.Pkg().Path() + "." + obj.Name() pos := pkg.Fset.Position(obj.Pos()) switch o := obj.(type) { case *types.Func: defs[key] = ObjInfo{ Name: obj.Name(), Type: "function", File: pos.Filename, Line: pos.Line, Package: obj.Pkg().Path(), Exported: obj.Exported(), Signature: o.Type().String(), } case *types.TypeName: defs[key] = ObjInfo{ Name: obj.Name(), Type: "type", File: pos.Filename, Line: pos.Line, Package: obj.Pkg().Path(), Exported: obj.Exported(), Underlying: o.Type().Underlying().String(), } case *types.Var: if obj.Exported() { defs[key] = ObjInfo{ Name: obj.Name(), Type: "variable", File: pos.Filename, Line: pos.Line, Package: obj.Pkg().Path(), Exported: obj.Exported(), } } } } } testPkgs := make(map[string]bool) for _, pkg := range pkgs { if strings.HasSuffix(pkg.PkgPath, "_test") || strings.Contains(pkg.Name, "test") { testPkgs[pkg.PkgPath] = true } for _, file := range pkg.GoFiles { if strings.HasSuffix(file, "_test.go") { testPkgs[pkg.PkgPath] = true } } } var findings []quality.Finding for key, def := range defs { if entryPoints[key] { continue } if strings.HasPrefix(def.Name, "Test") || strings.HasPrefix(def.Name, "Benchmark") || strings.HasPrefix(def.Name, "Fuzz") { continue } if strings.HasSuffix(def.Name, "Error") && def.Type == "type" { continue } if strings.Contains(def.File, "_test.go") { continue } if !used[key] && def.Exported { severity := quality.SeverityT2 score := 5 if strings.HasSuffix(def.File, "/cmd/") || strings.Contains(def.File, "/cmd/") { severity = quality.SeverityT3 score = 3 } if def.Type == "type" { severity = quality.SeverityT3 score = 4 } finding := quality.Finding{ ID: fmt.Sprintf("dead_code::%s::%s", def.File, def.Name), Type: "dead_code", Title: fmt.Sprintf("Unused exported %s: %s", def.Type, def.Name), Description: fmt.Sprintf("The exported %s '%s' is never used. Consider removing it or if it's part of a public API, document it.", def.Type, def.Name), File: def.File, Line: def.Line, Severity: severity, Score: score, Status: quality.StatusOpen, Metadata: map[string]string{ "name": def.Name, "obj_type": def.Type, "package": def.Package, "exported": "true", }, } findings = append(findings, finding) } } return findings, nil } type ObjInfo struct { Name string Type string File string Line int Package string Exported bool Signature string Underlying string }