package analyzers import ( "context" "fmt" "go/parser" "go/token" "os" "strings" "github.com/yourorg/devour/internal/quality" "golang.org/x/tools/go/packages" ) type DeadCodeDetector struct { *quality.BaseDetector } func NewDeadCodeDetector(finder quality.FileFinder) *DeadCodeDetector { return &DeadCodeDetector{ BaseDetector: quality.NewBaseDetector("dead_code", quality.SeverityT2, finder), } } func (d *DeadCodeDetector) Name() string { return "dead_code" } func (d *DeadCodeDetector) Severity() quality.Severity { return quality.SeverityT2 } func (d *DeadCodeDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles, Dir: path, } pkgs, err := packages.Load(cfg, "./...") if err != nil { return nil, fmt.Errorf("failed to load packages: %w", err) } var findings []quality.Finding used := make(map[string]bool) for _, pkg := range pkgs { for _, obj := range pkg.TypesInfo.Uses { if obj != nil && obj.Pkg() != nil { used[obj.Pkg().Path()+"."+obj.Name()] = true } } } for _, pkg := range pkgs { for _, obj := range pkg.TypesInfo.Defs { if obj == nil || obj.Pkg() == nil { continue } if !obj.Exported() { continue } key := obj.Pkg().Path() + "." + obj.Name() if !used[key] { pos := pkg.Fset.Position(obj.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("dead_code::%s::%s", pos.Filename, obj.Name()), Type: "dead_code", Title: fmt.Sprintf("Unused exported identifier: %s", obj.Name()), Description: fmt.Sprintf("The exported %s '%s' is never used in the codebase. Consider removing it or documenting its intended use.", obj.Type(), obj.Name()), File: pos.Filename, Line: pos.Line, Severity: quality.SeverityT2, Score: 5, Status: quality.StatusOpen, Metadata: map[string]string{ "name": obj.Name(), "type": obj.Type().String(), "package": obj.Pkg().Path(), "exported": "true", }, } findings = append(findings, finding) } } } return findings, nil } type UnusedImportDetector struct { *quality.BaseDetector } func NewUnusedImportDetector(finder quality.FileFinder) *UnusedImportDetector { return &UnusedImportDetector{ BaseDetector: quality.NewBaseDetector("unused_import", quality.SeverityT1, finder), } } func (d *UnusedImportDetector) Name() string { return "unused_import" } func (d *UnusedImportDetector) Severity() quality.Severity { return quality.SeverityT1 } func (d *UnusedImportDetector) 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 { continue } findings = append(findings, fileFindings...) } return findings, nil } func (d *UnusedImportDetector) analyzeFile(path string) ([]quality.Finding, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly|parser.ParseComments) if err != nil { return nil, err } imports := make(map[string]string) for _, imp := range node.Imports { pkgPath := strings.Trim(imp.Path.Value, `"`) name := "" if imp.Name != nil { name = imp.Name.Name } else { parts := strings.Split(pkgPath, "/") name = parts[len(parts)-1] } imports[pkgPath] = name } content, err := os.ReadFile(path) if err != nil { return nil, err } contentStr := string(content) var findings []quality.Finding for _, imp := range node.Imports { pkgPath := strings.Trim(imp.Path.Value, `"`) name := "" if imp.Name != nil { name = imp.Name.Name } else { parts := strings.Split(pkgPath, "/") name = parts[len(parts)-1] } if name == "_" || name == "." { continue } pattern := name + "." if !strings.Contains(contentStr, pattern) { pos := fset.Position(imp.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("unused_import::%s::%s", path, pkgPath), Type: "unused_import", Title: fmt.Sprintf("Unused import: %s", pkgPath), Description: fmt.Sprintf("The import '%s' is not used in this file. Remove it to clean up the code.", pkgPath), File: path, Line: pos.Line, Severity: quality.SeverityT1, Score: 2, Status: quality.StatusOpen, Metadata: map[string]string{ "import_path": pkgPath, "alias": name, }, } findings = append(findings, finding) } } return findings, nil } type CycleDetector struct { *quality.BaseDetector } func NewCycleDetector(finder quality.FileFinder) *CycleDetector { return &CycleDetector{ BaseDetector: quality.NewBaseDetector("import_cycle", quality.SeverityT4, finder), } } func (d *CycleDetector) Name() string { return "import_cycle" } func (d *CycleDetector) Severity() quality.Severity { return quality.SeverityT4 } func (d *CycleDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedImports, Dir: path, } pkgs, err := packages.Load(cfg, "./...") if err != nil { return nil, fmt.Errorf("failed to load packages: %w", err) } localPkgs := make(map[string]bool) for _, pkg := range pkgs { localPkgs[pkg.PkgPath] = true } graph := make(map[string][]string) for _, pkg := range pkgs { for _, imp := range pkg.Imports { if localPkgs[imp.PkgPath] { graph[pkg.PkgPath] = append(graph[pkg.PkgPath], imp.PkgPath) } } } cycles := d.findCycles(graph) var findings []quality.Finding for i, cycle := range cycles { finding := quality.Finding{ ID: fmt.Sprintf("import_cycle::%d", i), Type: "import_cycle", Title: "Import cycle detected", Description: fmt.Sprintf("Circular import dependency: %s", strings.Join(cycle, " → ")), File: cycle[0], Line: 1, Severity: quality.SeverityT4, Score: 20, Status: quality.StatusOpen, Metadata: map[string]string{ "cycle": strings.Join(cycle, ","), }, } findings = append(findings, finding) } return findings, nil } func (d *CycleDetector) findCycles(graph map[string][]string) [][]string { var cycles [][]string visited := make(map[string]bool) recStack := make(map[string]bool) var dfs func(node string, path []string) dfs = func(node string, path []string) { visited[node] = true recStack[node] = true path = append(path, node) for _, neighbor := range graph[node] { if !visited[neighbor] { dfs(neighbor, path) } else if recStack[neighbor] { cycleStart := -1 for i, n := range path { if n == neighbor { cycleStart = i break } } if cycleStart >= 0 { cycle := make([]string, len(path)-cycleStart) copy(cycle, path[cycleStart:]) cycles = append(cycles, cycle) } } } path = path[:len(path)-1] recStack[node] = false } for node := range graph { if !visited[node] { dfs(node, []string{}) } } return cycles }