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
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
1"""Cognitive complexity for TypeScript (SonarSource spec)."""
3from __future__ import annotations
5from common import _is_logical_binary, _score_boolean_chain
6from metrics.cognitive.common import make_compute_cognitive
8_NESTING_CONTAINERS = {
9 "function_declaration",
10 "function_expression",
11 "method_definition",
12 "arrow_function",
13 "generator_function_declaration",
14}
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}
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
37def _walk(node, nesting: int, func_name: str, top_func) -> int:
38 """Recursively walk AST and compute cognitive complexity."""
39 total = 0
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
47 if child.type == "if_statement":
48 total += 1 + nesting
49 total += _walk_if(child, nesting, func_name, top_func)
50 continue
52 if child.type in _TS_NESTING_INCREMENTORS:
53 total += 1 + nesting
54 total += _walk(child, nesting + 1, func_name, top_func)
55 continue
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
62 if child.type == "call_expression":
63 total += _check_recursion(child, func_name)
65 total += _walk(child, nesting, func_name, top_func)
67 return total
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)
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
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
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
127compute_cognitive_ts = make_compute_cognitive(_walk)