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

1"""NPath complexity for Python.""" 

2 

3from __future__ import annotations 

4 

5 

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 

14 

15 

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 

22 

23 

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 

30 

31 

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 

40 

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 

60 

61 

62def _npath_if_py(node, top_func) -> int: 

63 """NPath for if_statement with elif/else chains.""" 

64 total = 0 

65 has_else = False 

66 

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) 

72 

73 for child in node.children: 

74 if child.type == "block": 

75 total += _npath_block_py(child, top_func) 

76 break 

77 

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) 

86 

87 if not has_else: 

88 total += 1 

89 

90 total += condition_bool_ops 

91 return total 

92 

93 

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 

104 

105 

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 

112 

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) 

121 

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) 

127 

128 if has_else: 

129 return body_npath + else_npath + bool_ops 

130 return body_npath + 1 + bool_ops 

131 

132 

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) 

148 

149 

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) 

169 

170 

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 

183 

184 

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 

198 

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