orchestrata_ultimate.refactor.codemod.analysis.metrics

Reference › orchestrata_ultimate › refactor › codemod › analysis › metrics

ComplexityMetrics computes three complexity measures over ast nodes: cyclomatic (decision points plus one), max_nesting (deepest nesting of control structures) and cognitive (a score that weights each branch by its nesting level, with special handling of elif). The helpers _scope_children and _scope_nodes stop at nested function and class scopes so each function is measured on its own. ComplexityAnalyzer is its caller; it has no dependencies beyond the traceability decorator.

At a glance

Source src/orchestrata_ultimate/refactor/codemod/analysis/metrics.py
Lines of code 169
Classes 1
Functions 8
Imports (internal) orchestrata_ultimate.core.base.traces
Imported by orchestrata_ultimate.refactor.codemod.analysis, orchestrata_ultimate.refactor.codemod.analysis.complexity
Tests that import it none

Class diagram

Class diagram orchestrata_ultimate.refactor.codemod.analysis.metrics

Dependency diagram

Dependency diagram orchestrata_ultimate.refactor.codemod.analysis.metrics

API

Classes

ComplexityMetrics

Decorators: traces(spec='110', story='US-110-006', reqs=['REQ-110-031', 'REQ-110-032']) · Line 52

Static complexity metrics over a function's ast subtree.

Method Signature Summary
cyclomatic staticmethod cyclomatic(node: ast.AST) -> int McCabe complexity: 1 + decision points.
max_nesting staticmethod max_nesting(node: ast.AST) -> int Deepest nesting of control structures (an elif does not nest).
cognitive staticmethod cognitive(node: ast.AST) -> int Sonar-style cognitive complexity.

Functions

Function Signature Summary
_scope_children _scope_children(node: ast.AST) -> list[ast.AST] Children that belong to node's own scope (a def's body, not its header).
_scope_nodes _scope_nodes(node: ast.AST) -> Iterator[ast.AST] Every node in node's scope, not descending into nested defs.
_nesting _nesting(children: list[ast.AST], level: int) -> int
_nesting_if _nesting_if(node: ast.If, level: int) -> int Depth below an if already counted at level; an elif shares it.
_cognitive _cognitive(children: list[ast.AST], nesting: int) -> int
_cognitive_node _cognitive_node(node: ast.AST, nesting: int) -> int
_cognitive_fields _cognitive_fields(node: ast.AST, nesting: int) -> int Children of a structure that nests one level deeper.
_cognitive_if _cognitive_if(node: ast.If, nesting: int, *, is_elif: bool) -> int

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/refactor/codemod/analysis/metrics.py

"""ComplexityMetrics — cyclomatic, nesting and cognitive complexity over ``ast`` nodes.

The CIG answers no complexity query (GAP-6), so the metrics are computed from
the forest's ``ast`` overlay. All three scan one function's own scope: nested
function and class bodies are separate scopes and are not counted into the
enclosing function (lambdas and comprehensions are part of the scope).
"""

from __future__ import annotations

import ast
from collections.abc import Iterator

from orchestrata_ultimate.core.base.traces import traces

__all__ = ["ComplexityMetrics"]

_DEFS = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
_LOOPS = (ast.For, ast.AsyncFor, ast.While)
_NESTING = (
    ast.If,
    ast.For,
    ast.AsyncFor,
    ast.While,
    ast.Try,
    ast.TryStar,
    ast.With,
    ast.AsyncWith,
    ast.Match,
)


def _scope_children(node: ast.AST) -> list[ast.AST]:
    """Children that belong to ``node``'s own scope (a def's body, not its header)."""
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
        return list(node.body)
    return list(ast.iter_child_nodes(node))


def _scope_nodes(node: ast.AST) -> Iterator[ast.AST]:
    """Every node in ``node``'s scope, not descending into nested defs."""
    stack = _scope_children(node)
    while stack:
        current = stack.pop()
        yield current
        if isinstance(current, _DEFS):
            continue
        stack.extend(ast.iter_child_nodes(current))


