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
Dependency diagram
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
"""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