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

1"""Cognitive complexity for TypeScript (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

8 _NESTING_CONTAINERS = {

9 "function_declaration",

10 "function_expression",

11 "method_definition",

12 "arrow_function",

13 "generator_function_declaration",

14}

15

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

17 _TS_NESTING_INCREMENTORS = {

18 "for_statement",

19 "for_in_statement",

20 "while_statement",

21 "do_statement",

22 "catch_clause",

23 "ternary_expression",

24 "switch_statement",

25}

26

27

28 def _is_skip_container(child, top_func) -> bool:

29 """Check if a child is a nested function container that should be skipped."""

30 if child.type not in _NESTING_CONTAINERS or child is top_func:

31 return False

32 if child.type == "arrow_function":

33 return bool(child.parent and child.parent.type == "variable_declarator")

34 return True

35

36

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

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

39 total = 0

40

41 for child in node.children:

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

43 if not _is_skip_container(child, top_func):

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

45 continue

46

47 if child.type == "if_statement":

48 total += 1 + nesting

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

50 continue

51

52 if child.type in _TS_NESTING_INCREMENTORS:

53 total += 1 + nesting

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

55 continue

56

57 if _is_logical_binary(child):

58 total += _score_boolean_chain(child)

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

60 continue

61

62 if child.type == "call_expression":

63 total += _check_recursion(child, func_name)

64

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

66

67 return total

68

69

70 def _walk_if_else(child, nesting, func_name, top_func):

71 """Handle an else_clause within an if_statement."""

72 inner_ifs = [c for c in child.children if c.type == "if_statement"]

73 if inner_ifs:

74 return 1 + _walk_if(inner_ifs[0], nesting, func_name, top_func)

75 return 1 + _walk(child, nesting + 1, func_name, top_func)

76

77

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

79 """Walk children of an if_statement."""

80 total = 0

81 for child in node.children:

82 if child.type == "else_clause":

83 total += _walk_if_else(child, nesting, func_name, top_func)

84 elif child.type == "statement_block":

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

86 elif _is_logical_binary(child):

87 total += _score_boolean_chain(child)

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

89 else:

90 if child.type == "call_expression":

91 total += _check_recursion(child, func_name)

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

93 return total

94

95

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

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

98 total = 0

99 for child in node.children:

100 if _is_logical_binary(child):

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

102 continue

103 if child.type == "ternary_expression":

104 total += 1 + nesting

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

106 continue

107 if child.type == "call_expression":

108 total += _check_recursion(child, func_name)

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

110 continue

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

112 return total

113

114

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

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

117 if not func_name:

118 return 0

119 for child in call_node.children:

120 if child.type == "identifier" and child.text.decode() == func_name:

121 return 1

122 if child.type == "arguments":

123 break

124 return 0

125

126

127 compute_cognitive_ts = make_compute_cognitive(_walk)