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