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

1"""Cognitive complexity for Python (SonarSource spec).""" 

2 

3from __future__ import annotations 

4 

5import functools 

6 

7from common import _score_boolean_chain as _score_boolean_chain_generic 

8from metrics.cognitive.common import make_compute_cognitive 

9 

10 

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} 

19 

20 

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) 

35 

36 

37_score_boolean_chain = functools.partial( 

38 _score_boolean_chain_generic, flatten_fn=_flatten_boolean_ops 

39) 

40 

41 

42def _walk(node, nesting: int, func_name: str, top_func) -> int: 

43 """Recursively walk AST and compute cognitive complexity.""" 

44 total = 0 

45 

46 for child in node.children: 

47 if child.type == "function_definition" and child is not top_func: 

48 continue 

49 

50 if child.type == "lambda": 

51 total += _walk(child, nesting + 1, func_name, top_func) 

52 continue 

53 

54 if child.type == "if_statement": 

55 total += 1 + nesting 

56 total += _walk_if(child, nesting, func_name, top_func) 

57 continue 

58 

59 if child.type in _PY_NESTING_INCREMENTORS: 

60 total += 1 + nesting 

61 total += _walk(child, nesting + 1, func_name, top_func) 

62 continue 

63 

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 

68 

69 if child.type == "call": 

70 total += _check_recursion(child, func_name) 

71 

72 total += _walk(child, nesting, func_name, top_func) 

73 

74 return total 

75 

76 

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 

98 

99 

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 

117 

118 

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 

129 

130 

131compute_cognitive_py = make_compute_cognitive(_walk)