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

79 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 15:04 -0800

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

2 

3from __future__ import annotations 

4 

5from common import _is_logical_binary, _score_boolean_chain 

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

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

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

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

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

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

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

127compute_cognitive_ts = make_compute_cognitive(_walk)