Coverage for metrics/npath/python.py: 92%

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

2

3 from __future__ import annotations

4

5

6 def _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

16 def 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

24 def _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

32 def _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

62 def _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

94 def _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

106 def _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

133 def _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

150 def _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

171 def _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

185 def _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