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 }