Coverage for common.py: 91%
126 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"""Shared helpers for cognitive complexity analyzers (TypeScript & Go)."""
3from __future__ import annotations
5import math
6from collections import Counter
8from models import HalsteadMetrics
11def _halstead_derived(n1, n2, N1, N2, n, N):
12 """Compute derived Halstead measures from base counts."""
13 volume = N * math.log2(n)
14 difficulty = (n1 / 2) * (N2 / n2)
15 effort = difficulty * volume
16 return (
17 round(volume, 2),
18 round(difficulty, 2),
19 round(effort, 2),
20 round(effort / 18, 2),
21 round(effort ** (2 / 3) / 3000, 4),
22 )
25def compute_halstead(operators: list[str], operands: list[str]) -> HalsteadMetrics:
26 """Compute Halstead metrics from raw operator and operand lists."""
27 op_counts = Counter(operators)
28 od_counts = Counter(operands)
30 n1 = len(op_counts)
31 n2 = len(od_counts)
32 N1 = sum(op_counts.values())
33 N2 = sum(od_counts.values())
35 n = n1 + n2 # vocabulary
36 N = N1 + N2 # length
38 if n == 0 or n2 == 0:
39 return HalsteadMetrics(n1=n1, n2=n2, N1=N1, N2=N2, vocabulary=n, length=N)
41 volume, difficulty, effort, time, bugs = _halstead_derived(n1, n2, N1, N2, n, N)
42 return HalsteadMetrics(
43 n1=n1,
44 n2=n2,
45 N1=N1,
46 N2=N2,
47 vocabulary=n,
48 length=N,
49 volume=volume,
50 difficulty=difficulty,
51 effort=effort,
52 time=time,
53 bugs=bugs,
54 )
57# ---------------------------------------------------------------------------
58# Line-metric helpers (LOC / SLOC) -- shared across all languages
59# ---------------------------------------------------------------------------
62def _compute_loc(source: str) -> int:
63 """Count lines of code (non-blank lines)."""
64 return sum(1 for line in source.splitlines() if line.strip())
67def _collect_comment_nodes(node, acc: list) -> None:
68 """Recursively collect all comment nodes from the AST."""
69 if node.type == "comment":
70 acc.append(node)
71 for child in node.children:
72 _collect_comment_nodes(child, acc)
75def _compute_sloc(source: str, root_node) -> int:
76 """Count source lines of code (non-blank, non-comment-only lines)."""
77 lines = source.splitlines()
78 comment_only: set[int] = set()
80 comments: list = []
81 _collect_comment_nodes(root_node, comments)
83 for comment in comments:
84 start_line = comment.start_point[0]
85 end_line = comment.end_point[0]
86 start_col = comment.start_point[1]
87 end_col = comment.end_point[1]
89 for line_idx in range(start_line, end_line + 1):
90 if line_idx >= len(lines):
91 continue
92 line = lines[line_idx]
93 if start_line == end_line:
94 # Single-line comment -- comment-only if nothing else on the line
95 if not line[:start_col].strip() and not line[end_col:].strip():
96 comment_only.add(line_idx)
97 elif line_idx == start_line:
98 if not line[:start_col].strip():
99 comment_only.add(line_idx)
100 elif line_idx == end_line:
101 if not line[end_col:].strip():
102 comment_only.add(line_idx)
103 else:
104 # Interior line of a multi-line comment
105 comment_only.add(line_idx)
107 return sum(
108 1 for i, line in enumerate(lines) if line.strip() and i not in comment_only
109 )
112def _unwrap_parens(node):
113 """Unwrap parenthesized_expression nodes to get the inner expression."""
114 while node.type == "parenthesized_expression":
115 for child in node.children:
116 if child.type not in ("(", ")"):
117 node = child
118 break
119 else:
120 break
121 return node
124def _is_logical_binary(node) -> bool:
125 """Check if a node (possibly parenthesized) is a binary_expression with && or ||."""
126 node = _unwrap_parens(node)
127 if node.type != "binary_expression":
128 return False
129 for child in node.children:
130 if child.type in ("&&", "||"):
131 return True
132 return False
135def _flatten_boolean_ops(node, ops: list, right_subtrees: list) -> None:
136 """Flatten a left-associative binary_expression tree of logical operators."""
137 node = _unwrap_parens(node)
138 if not _is_logical_binary(node):
139 return
140 children = node.children
141 if len(children) >= 3:
142 left = children[0]
143 operator = children[1]
144 right = children[2]
145 if _is_logical_binary(left):
146 _flatten_boolean_ops(left, ops, right_subtrees)
147 ops.append(operator.type)
148 if _is_logical_binary(right):
149 right_subtrees.append(_unwrap_parens(right))
152def _count_bool_ops(node) -> int:
153 """Count logical binary operators (&& / ||) in an expression subtree."""
154 unwrapped = _unwrap_parens(node)
155 if unwrapped.type == "binary_expression":
156 is_logical = False
157 for child in unwrapped.children:
158 if child.type in ("&&", "||"):
159 is_logical = True
160 break
161 count = 1 if is_logical else 0
162 # Recurse into the unwrapped node's children to avoid double-counting
163 for child in unwrapped.children:
164 if child.type not in ("&&", "||"):
165 count += _count_bool_ops(child)
166 return count
167 # For non-binary nodes, recurse normally
168 count = 0
169 for child in unwrapped.children:
170 count += _count_bool_ops(child)
171 return count
174def compute_maintainability_index(
175 halstead_volume: float, cyclomatic: int, loc: int
176) -> float:
177 """Compute the VSCode Maintainability Index for a single function.
179 MI = max(0, (171 - 5.2*ln(HV) - 0.23*CC - 16.2*ln(LOC)) * 100 / 171)
180 Scale: 0-100, higher = more maintainable.
181 """
182 if halstead_volume <= 0 or loc <= 0:
183 return 100.0
184 mi = (
185 171 - 5.2 * math.log(halstead_volume) - 0.23 * cyclomatic - 16.2 * math.log(loc)
186 )
187 return round(max(0.0, mi * 100 / 171), 2)
190def compute_ldi(volume: float, vocabulary: int, n2: int, difficulty: float) -> float:
191 """Compute the Logic Density Index for a single function.
193 LDI = min(100, (vocabulary/100 * 40) + (difficulty/60 * 40) + (n2/volume * 200))
194 Scale: 0-100, higher = more complex logic.
195 """
196 if volume <= 0:
197 return 0.0
198 ldi = (vocabulary / 100 * 40) + (difficulty / 60 * 40) + (n2 / volume * 200)
199 return round(min(100.0, ldi), 2)
202def _score_boolean_chain(node, flatten_fn=None) -> int:
203 """Score a boolean chain in binary_expression nodes using && and ||.
205 Rules:
206 - +1 for the first boolean operator in a sequence
207 - +1 for each switch between operator types (&& -> ||, || -> &&)
208 - Same consecutive operators don't add extra cost
210 If *flatten_fn* is provided it replaces the default `_flatten_boolean_ops`.
211 """
212 if flatten_fn is None:
213 flatten_fn = _flatten_boolean_ops
214 ops = []
215 right_subtrees = []
216 flatten_fn(node, ops, right_subtrees)
218 score = 0
219 if ops:
220 score = 1
221 for i in range(1, len(ops)):
222 if ops[i] != ops[i - 1]:
223 score += 1
225 for subtree in right_subtrees:
226 score += _score_boolean_chain(subtree, flatten_fn=flatten_fn)
228 return score