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

Class diagram orchestrata_tools.complexity.analyzer

Dependency diagram

Dependency diagram orchestrata_tools.complexity.analyzer

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

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_tools/complexity/analyzer.py

"""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]