Files
Devour/internal/quality/detectors/complexity.go
T
Tomas Dvorak 55885a0e8f first commit
2026-02-22 10:42:17 +01:00

213 lines
4.9 KiB
Go

package detectors
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/yourorg/devour/internal/quality"
)
// ComplexityDetector detects complexity issues in source code
type ComplexityDetector struct {
*quality.BaseDetector
signals []ComplexitySignal
}
// ComplexitySignal represents a complexity pattern to detect
type ComplexitySignal struct {
Name string
Pattern *regexp.Regexp
Weight int
Threshold int
Compute func(content string, lines []string) (int, string)
}
// NewComplexityDetector creates a new complexity detector
func NewComplexityDetector(finder quality.FileFinder) *ComplexityDetector {
detector := &ComplexityDetector{
BaseDetector: quality.NewBaseDetector("complexity", quality.SeverityT2, finder),
signals: []ComplexitySignal{
{
Name: "nested if statements",
Pattern: regexp.MustCompile(`^\s*if\s+.*\{\s*$`),
Weight: 2,
Threshold: 3,
},
{
Name: "nested for loops",
Pattern: regexp.MustCompile(`^\s*for\s+.*\{\s*$`),
Weight: 3,
Threshold: 2,
},
{
Name: "switch statements",
Pattern: regexp.MustCompile(`^\s*switch\s+.*\{\s*$`),
Weight: 1,
Threshold: 5,
},
{
Name: "function calls",
Pattern: regexp.MustCompile(`\w+\(`),
Weight: 1,
Threshold: 20,
},
},
}
// Add Go-specific complexity signals
detector.addGoSignals()
return detector
}
// addGoSignals adds Go-specific complexity signals
func (d *ComplexityDetector) addGoSignals() {
goSignals := []ComplexitySignal{
{
Name: "goroutines",
Pattern: regexp.MustCompile(`go\s+\w+\(`),
Weight: 2,
Threshold: 3,
},
{
Name: "channels",
Pattern: regexp.MustCompile(`make\s*\(\s*chan`),
Weight: 2,
Threshold: 3,
},
{
Name: "select statements",
Pattern: regexp.MustCompile(`^\s*select\s*\{`),
Weight: 3,
Threshold: 2,
},
{
Name: "defer statements",
Pattern: regexp.MustCompile(`^\s*defer\s+`),
Weight: 1,
Threshold: 5,
},
}
d.signals = append(d.signals, goSignals...)
}
// Name returns the detector name
func (d *ComplexityDetector) Name() string {
return "complexity"
}
// Severity returns the default severity
func (d *ComplexityDetector) Severity() quality.Severity {
return quality.SeverityT2
}
// Detect runs complexity detection on the given path
func (d *ComplexityDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
files, err := d.FindFiles(path, config.Language)
if err != nil {
return nil, fmt.Errorf("failed to find files: %w", err)
}
var findings []quality.Finding
for _, file := range files {
if quality.ShouldExclude(file, config.Exclude) {
continue
}
fileFindings, err := d.analyzeFile(file, config)
if err != nil {
log.Printf("Failed to analyze file %s: %v", file, err)
continue
}
findings = append(findings, fileFindings...)
}
return findings, nil
}
// analyzeFile analyzes a single file for complexity issues
func (d *ComplexityDetector) analyzeFile(filePath string, config *quality.Config) ([]quality.Finding, error) {
content, err := filepath.Abs(filePath)
if err != nil {
return nil, err
}
// Read file content
fileContent, err := os.ReadFile(content)
if err != nil {
return nil, err
}
contentStr := string(fileContent)
lines := strings.Split(contentStr, "\n")
loc := len(lines)
if loc < config.MinLOC {
return nil, nil
}
var findings []quality.Finding
score := 0
var signals []string
// Check each complexity signal
for _, signal := range d.signals {
var count int
var label string
if signal.Compute != nil {
c, l := signal.Compute(contentStr, lines)
if c > 0 {
count = c
label = l
}
} else if signal.Pattern != nil {
matches := signal.Pattern.FindAllString(contentStr, -1)
count = len(matches)
if count > signal.Threshold {
label = fmt.Sprintf("%d %s", count, signal.Name)
}
}
if count > signal.Threshold {
signals = append(signals, label)
excess := count - signal.Threshold
if signal.Threshold == 0 {
excess = count
}
score += excess * signal.Weight
}
}
// Create finding if score exceeds threshold
if score >= config.Threshold && len(signals) > 0 {
finding := quality.Finding{
ID: fmt.Sprintf("complexity-%s-%d", filepath.Base(filePath), score),
Type: "complexity",
Title: "High complexity detected",
Description: fmt.Sprintf("File has complexity score of %d with signals: %s", score, strings.Join(signals, ", ")),
File: filePath,
Line: 1,
Severity: d.Severity(),
Score: score,
Status: quality.StatusOpen,
Metadata: map[string]string{
"loc": strconv.Itoa(loc),
"signals": strings.Join(signals, ";"),
},
}
findings = append(findings, finding)
}
return findings, nil
}