orchestrata_tools.complexity.analyzer
Reference › orchestrata_tools › complexity › analyzer
This module scores the complexity of methods and flags oversized classes. It works from method metadata held in an ASTIndex, not from method bodies, so its numbers are heuristics. The module itself says an accurate measure needs the source parser.
ComplexityAnalyzer.cyclomatic starts at 1 and adds one for each branch keyword (if, elif, else, for, while, except, finally, with, assert, and, or) found in the docstring, one for an async method, one for a property, and one for each parameter beyond three, capped at 20. cognitive adds the deepest docstring indent in four-space steps plus the control-flow keyword count, capped at 30. analyze_codebase returns a ComplexityReport with the method count, average and maximum of both scores, the 20 most complex methods, and god classes, meaning classes with more than 15 methods or a summed cyclomatic score above 30. most_complex_methods returns the top N with scores. An empty codebase gives an empty report.
At a glance
| Source | src/orchestrata_tools/complexity/analyzer.py |
| Lines of code | 131 |
| Classes | 2 |
| Functions | 0 |
| Imports (internal) | orchestrata_tools.models.ast_models, orchestrata_tools.parser.indexer |
| Imported by | orchestrata_tools.complexity |
| Tests that import it | none |
Class diagram
Dependency diagram
API
Classes
ComplexityReport (dataclass)
Decorators: dataclass · Line 19
Aggregate complexity metrics for a codebase.
| Attribute | Type | Default |
|---|---|---|
total_methods |
int |
0 |
avg_cyclomatic |
float |
0.0 |
avg_cognitive |
float |
0.0 |
max_cyclomatic |
int |
0 |
max_cognitive |
int |
0 |
god_classes |
list[tuple[str, int]] |
field(default_factory=list) |
most_complex |
list[tuple[str, int, int]] |
field(default_factory=list) |
ComplexityAnalyzer
Line 32
Analyze complexity of methods and classes in a codebase.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(index: ASTIndex) |
|
cyclomatic |
cyclomatic(method: MethodInfo) -> int |
Estimate cyclomatic complexity from docstring and metadata. |
cognitive |
cognitive(method: MethodInfo) -> int |
Estimate cognitive complexity (nesting depth heuristic). |
analyze_codebase |
analyze_codebase() -> ComplexityReport |
Run full complexity analysis and return a report. |
most_complex_methods |
most_complex_methods(n: int = 20) -> list[tuple[MethodInfo, int, int]] |
Return the N most complex methods with their scores. |
Source
"""Complexity analysis engine.
Computes cyclomatic and cognitive complexity from method metadata,
detects god classes, and identifies the most complex methods.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from orchestrata_tools.models.ast_models import MethodInfo
from orchestrata_tools.parser.indexer import ASTIndex
@dataclass
class ComplexityReport:
"""Aggregate complexity metrics for a codebase."""
total_methods: int = 0
avg_cyclomatic: float = 0.0
avg_cognitive: float = 0.0
max_cyclomatic: int = 0
max_cognitive: int = 0
god_classes: list[tuple[str, int]] = field(default_factory=list)
most_complex: list[tuple[str, int, int]] = field(default_factory=list)
# (qualified_name, cyclomatic, cognitive)
class ComplexityAnalyzer:
"""Analyze complexity of methods and classes in a codebase."""
# Complexity keywords that indicate branching
BRANCH_KEYWORDS = (
"if",
"elif",
"else",
"for",
"while",
"except",
"finally",
"with",
"assert",
"and",
"or",
)
def __init__(self, index: ASTIndex):
self.index = index
def cyclomatic(self, method: MethodInfo) -> int:
"""Estimate cyclomatic complexity from docstring and metadata.
This is a heuristic based on method properties since we don't have
the raw AST body. For accurate complexity, use source_parser.
"""
score = 1 # Base complexity
doc = method.docstring or ""
# Count branch indicators in docstring (heuristic)
for kw in self.BRANCH_KEYWORDS:
score += len(re.findall(rf"\b{kw}\b", doc, re.IGNORECASE))
# Async adds a branch point
if method.is_async:
score += 1
# Property with getter/setter logic
if method.is_property:
score += 1
# Parameter count adds decision points
score += max(0, len(method.parameters) - 3)
return min(score, 20) # Cap heuristic
def cognitive(self, method: MethodInfo) -> int:
"""Estimate cognitive complexity (nesting depth heuristic)."""
score = 0
doc = method.docstring or ""
# Nested structures in docstring suggest nesting
indent_pattern = re.compile(r"^\s+", re.MULTILINE)
indents = [len(m.group()) for m in indent_pattern.finditer(doc)]
if indents:
score += max(indents) // 4 # ~4 spaces per indent level
# Each branch keyword adds cognitive load
for kw in ("if", "elif", "else", "for", "while", "except", "with"):
score += len(re.findall(rf"\b{kw}\b", doc, re.IGNORECASE))
return min(score, 30)
def analyze_codebase(self) -> ComplexityReport:
"""Run full complexity analysis and return a report."""
methods = self.index.codebase.all_methods
if not methods:
return ComplexityReport()
cyclomatic_scores = [self.cyclomatic(m) for m in methods]
cognitive_scores = [self.cognitive(m) for m in methods]
most_complex = sorted(
[
(m.qualified_name, cyc, cog)
for m, cyc, cog in zip(methods, cyclomatic_scores, cognitive_scores)
],
key=lambda x: (x[1], x[2]),
reverse=True,
)[:20]
# God classes: classes with many methods or high total complexity
god_classes = []
for cls in self.index.codebase.all_classes:
total_complexity = sum(self.cyclomatic(m) for m in cls.methods)
if cls.method_count > 15 or total_complexity > 30:
god_classes.append((cls.qualified_name, cls.method_count))
god_classes.sort(key=lambda x: x[1], reverse=True)
return ComplexityReport(
total_methods=len(methods),
avg_cyclomatic=sum(cyclomatic_scores) / len(methods),
avg_cognitive=sum(cognitive_scores) / len(methods),
max_cyclomatic=max(cyclomatic_scores),
max_cognitive=max(cognitive_scores),
god_classes=god_classes[:20],
most_complex=most_complex,
)
def most_complex_methods(self, n: int = 20) -> list[tuple[MethodInfo, int, int]]:
"""Return the N most complex methods with their scores."""
scored = [
(m, self.cyclomatic(m), self.cognitive(m))
for m in self.index.codebase.all_methods
]
scored.sort(key=lambda x: (x[1], x[2]), reverse=True)
return scored[:n]