@traces(spec="110", story="US-110-006", reqs=["REQ-110-031", "REQ-110-032"])
class ComplexityMetrics:
    """Static complexity metrics over a function's ``ast`` subtree."""

    @traces(spec="110", story="US-110-006", reqs=["REQ-110-031"])
    @staticmethod
    def cyclomatic(node: ast.AST) -> int:
        """McCabe complexity: 1 + decision points.

        Decision points: if/elif, for, while, except handler, conditional
        expression, each extra operand of a boolean operator, each
        comprehension generator plus its filters, each match case.
        """
        total = 1
        for current in _scope_nodes(node):
            if isinstance(
                current,
                (ast.If, ast.IfExp, ast.ExceptHandler, ast.match_case, *_LOOPS),
            ):
                total += 1
            elif isinstance(current, ast.BoolOp):
                total += len(current.values) - 1
            elif isinstance(current, ast.comprehension):
                total += 1 + len(current.ifs)
        return total

    @traces(spec="110", story="US-110-006", reqs=["REQ-110-031"])
    @staticmethod
    def max_nesting(node: ast.AST) -> int:
        """Deepest nesting of control structures (an elif does not nest)."""
        return _nesting(_scope_children(node), 0)

    @traces(spec="110", story="US-110-006", reqs=["REQ-110-031"])
    @staticmethod
    def cognitive(node: ast.AST) -> int:
        """Sonar-style cognitive complexity.

        +1 per branching structure plus its nesting depth; elif and else
        add 1 without a nesting penalty; each boolean-operator node adds 1;
        a lambda raises the nesting of its body.
        """
        return _cognitive(_scope_children(node), 0)


def _nesting(children: list[ast.AST], level: int) -> int:
    deepest = level
    for child in children:
        if isinstance(child, _DEFS):
            continue
        if isinstance(child, ast.If):
            deepest = max(deepest, _nesting_if(child, level + 1))
        elif isinstance(child, _NESTING):
            deepest = max(
                deepest, _nesting(list(ast.iter_child_nodes(child)), level + 1)
            )
        else:
            deepest = max(deepest, _nesting(list(ast.iter_child_nodes(child)), level))
    return deepest


def _nesting_if(node: ast.If, level: int) -> int:
    """Depth below an ``if`` already counted at ``level``; an elif shares it."""
    deepest = _nesting(list(node.body), level)
    orelse = node.orelse
    if len(orelse) == 1 and isinstance(orelse[0], ast.If):
        return max(deepest, _nesting_if(orelse[0], level))
    return max(deepest, _nesting(list(orelse), level))


def _cognitive(children: list[ast.AST], nesting: int) -> int:
    total = 0
    for child in children:
        if isinstance(child, _DEFS):
            continue
        total += _cognitive_node(child, nesting)
    return total


def _cognitive_node(node: ast.AST, nesting: int) -> int:
    if isinstance(node, ast.If):
        return _cognitive_if(node, nesting, is_elif=False)
    if isinstance(node, _LOOPS):
        return 1 + nesting + _cognitive_fields(node, nesting)
    if isinstance(node, (ast.Try, ast.TryStar)):
        total = _cognitive(list(node.body), nesting)
        for handler in node.handlers:
            total += 1 + nesting + _cognitive(list(handler.body), nesting + 1)
        total += _cognitive(list(node.orelse), nesting)
        total += _cognitive(list(node.finalbody), nesting)
        return total
    if isinstance(node, ast.IfExp):
        return 1 + nesting + _cognitive_fields(node, nesting)
    if isinstance(node, ast.Match):
        total = 1 + nesting
        for case in node.cases:
            total += _cognitive(list(case.body), nesting + 1)
        return total
    if isinstance(node, ast.BoolOp):
        return 1 + _cognitive(list(ast.iter_child_nodes(node)), nesting)
    if isinstance(node, ast.Lambda):
        return _cognitive([node.body], nesting + 1)
    return _cognitive(list(ast.iter_child_nodes(node)), nesting)


def _cognitive_fields(node: ast.AST, nesting: int) -> int:
    """Children of a structure that nests one level deeper."""
    return _cognitive(list(ast.iter_child_nodes(node)), nesting + 1)


def _cognitive_if(node: ast.If, nesting: int, *, is_elif: bool) -> int:
    total = 1 if is_elif else 1 + nesting
    total += _cognitive([node.test], nesting)
    total += _cognitive(list(node.body), nesting + 1)
    orelse = node.orelse
    if len(orelse) == 1 and isinstance(orelse[0], ast.If):
        total += _cognitive_if(orelse[0], nesting, is_elif=True)
    elif orelse:
        total += 1 + _cognitive(list(orelse), nesting + 1)
    return total