File size: 3,665 Bytes
4b445f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""
Radon Complexity Analysis Tool
================================

Radon measures cyclomatic complexity — the number of independent execution paths
through a function. Higher complexity = more branches = harder to test and maintain,
AND often correlates with performance issues (deeply nested conditionals often
indicate O(n²) or worse algorithms).

Complexity grades:
  A (1-5):   Simple, low risk
  B (6-10):  Moderate complexity
  C (11-15): High complexity — consider refactoring
  D (16-20): Very high — likely performance and maintenance issues
  E (21-25): Extremely complex
  F (26+):   Unmaintainable

We report functions with complexity grade C or worse (>10) to the Performance Agent.
The agent uses this as a signal to look deeper at those functions for algorithmic issues.
"""

from __future__ import annotations

import json
import subprocess
import tempfile
from pathlib import Path

import structlog

logger = structlog.get_logger()


async def run_radon(file_contents: dict[str, str]) -> str:
    """
    Run radon cyclomatic complexity analysis on Python files.

    Returns a formatted string summarizing high-complexity functions.
    """
    python_files = {
        path: content
        for path, content in file_contents.items()
        if path.endswith(".py")
    }

    if not python_files:
        return ""

    try:
        with tempfile.TemporaryDirectory(prefix="ninjacg_radon_") as tmpdir:
            tmpdir_path = Path(tmpdir)

            for filepath, content in python_files.items():
                file_path = tmpdir_path / filepath
                file_path.parent.mkdir(parents=True, exist_ok=True)
                file_path.write_text(content, encoding="utf-8")

            # Run radon cc (cyclomatic complexity) with JSON output
            # -j: JSON output
            # -n C: only show grade C or worse (complexity > 10)
            result = subprocess.run(
                ["radon", "cc", "-j", "-n", "C", str(tmpdir_path)],
                capture_output=True,
                text=True,
                timeout=30,
            )

            if not result.stdout.strip() or result.stdout.strip() == "{}":
                return ""

            radon_output = json.loads(result.stdout)

            # Collect high-complexity functions
            findings = []
            for file_path, functions in radon_output.items():
                try:
                    relative = str(Path(file_path).relative_to(tmpdir)).replace("\\", "/")
                except ValueError:
                    relative = Path(file_path).name

                for func in functions:
                    if not isinstance(func, dict):
                        continue
                    name = func.get("name", "unknown")
                    complexity = func.get("complexity", 0)
                    rank = func.get("rank", "?")
                    lineno = func.get("lineno", 0)
                    findings.append(
                        f"- {relative}:{lineno} — `{name}()` complexity={complexity} (grade {rank})"
                    )

            if not findings:
                return ""

            summary = (
                f"Radon complexity analysis found {len(findings)} high-complexity function(s):\n"
                + "\n".join(findings)
            )
            logger.info("Radon analysis complete", high_complexity_count=len(findings))
            return summary

    except FileNotFoundError:
        logger.warning("radon not found in PATH — skipping complexity analysis")
        return ""
    except Exception as e:
        logger.warning("Radon analysis failed", error=str(e))
        return ""