Coverage for metrics / npath / python.py: 92%
158 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"""NPath complexity for Python."""
3from __future__ import annotations
6def _count_bool_ops_py(node) -> int:
7 """Count boolean_operator nodes (and/or) in an expression subtree."""
8 count = 0
9 if node.type == "boolean_operator":
10 count += 1
11 for child in node.children:
12 count += _count_bool_ops_py(child)
13 return count
16def compute_npath_py(func_node) -> int:
17 """Compute NPath complexity for a Python function_definition node."""
18 for child in func_node.children:
19 if child.type == "block":
20 return _npath_block_py(child, func_node)
21 return 1
24def _npath_block_py(block, top_func) -> int:
25 """Multiply NPath of each child statement in a block."""
26 result = 1
27 for child in block.children:
28 result *= _npath_stmt_py(child, top_func)
29 return result
32def _npath_stmt_py(node, top_func) -> int:
33 """Dispatch a statement node to its NPath handler."""
34 if node.type == "function_definition" and node is not top_func:
35 return 1
36 if node.type == "class_definition":
37 return 1
38 if node.type == "decorated_definition":
39 return 1
41 if node.type == "if_statement":
42 return _npath_if_py(node, top_func)
43 if node.type in ("for_statement", "while_statement"):
44 return _npath_loop_py(node, top_func)
45 if node.type == "try_statement":
46 return _npath_try_py(node, top_func)
47 if node.type == "match_statement":
48 return _npath_match_py(node, top_func)
49 if node.type == "block":
50 return _npath_block_py(node, top_func)
51 if node.type == "with_statement":
52 for child in node.children:
53 if child.type == "block":
54 return _npath_block_py(child, top_func)
55 return 1
56 if node.type in ("expression_statement", "return_statement",
57 "assignment", "augmented_assignment"):
58 return _npath_expr_py(node, top_func)
59 return 1
62def _npath_if_py(node, top_func) -> int:
63 """NPath for if_statement with elif/else chains."""
64 total = 0
65 has_else = False
67 condition_bool_ops = 0
68 for child in node.children:
69 if child.type in ("block", "elif_clause", "else_clause"):
70 break
71 condition_bool_ops += _count_bool_ops_py(child)
73 for child in node.children:
74 if child.type == "block":
75 total += _npath_block_py(child, top_func)
76 break
78 for child in node.children:
79 if child.type == "elif_clause":
80 total += _npath_elif_py(child, top_func)
81 elif child.type == "else_clause":
82 has_else = True
83 for sub in child.children:
84 if sub.type == "block":
85 total += _npath_block_py(sub, top_func)
87 if not has_else:
88 total += 1
90 total += condition_bool_ops
91 return total
94def _npath_elif_py(node, top_func) -> int:
95 """NPath contribution of a single elif clause."""
96 body_npath = 1
97 bool_ops = 0
98 for child in node.children:
99 if child.type == "block":
100 body_npath = _npath_block_py(child, top_func)
101 elif child.type != "block":
102 bool_ops += _count_bool_ops_py(child)
103 return body_npath + bool_ops
106def _npath_loop_py(node, top_func) -> int:
107 """NPath for for_statement / while_statement (with optional else)."""
108 body_npath = 1
109 else_npath = 0
110 bool_ops = 0
111 has_else = False
113 for child in node.children:
114 if child.type == "block" and not has_else:
115 body_npath = _npath_block_py(child, top_func)
116 elif child.type == "else_clause":
117 has_else = True
118 for sub in child.children:
119 if sub.type == "block":
120 else_npath = _npath_block_py(sub, top_func)
122 if node.type == "while_statement":
123 for child in node.children:
124 if child.type == "block":
125 break
126 bool_ops += _count_bool_ops_py(child)
128 if has_else:
129 return body_npath + else_npath + bool_ops
130 return body_npath + 1 + bool_ops
133def _npath_try_py(node, top_func) -> int:
134 """NPath for try_statement: sum of try body + except/finally bodies."""
135 total = 0
136 for child in node.children:
137 if child.type == "block":
138 total += _npath_block_py(child, top_func)
139 elif child.type == "except_clause":
140 for sub in child.children:
141 if sub.type == "block":
142 total += _npath_block_py(sub, top_func)
143 elif child.type == "finally_clause":
144 for sub in child.children:
145 if sub.type == "block":
146 total += _npath_block_py(sub, top_func)
147 return max(total, 1)
150def _npath_match_py(node, top_func) -> int:
151 """NPath for match_statement: sum of case bodies (+1 if no wildcard)."""
152 total = 0
153 has_wildcard = False
154 for child in node.children:
155 if child.type == "block":
156 for case_node in child.children:
157 if case_node.type == "case_clause":
158 for sub in case_node.children:
159 if sub.type == "case_pattern":
160 pattern_text = sub.text.decode().strip()
161 if pattern_text == "_":
162 has_wildcard = True
163 for sub in case_node.children:
164 if sub.type == "block":
165 total += _npath_block_py(sub, top_func)
166 if not has_wildcard:
167 total += 1
168 return max(total, 1)
171def _npath_ternary_py(node, top_func) -> int:
172 """NPath for conditional_expression: NP(true) + NP(false) + bool_ops(cond)."""
173 children = [c for c in node.children if c.type not in ("if", "else")]
174 if len(children) >= 3:
175 true_expr = children[0]
176 condition = children[1]
177 false_expr = children[2]
178 np_true = _npath_expr_py(true_expr, top_func)
179 np_false = _npath_expr_py(false_expr, top_func)
180 bool_ops = _count_bool_ops_py(condition)
181 return np_true + np_false + bool_ops
182 return 2
185def _npath_expr_py(node, top_func) -> int:
186 """Scan an expression for embedded ternaries/lambdas, multiplying results."""
187 if node.type == "conditional_expression":
188 return _npath_ternary_py(node, top_func)
189 if node.type == "lambda":
190 for child in node.children:
191 if child.type not in ("lambda", "lambda_parameters", ":"):
192 return _npath_expr_py(child, top_func)
193 return 1
194 if node.type == "function_definition":
195 return 1
196 if node.type == "class_definition":
197 return 1
199 result = 1
200 for child in node.children:
201 child_np = _npath_expr_py(child, top_func)
202 if child_np > 1:
203 result *= child_np
204 return result