mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 12:33:04 +00:00
213 lines
4.9 KiB
Go
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
|
|
}
|