Coverage for metrics/cognitive/go.py: 87%

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

2

3 from __future__ import annotations

4

5 from common import _is_logical_binary, _score_boolean_chain

6 from metrics.cognitive.common import make_compute_cognitive

7 from parsers.go import is_named_func_literal

8

9 _NESTING_CONTAINERS = {

10 "function_declaration",

11 "method_declaration",

12 "func_literal",

13}

14

15# Control flow nodes that add 1 + nesting and then recurse at nesting + 1

16 _GO_NESTING_INCREMENTORS = {

17 "for_statement",

18 "expression_switch_statement",

19 "type_switch_statement",

20 "select_statement",

21}

22

23

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

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

26 total = 0

27

28 for child in node.children:

29 if child.type in _NESTING_CONTAINERS and child is not top_func:

30 if child.type == "func_literal" and not is_named_func_literal(child):

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

32 continue

33

34 if child.type == "if_statement":

35 total += 1 + nesting

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

37 continue

38

39 if child.type in _GO_NESTING_INCREMENTORS:

40 total += 1 + nesting

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

42 continue

43

44 if _is_logical_binary(child):

45 total += _score_boolean_chain(child)

46 total += _walk_non_boolean(child, nesting, func_name, top_func)

47 continue

48

49 if child.type == "call_expression":

50 total += _check_recursion(child, func_name)

51

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

53

54 return total

55

56

57 def _walk_if(node, nesting: int, func_name: str, top_func) -> int:

58 """Walk children of a Go if_statement."""

59 total = 0

60 consequence = node.child_by_field_name("consequence")

61 alternative = node.child_by_field_name("alternative")

62

63 for child in node.children:

64 if consequence is not None and child == consequence:

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

66 elif alternative is not None and child == alternative:

67 if child.type == "if_statement":

68 total += 1

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

70 elif child.type == "block":

71 total += 1

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

73 elif _is_logical_binary(child):

74 total += _score_boolean_chain(child)

75 total += _walk_non_boolean(child, nesting, func_name, top_func)

76 elif child.type == "call_expression":

77 total += _check_recursion(child, func_name)

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

79 else:

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

81

82 return total

83

84

85 def _walk_non_boolean(node, nesting: int, func_name: str, top_func) -> int:

86 """Walk inside boolean expressions looking only for nested structures and recursion."""

87 total = 0

88 for child in node.children:

89 if _is_logical_binary(child):

90 total += _walk_non_boolean(child, nesting, func_name, top_func)

91 continue

92 if child.type == "call_expression":

93 total += _check_recursion(child, func_name)

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

95 continue

96 total += _walk_non_boolean(child, nesting, func_name, top_func)

97 return total

98

99

100 def _check_recursion(call_node, func_name: str) -> int:

101 """Check if a call_expression is a recursive call to the enclosing function."""

102 if not func_name:

103 return 0

104 callee = call_node.child_by_field_name("function")

105 if callee and callee.type == "identifier" and callee.text.decode() == func_name:

106 return 1

107 return 0

108

109

110 compute_cognitive_go = make_compute_cognitive(_walk)