Coverage for metrics / cognitive / python.py: 89%
88 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
1"""Cognitive complexity for Python (SonarSource spec)."""
3from __future__ import annotations
5import functools
7from common import _score_boolean_chain as _score_boolean_chain_generic
8from metrics.cognitive.common import make_compute_cognitive
11# Control flow nodes that add 1 + nesting and then recurse at nesting + 1
12_PY_NESTING_INCREMENTORS = {
13 "for_statement",
14 "while_statement",
15 "except_clause",
16 "conditional_expression",
17 "match_statement",
18}
21def _flatten_boolean_ops(node, ops: list, right_subtrees: list) -> None:
22 """Flatten a left-associative boolean_operator tree into a list of operator strings."""
23 if node.type != "boolean_operator":
24 return
25 children = node.children
26 if len(children) >= 3:
27 left = children[0]
28 operator = children[1]
29 right = children[2]
30 if left.type == "boolean_operator":
31 _flatten_boolean_ops(left, ops, right_subtrees)
32 ops.append(operator.text.decode())
33 if right.type == "boolean_operator":
34 right_subtrees.append(right)
37_score_boolean_chain = functools.partial(
38 _score_boolean_chain_generic, flatten_fn=_flatten_boolean_ops
39)
42def _walk(node, nesting: int, func_name: str, top_func) -> int:
43 """Recursively walk AST and compute cognitive complexity."""
44 total = 0
46 for child in node.children:
47 if child.type == "function_definition" and child is not top_func:
48 continue
50 if child.type == "lambda":
51 total += _walk(child, nesting + 1, func_name, top_func)
52 continue
54 if child.type == "if_statement":
55 total += 1 + nesting
56 total += _walk_if(child, nesting, func_name, top_func)
57 continue
59 if child.type in _PY_NESTING_INCREMENTORS:
60 total += 1 + nesting
61 total += _walk(child, nesting + 1, func_name, top_func)
62 continue
64 if child.type == "boolean_operator":
65 total += _score_boolean_chain(child)
66 total += _walk_non_boolean(child, nesting, func_name, top_func)
67 continue
69 if child.type == "call":
70 total += _check_recursion(child, func_name)
72 total += _walk(child, nesting, func_name, top_func)
74 return total
77def _walk_if(node, nesting: int, func_name: str, top_func) -> int:
78 """Walk children of an if_statement/elif_clause."""
79 total = 0
80 for child in node.children:
81 if child.type == "elif_clause":
82 total += 1
83 total += _walk_if(child, nesting, func_name, top_func)
84 elif child.type == "else_clause":
85 total += 1
86 total += _walk(child, nesting + 1, func_name, top_func)
87 elif child.type == "block":
88 total += _walk(child, nesting + 1, func_name, top_func)
89 elif child.type == "boolean_operator":
90 total += _score_boolean_chain(child)
91 total += _walk_non_boolean(child, nesting, func_name, top_func)
92 elif child.type == "call":
93 total += _check_recursion(child, func_name)
94 total += _walk(child, nesting, func_name, top_func)
95 else:
96 total += _walk(child, nesting, func_name, top_func)
97 return total
100def _walk_non_boolean(node, nesting: int, func_name: str, top_func) -> int:
101 """Walk inside boolean expressions looking only for nested structures and recursion."""
102 total = 0
103 for child in node.children:
104 if child.type == "boolean_operator":
105 total += _walk_non_boolean(child, nesting, func_name, top_func)
106 continue
107 if child.type == "conditional_expression":
108 total += 1 + nesting
109 total += _walk(child, nesting + 1, func_name, top_func)
110 continue
111 if child.type == "call":
112 total += _check_recursion(child, func_name)
113 total += _walk(child, nesting, func_name, top_func)
114 continue
115 total += _walk_non_boolean(child, nesting, func_name, top_func)
116 return total
119def _check_recursion(call_node, func_name: str) -> int:
120 """Check if a call node is a recursive call to the enclosing function."""
121 if not func_name:
122 return 0
123 for child in call_node.children:
124 if child.type == "identifier" and child.text.decode() == func_name:
125 return 1
126 if child.type == "argument_list":
127 break
128 return 0
131compute_cognitive_py = make_compute_cognitive(_walk)