Source code for rude.rules.mccabe.complexity
"""
McCabe complexity rule: C901.
C901: function is too complex
"""
from __future__ import annotations
from collections.abc import Iterator
from typing import TYPE_CHECKING, Any, ClassVar
from rude.core.node_types import NodeType
from rude.core.rule import Rule
from rude.core.types import Diagnostic
if TYPE_CHECKING:
from rude.core.node import Node
[docs]
class FunctionTooComplex(Rule):
"""
C901: Function is too complex (cyclomatic complexity).
Cyclomatic complexity measures the number of independent paths through
a function. High complexity indicates code that is hard to test and
maintain.
Complexity is calculated as:
1 (base) + if + elif + for + while + except + ternary + case
Example::
# Bad
def complex_function(x): # C901 if complexity > threshold
if x > 0: # +1
if x > 10: # +1
return "big"
elif x > 5: # +1
return "medium"
for i in range(x): # +1
if i % 2 == 0: # +1
print(i)
return "small"
# Total: 1 + 5 = 6
# Good - extract branches into helpers
def process(x):
if x > 0:
return handle_positive(x)
return handle_range(x)
Configuration:
[tool.rude.rules.C901]
max_complexity = 10 # default
"""
code: ClassVar[str] = "C901"
message: ClassVar[str] = "'{name}' is too complex ({complexity})"
node_types = {NodeType.FUNCTION_DEFINITION}
max_complexity: int = 10
# Node types that increase complexity (matches McCabe/flake8 semantics)
DECISION_NODES = {
"if_statement",
"elif_clause",
"for_statement",
"while_statement",
"except_clause",
"conditional_expression", # ternary: x if cond else y
"case_clause",
}
_FUNC_TYPES = frozenset(("function_definition",))
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Skip if complexity checking is disabled
if self.max_complexity < 0:
return
# Skip nested functions — their branches are counted as part of the
# enclosing function's complexity (matching mccabe/flake8 behaviour).
parent = node.raw.parent
while parent is not None:
if parent.type in self._FUNC_TYPES:
return
parent = parent.parent
name_node = node.child_by_field("name")
name = name_node.text if name_node else "<anonymous>"
complexity = self._calculate_complexity(node)
if complexity > self.max_complexity:
yield self.diagnostic(
node,
self.message.format(name=name, complexity=complexity),
)
def _calculate_complexity(self, node: Node) -> int:
"""Calculate cyclomatic complexity of a function.
Traverses the entire subtree including nested functions, matching
mccabe/flake8 semantics where nested function branches count towards
the enclosing function's complexity.
"""
complexity = 1 # Base complexity
cursor = node.raw.walk()
decision_nodes = self.DECISION_NODES
# Track root node ID to know when we're done
root_node = cursor.node
if root_node is None:
return complexity
root_id = root_node.id
# Descend into children first (skip the root function node itself)
if not cursor.goto_first_child():
return complexity
while True:
current = cursor.node
if current is None:
return complexity
node_type = current.type
# Count complexity
if node_type in decision_nodes:
complexity += 1
# Continue traversal: depth-first
if cursor.goto_first_child():
continue
if cursor.goto_next_sibling():
continue
# Backtrack up the tree
while cursor.goto_parent():
parent = cursor.node
if parent is not None and parent.id == root_id:
return complexity
if cursor.goto_next_sibling():
break
else:
return complexity
return complexity
COMPLEXITY_RULES = [
FunctionTooComplex,
]
__all__ = [
"COMPLEXITY_RULES",
"FunctionTooComplex",
]