mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
505 lines
13 KiB
Go
505 lines
13 KiB
Go
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 {
|
|
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 := d.analyzeFile(file)
|
|
findings = append(findings, fileFindings...)
|
|
}
|
|
|
|
return findings, nil
|
|
}
|
|
|
|
func (d *GodStructDetector) analyzeFile(path string) []quality.Finding {
|
|
fset := token.NewFileSet()
|
|
node, err := parser.ParseFile(fset, path, nil, 0)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 := d.analyzeFile(file)
|
|
findings = append(findings, fileFindings...)
|
|
}
|
|
|
|
return findings, nil
|
|
}
|
|
|
|
func (d *DebugLogDetector) analyzeFile(path string) []quality.Finding {
|
|
fset := token.NewFileSet()
|
|
node, err := parser.ParseFile(fset, path, nil, 0)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
normPath := filepath.ToSlash(path)
|
|
if strings.Contains(normPath, "internal/ui/") || strings.Contains(normPath, "examples/") {
|
|
return 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
|
|
}
|
|
|
|
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
|
|
}
|