Coverage for metrics/cognitive/python.py: 89%

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

2

3 from __future__ import annotations

4

5 import functools

6

7 from common import _score_boolean_chain as _score_boolean_chain_generic

8 from 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

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

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

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

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

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

131 compute_cognitive_py = make_compute_cognitive(_walk)