mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 12:33:04 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user