@pythonidaer/complexity-report 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/LICENSE +21 -0
  3. package/README.md +103 -0
  4. package/assets/prettify.css +1 -0
  5. package/assets/prettify.js +2 -0
  6. package/assets/sort-arrow-sprite.png +0 -0
  7. package/complexity-breakdown.js +53 -0
  8. package/decision-points/ast-utils.js +127 -0
  9. package/decision-points/decision-type.js +92 -0
  10. package/decision-points/function-matching.js +185 -0
  11. package/decision-points/in-params.js +262 -0
  12. package/decision-points/index.js +6 -0
  13. package/decision-points/node-helpers.js +89 -0
  14. package/decision-points/parent-map.js +62 -0
  15. package/decision-points/parse-main.js +101 -0
  16. package/decision-points/ternary-multiline.js +86 -0
  17. package/export-generators/helpers.js +309 -0
  18. package/export-generators/index.js +143 -0
  19. package/export-generators/md-exports.js +160 -0
  20. package/export-generators/txt-exports.js +262 -0
  21. package/function-boundaries/arrow-brace-body.js +302 -0
  22. package/function-boundaries/arrow-helpers.js +93 -0
  23. package/function-boundaries/arrow-jsx.js +73 -0
  24. package/function-boundaries/arrow-object-literal.js +65 -0
  25. package/function-boundaries/arrow-single-expr.js +72 -0
  26. package/function-boundaries/brace-scanning.js +151 -0
  27. package/function-boundaries/index.js +67 -0
  28. package/function-boundaries/named-helpers.js +227 -0
  29. package/function-boundaries/parse-utils.js +456 -0
  30. package/function-extraction/ast-utils.js +112 -0
  31. package/function-extraction/extract-callback.js +65 -0
  32. package/function-extraction/extract-from-eslint.js +91 -0
  33. package/function-extraction/extract-name-ast.js +133 -0
  34. package/function-extraction/extract-name-regex.js +267 -0
  35. package/function-extraction/index.js +6 -0
  36. package/function-extraction/utils.js +29 -0
  37. package/function-hierarchy.js +427 -0
  38. package/html-generators/about.js +75 -0
  39. package/html-generators/file-boundary-builders.js +36 -0
  40. package/html-generators/file-breakdown.js +412 -0
  41. package/html-generators/file-data.js +50 -0
  42. package/html-generators/file-helpers.js +100 -0
  43. package/html-generators/file-javascript.js +430 -0
  44. package/html-generators/file-line-render.js +160 -0
  45. package/html-generators/file.css +370 -0
  46. package/html-generators/file.js +207 -0
  47. package/html-generators/folder.js +424 -0
  48. package/html-generators/index.js +6 -0
  49. package/html-generators/main-index.js +346 -0
  50. package/html-generators/shared.css +471 -0
  51. package/html-generators/utils.js +15 -0
  52. package/index.js +36 -0
  53. package/integration/eslint/index.js +94 -0
  54. package/integration/threshold/index.js +45 -0
  55. package/package.json +64 -0
  56. package/report/cli.js +58 -0
  57. package/report/index.js +559 -0
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Generates JavaScript code for the file HTML page
3
+ * @param {boolean} initialShowAllColumns - Show all columns initially
4
+ * @param {Object} columnStructure - Column structure configuration
5
+ * @param {Set} emptyColumns - Set of empty column keys
6
+ * @param {Object} lineToSpan - Map of line numbers to span objects
7
+ * @param {boolean} [initialShowLines=true] - Show Line column initially
8
+ * @returns {string} JavaScript code
9
+ */
10
+ export function generateJavaScriptCode(
11
+ initialShowAllColumns,
12
+ columnStructure,
13
+ emptyColumns,
14
+ lineToSpan,
15
+ initialShowLines = true
16
+ ) {
17
+ return `
18
+ let showAllColumns = ${initialShowAllColumns};
19
+ let showLines = ${initialShowLines};
20
+ let sortColumn = 'complexity';
21
+ let sortDirection = 'desc';
22
+ const columnConfig = ${JSON.stringify({
23
+ groups: columnStructure.groups.map(group => ({
24
+ name: group.name,
25
+ columns: group.columns,
26
+ totalColumns: group.columns.length
27
+ })),
28
+ emptyColumns: Array.from(emptyColumns),
29
+ baseColumn: columnStructure.baseColumn
30
+ })};
31
+ const COVERAGE_LINE_TO_SPAN = ${JSON.stringify(lineToSpan)};
32
+
33
+ (function initHoverVerticalLine() {
34
+ return;
35
+ const table = document.getElementById('coverage-table');
36
+ const wrapper = table && table.closest('.coverage-table-wrapper');
37
+ const lineEl = document.getElementById('hover-vertical-line');
38
+ if (!table || !wrapper || !lineEl) return;
39
+ const pre = table.querySelector('pre.prettyprint');
40
+ let chWidth = 0;
41
+ if (pre) {
42
+ const s = document.createElement('span');
43
+ s.style.cssText = 'position:absolute;visibility:hidden;white-space:pre;font:' + getComputedStyle(pre).font + ';';
44
+ s.textContent = '0';
45
+ document.body.appendChild(s);
46
+ chWidth = s.offsetWidth;
47
+ document.body.removeChild(s);
48
+ }
49
+ function onEnter(ev) {
50
+ const td = ev.target.closest('td');
51
+ if (!td || (!td.classList.contains('line-count') && !td.classList.contains('line-coverage'))) return;
52
+ const tr = td.closest('tr');
53
+ const line = tr && tr.getAttribute('data-line');
54
+ if (!line) return;
55
+ const span = COVERAGE_LINE_TO_SPAN[line];
56
+ if (!span) return;
57
+ const first = table.querySelector(\`tr[data-line="\${span.start}"]\`);
58
+ const last = table.querySelector(\`tr[data-line="\${span.end}"]\`);
59
+ if (!first || !last) return;
60
+ const codeCell = first.querySelector('td.text');
61
+ if (!codeCell) return;
62
+ const wr = wrapper.getBoundingClientRect();
63
+ const fr = first.getBoundingClientRect();
64
+ const lr = last.getBoundingClientRect();
65
+ const cr = codeCell.getBoundingClientRect();
66
+ const top = fr.top - wr.top;
67
+ const height = lr.bottom - fr.top;
68
+ const indent = (span.indent != null ? span.indent : 0) * chWidth;
69
+ const left = (cr.left - wr.left) + indent;
70
+ lineEl.style.top = top + 'px';
71
+ lineEl.style.height = height + 'px';
72
+ lineEl.style.left = left + 'px';
73
+ lineEl.classList.add('visible');
74
+ }
75
+ function onLeave(ev) {
76
+ const td = ev.target.closest('td');
77
+ if (!td || (!td.classList.contains('line-count') && !td.classList.contains('line-coverage'))) return;
78
+ const next = ev.relatedTarget;
79
+ if (next && table.contains(next)) {
80
+ const nextTd = next.closest && next.closest('td');
81
+ if (nextTd && (nextTd.classList.contains('line-count') || nextTd.classList.contains('line-coverage'))) return;
82
+ }
83
+ lineEl.classList.remove('visible');
84
+ lineEl.style.left = '';
85
+ lineEl.style.top = '';
86
+ lineEl.style.height = '';
87
+ }
88
+ table.addEventListener('mouseover', onEnter);
89
+ table.addEventListener('mouseout', onLeave);
90
+ })();
91
+
92
+ function updateSortIndicators() {
93
+ // Reset all indicators
94
+ const allIndicators = document.querySelectorAll('.sort-indicator');
95
+ allIndicators.forEach(indicator => {
96
+ indicator.className = 'sort-indicator';
97
+ });
98
+
99
+ // Set active indicator
100
+ if (sortColumn) {
101
+ const indicator = document.getElementById(\`sort-\${sortColumn}-indicator\`);
102
+ if (indicator) {
103
+ indicator.className = \`sort-indicator sort-\${sortDirection}\`;
104
+ }
105
+ }
106
+ }
107
+
108
+ function sortTable(column) {
109
+ const tbody = document.getElementById('complexity-breakdown-tbody');
110
+ const rows = Array.from(tbody.querySelectorAll('tr'));
111
+ if (sortColumn === column) {
112
+ sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
113
+ } else {
114
+ sortColumn = column;
115
+ // Default to desc for complexity/line, asc for function
116
+ sortDirection = (column === 'complexity' || column === 'line') ? 'desc' : 'asc';
117
+ }
118
+ rows.sort((a, b) => {
119
+ let aValue, bValue;
120
+ if (column === 'function') {
121
+ const aCell = a.querySelector('.function-name');
122
+ const bCell = b.querySelector('.function-name');
123
+ aValue = aCell ? aCell.textContent.trim().toLowerCase() : '';
124
+ bValue = bCell ? bCell.textContent.trim().toLowerCase() : '';
125
+ } else if (column === 'line') {
126
+ const aLine = a.getAttribute('data-line');
127
+ const bLine = b.getAttribute('data-line');
128
+ aValue = aLine ? parseInt(aLine, 10) || 0 : 0;
129
+ bValue = bLine ? parseInt(bLine, 10) || 0 : 0;
130
+ } else if (column === 'complexity') {
131
+ const aCell = a.querySelector('.complexity-value');
132
+ const bCell = b.querySelector('.complexity-value');
133
+ aValue = aCell ? parseInt(aCell.textContent.trim(), 10) || 0 : 0;
134
+ bValue = bCell ? parseInt(bCell.textContent.trim(), 10) || 0 : 0;
135
+ } else {
136
+ // Breakdown column (if, for, ternary, &&, ||, etc.)
137
+ const aCell = a.querySelector(\`td[data-column-key="\${column}"]\`);
138
+ const bCell = b.querySelector(\`td[data-column-key="\${column}"]\`);
139
+ const aText = aCell ? aCell.textContent.trim() : '-';
140
+ const bText = bCell ? bCell.textContent.trim() : '-';
141
+ aValue = aText === '-' ? 0 : parseInt(aText, 10) || 0;
142
+ bValue = bText === '-' ? 0 : parseInt(bText, 10) || 0;
143
+ }
144
+ if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1;
145
+ if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1;
146
+ return 0;
147
+ });
148
+ rows.forEach(row => tbody.appendChild(row));
149
+ updateSortIndicators();
150
+ }
151
+
152
+ function getTotalBreakdownCols(visibleGroups) {
153
+ return visibleGroups.reduce((sum, group) => sum + group.columns.length, 0);
154
+ }
155
+
156
+ function updateLineColumnVisibility() {
157
+ const table = document.getElementById('complexity-breakdown-table');
158
+ const headerSpan = document.getElementById('breakdown-header-span');
159
+ const noMatchesRow = document.getElementById('no-matches-row');
160
+ const noMatchesCell = noMatchesRow && noMatchesRow.querySelector('.no-matches-message');
161
+ if (!table || !headerSpan) return;
162
+ const visibleGroups = columnConfig.groups.map(group => {
163
+ const visibleColumns = showAllColumns
164
+ ? group.columns
165
+ : group.columns.filter(col => !columnConfig.emptyColumns.includes(col.key));
166
+ return { name: group.name, columns: visibleColumns, totalColumns: group.columns.length };
167
+ });
168
+ const totalBreakdownCols = getTotalBreakdownCols(visibleGroups);
169
+ const colspan = showLines ? (3 + totalBreakdownCols) : (2 + totalBreakdownCols);
170
+ headerSpan.setAttribute('colspan', colspan);
171
+ if (noMatchesCell) noMatchesCell.setAttribute('colspan', colspan);
172
+ table.classList.toggle('hide-lines', !showLines);
173
+ }
174
+
175
+ function toggleLineColumn() {
176
+ const checkbox = document.getElementById('breakdown-show-lines');
177
+ showLines = checkbox ? checkbox.checked : true;
178
+ updateLineColumnVisibility();
179
+ }
180
+
181
+ function toggleHighlights() {
182
+ const checkbox = document.getElementById('breakdown-show-highlights');
183
+ const wrapper = document.querySelector('.coverage-table-wrapper');
184
+ if (wrapper && checkbox) {
185
+ wrapper.classList.toggle('hide-highlights', !checkbox.checked);
186
+ }
187
+ }
188
+
189
+ function rebuildTableHeaders(showAll) {
190
+ const headerSpan = document.getElementById('breakdown-header-span');
191
+ const groupHeadersRow = document.getElementById('breakdown-group-headers-row');
192
+ const colHeadersRow = document.getElementById('breakdown-col-headers-row');
193
+ const visibleGroups = columnConfig.groups.map(group => {
194
+ const visibleColumns = showAll
195
+ ? group.columns
196
+ : group.columns.filter(col => !columnConfig.emptyColumns.includes(col.key));
197
+ return {
198
+ name: group.name,
199
+ columns: visibleColumns,
200
+ totalColumns: group.columns.length
201
+ };
202
+ });
203
+ const totalBreakdownCols = getTotalBreakdownCols(visibleGroups);
204
+ const colspan = showLines ? (3 + totalBreakdownCols) : (2 + totalBreakdownCols);
205
+ headerSpan.setAttribute('colspan', colspan);
206
+ const groupHeadersHTML = visibleGroups.map(group => {
207
+ if (group.columns.length === 0) return '';
208
+ return \`<th colspan="\${group.columns.length}" class="breakdown-group-header" data-group="\${group.name}">\${group.name}</th>\`;
209
+ }).filter(Boolean).join('');
210
+ const lineTh = \`<th rowspan="2" class="breakdown-line-header breakdown-line-column sortable" onclick="sortTable('line')" id="sort-line-header">Line <span class="sort-indicator" id="sort-line-indicator"></span></th>\`;
211
+ groupHeadersRow.innerHTML = \`<th rowspan="2" class="breakdown-function-header sortable" onclick="sortTable('function')" id="sort-function-header">Function (base = 1) <span class="sort-indicator" id="sort-function-indicator"></span></th>\${lineTh}<th rowspan="2" class="breakdown-complexity-header sortable" onclick="sortTable('complexity')" id="sort-complexity-header">Complexity <span class="sort-indicator" id="sort-complexity-indicator"></span></th>\${groupHeadersHTML}\`;
212
+ updateSortIndicators();
213
+ const colHeadersHTML = visibleGroups.map(group =>
214
+ group.columns.map(col => \`<th class="breakdown-col-header sortable" data-column-key="\${col.key}" onclick="sortTable('\${col.key}')" id="sort-\${col.key}-header">\${col.label} <span class="sort-indicator" id="sort-\${col.key}-indicator"></span></th>\`).join('')
215
+ ).join('');
216
+ colHeadersRow.innerHTML = colHeadersHTML;
217
+ }
218
+
219
+ function rebuildTableBody(showAll) {
220
+ const tbody = document.getElementById('complexity-breakdown-tbody');
221
+ const rows = tbody.querySelectorAll('tr');
222
+ const visibleColumns = columnConfig.groups.flatMap(group => {
223
+ if (showAll) {
224
+ return group.columns;
225
+ }
226
+ return group.columns.filter(col => !columnConfig.emptyColumns.includes(col.key));
227
+ });
228
+ rows.forEach(row => {
229
+ if (row.id === 'no-matches-row') {
230
+ return;
231
+ }
232
+ const cells = row.querySelectorAll('td');
233
+ const functionCell = cells[0];
234
+ const lineCell = cells[1];
235
+ const complexityCell = cells[2];
236
+ if (!functionCell || !complexityCell) {
237
+ return;
238
+ }
239
+ const breakdownData = {};
240
+ const existingBreakdownCells = Array.from(cells).slice(3);
241
+ existingBreakdownCells.forEach(cell => {
242
+ const key = cell.getAttribute('data-column-key');
243
+ if (key) {
244
+ const text = cell.textContent.trim();
245
+ breakdownData[key] = text === '-' ? 0 : parseInt(text, 10) || 0;
246
+ }
247
+ });
248
+ const breakdownCells = visibleColumns.map(col => {
249
+ const value = breakdownData[col.key] || 0;
250
+ const displayValue = value === 0 ? '-' : value;
251
+ const emptyClass = value === 0 ? ' breakdown-value-empty' : '';
252
+ return \`<td class="breakdown-value\${emptyClass}" data-column-key="\${col.key}">\${displayValue}</td>\`;
253
+ });
254
+ const lineHTML = lineCell ? lineCell.outerHTML : '';
255
+ row.innerHTML = \`\${functionCell.outerHTML}\${lineHTML}\${complexityCell.outerHTML}\${breakdownCells.join('')}\`;
256
+ });
257
+ }
258
+
259
+ function toggleEmptyColumns() {
260
+ const checkbox = document.getElementById('breakdown-show-all-columns');
261
+ showAllColumns = checkbox ? checkbox.checked : false;
262
+ rebuildTableHeaders(showAllColumns);
263
+ rebuildTableBody(showAllColumns);
264
+ if (sortColumn) {
265
+ sortTable(sortColumn);
266
+ }
267
+ const searchInput = document.getElementById('breakdown-search');
268
+ if (searchInput && searchInput.value) {
269
+ filterFunctions(searchInput.value);
270
+ }
271
+ }
272
+
273
+ function filterFunctions(searchTerm) {
274
+ const tbody = document.getElementById('complexity-breakdown-tbody');
275
+ const rows = tbody.querySelectorAll('tr:not(#no-matches-row)');
276
+ const noMatchesRow = document.getElementById('no-matches-row');
277
+ const searchLower = searchTerm.toLowerCase().trim();
278
+ let visibleCount = 0;
279
+ rows.forEach(row => {
280
+ const functionCell = row.querySelector('.function-name');
281
+ if (functionCell) {
282
+ const functionName = functionCell.textContent.trim().toLowerCase();
283
+ const matches = searchLower === '' || functionName.includes(searchLower);
284
+ row.style.display = matches ? '' : 'none';
285
+ if (matches) visibleCount += 1;
286
+ }
287
+ });
288
+ if (noMatchesRow) {
289
+ if (searchLower !== '' && visibleCount === 0) {
290
+ noMatchesRow.style.display = '';
291
+ const headerSpan = document.getElementById('breakdown-header-span');
292
+ const currentColspan = headerSpan ? parseInt(headerSpan.getAttribute('colspan'), 10) || 0 : 0;
293
+ const noMatchesCell = noMatchesRow.querySelector('.no-matches-message');
294
+ if (noMatchesCell) {
295
+ noMatchesCell.setAttribute('colspan', 2 + currentColspan);
296
+ }
297
+ } else {
298
+ noMatchesRow.style.display = 'none';
299
+ }
300
+ }
301
+ }
302
+
303
+ function toggleTableVisibility() {
304
+ const checkbox = document.getElementById('breakdown-show-table');
305
+ const table = document.getElementById('complexity-breakdown-table');
306
+ if (table && checkbox) {
307
+ table.style.display = checkbox.checked ? 'table' : 'none';
308
+ }
309
+ }
310
+
311
+ function clearFunctionRangeHighlight() {
312
+ const codeTable = document.getElementById('coverage-table');
313
+ const fcbTbody = document.getElementById('complexity-breakdown-tbody');
314
+ if (codeTable) {
315
+ codeTable.querySelectorAll('tr.function-range-highlight').forEach(tr => tr.classList.remove('function-range-highlight'));
316
+ }
317
+ if (fcbTbody) {
318
+ fcbTbody.querySelectorAll('tr.breakdown-row-selected').forEach(tr => tr.classList.remove('breakdown-row-selected'));
319
+ }
320
+ }
321
+
322
+ function selectFunctionInCode(tr) {
323
+ const start = parseInt(tr.getAttribute('data-function-start'), 10);
324
+ const end = parseInt(tr.getAttribute('data-function-end'), 10);
325
+ if (isNaN(start) || isNaN(end)) return;
326
+ clearFunctionRangeHighlight();
327
+ tr.classList.add('breakdown-row-selected');
328
+ const codeTable = document.getElementById('coverage-table');
329
+ if (!codeTable) return;
330
+ let firstRow = null;
331
+ codeTable.querySelectorAll('tr').forEach(row => {
332
+ const line = parseInt(row.getAttribute('data-line'), 10);
333
+ if (!isNaN(line) && line >= start && line <= end) {
334
+ row.classList.add('function-range-highlight');
335
+ if (!firstRow) firstRow = row;
336
+ }
337
+ });
338
+ if (firstRow) {
339
+ firstRow.scrollIntoView({ behavior: 'smooth', block: 'start' });
340
+ }
341
+ }
342
+
343
+ function initBreakdownRowClick() {
344
+ const tbody = document.getElementById('complexity-breakdown-tbody');
345
+ if (!tbody) return;
346
+ tbody.addEventListener('click', function(ev) {
347
+ const tr = ev.target.closest('tr.breakdown-function-row');
348
+ if (!tr || tr.id === 'no-matches-row') return;
349
+ const start = tr.getAttribute('data-function-start');
350
+ const end = tr.getAttribute('data-function-end');
351
+ if (!start || !end) return;
352
+ if (tr.classList.contains('breakdown-row-selected')) {
353
+ clearFunctionRangeHighlight();
354
+ } else {
355
+ selectFunctionInCode(tr);
356
+ }
357
+ });
358
+ tbody.addEventListener('keydown', function(ev) {
359
+ if (ev.key !== 'Enter' && ev.key !== ' ') return;
360
+ const tr = ev.target.closest('tr.breakdown-function-row');
361
+ if (!tr || tr.id === 'no-matches-row') return;
362
+ ev.preventDefault();
363
+ if (tr.classList.contains('breakdown-row-selected')) {
364
+ clearFunctionRangeHighlight();
365
+ } else {
366
+ selectFunctionInCode(tr);
367
+ }
368
+ });
369
+ }
370
+ initBreakdownRowClick();
371
+
372
+ function initCodeAreaClearClick() {
373
+ const codeTable = document.getElementById('coverage-table');
374
+ if (!codeTable) return;
375
+ codeTable.addEventListener('click', function(ev) {
376
+ const tr = ev.target.closest('tr');
377
+ if (tr && tr.classList.contains('function-range-highlight')) {
378
+ clearFunctionRangeHighlight();
379
+ }
380
+ });
381
+ }
382
+ initCodeAreaClearClick();
383
+
384
+ // Initialize with complexity sorted descending
385
+ // Apply initial sort after DOM is ready
386
+ setTimeout(function() {
387
+ const tbody = document.getElementById('complexity-breakdown-tbody');
388
+ if (tbody) {
389
+ const rows = Array.from(tbody.querySelectorAll('tr'));
390
+ rows.sort((a, b) => {
391
+ const aCell = a.querySelector('.complexity-value');
392
+ const bCell = b.querySelector('.complexity-value');
393
+ const aValue = aCell ? parseInt(aCell.textContent.trim(), 10) || 0 : 0;
394
+ const bValue = bCell ? parseInt(bCell.textContent.trim(), 10) || 0 : 0;
395
+ if (aValue < bValue) return 1; // desc: higher values first
396
+ if (aValue > bValue) return -1;
397
+ return 0;
398
+ });
399
+ rows.forEach(row => tbody.appendChild(row));
400
+ }
401
+ updateSortIndicators();
402
+ }, 0);
403
+
404
+ // Initialize syntax highlighting with prettify
405
+ if (typeof prettyPrint !== 'undefined') {
406
+ // Use setTimeout to ensure DOM is fully ready
407
+ setTimeout(function() {
408
+ prettyPrint();
409
+ }, 0);
410
+ }
411
+
412
+ (function initScrollToTop() {
413
+ const btn = document.getElementById('scroll-to-top');
414
+ if (!btn) return;
415
+ const scrollThreshold = 300;
416
+ function updateVisibility() {
417
+ if (window.scrollY > scrollThreshold) {
418
+ btn.classList.add('scroll-to-top-visible');
419
+ } else {
420
+ btn.classList.remove('scroll-to-top-visible');
421
+ }
422
+ }
423
+ window.addEventListener('scroll', updateVisibility, { passive: true });
424
+ updateVisibility();
425
+ btn.addEventListener('click', function() {
426
+ window.scrollTo({ top: 0, behavior: 'smooth' });
427
+ });
428
+ })();
429
+ `;
430
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Line rendering logic for file page (code line HTML, line row HTML)
3
+ */
4
+
5
+ /**
6
+ * Generates complexity annotation HTML for a function
7
+ */
8
+ export function generateComplexityAnnotation(func, getComplexityLevel, escapeHtml) {
9
+ if (!func) return '<span class="cline-any cline-neutral">&nbsp;</span>';
10
+ const complexityNum = parseInt(func.complexity, 10);
11
+ getComplexityLevel(func.complexity);
12
+ return `<span class="cline-any cline-yes" title="Function '${escapeHtml(func.functionName)}' has complexity ${complexityNum}">${complexityNum}</span>`;
13
+ }
14
+
15
+ /**
16
+ * Determines CSS classes for a line based on decision points
17
+ */
18
+ export function determineLineClasses(
19
+ decisionPointsOnLine
20
+ ) {
21
+ const isDecisionPoint = decisionPointsOnLine.length > 0;
22
+ const decisionPointClass = isDecisionPoint ? 'decision-point' : '';
23
+ const allClasses = decisionPointClass ? ` class="${decisionPointClass}"` : '';
24
+ return { classAttr: allClasses, isDecisionPoint };
25
+ }
26
+
27
+ /**
28
+ * Gets range for a single decision point using ONLY AST-provided columns.
29
+ * No regex, no keyword search - only what the AST tells us.
30
+ */
31
+ function getRangeForDecisionPoint(dp, line, lineLength) {
32
+ // Use only AST-provided column information
33
+ const hasValidColumns = typeof dp.column === 'number' &&
34
+ typeof dp.endColumn === 'number' &&
35
+ dp.endColumn > dp.column &&
36
+ dp.column >= 0 &&
37
+ dp.column < lineLength &&
38
+ dp.endColumn <= lineLength;
39
+
40
+ if (hasValidColumns) {
41
+ return {
42
+ start: dp.column,
43
+ end: dp.endColumn
44
+ };
45
+ }
46
+
47
+ // No fallback - if AST doesn't provide valid columns, don't highlight
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Builds sorted, non-overlapping ranges from decision points with
53
+ * column/endColumn (0-based from AST)
54
+ */
55
+ export function getDecisionPointRanges(decisionPointsOnLine, line) {
56
+ const lineLength = line.length;
57
+ const ranges = [];
58
+
59
+ for (const dp of decisionPointsOnLine) {
60
+ const range = getRangeForDecisionPoint(dp, line, lineLength);
61
+ if (range) {
62
+ ranges.push({ ...range, type: 'decision-point' });
63
+ }
64
+ }
65
+
66
+ if (ranges.length === 0) return [];
67
+
68
+ // Sort and merge overlapping ranges
69
+ ranges.sort((a, b) => a.start - b.start);
70
+ const merged = [ranges[0]];
71
+ for (let i = 1; i < ranges.length; i += 1) {
72
+ const last = merged[merged.length - 1];
73
+ if (ranges[i].start <= last.end) {
74
+ last.end = Math.max(last.end, ranges[i].end);
75
+ } else {
76
+ merged.push(ranges[i]);
77
+ }
78
+ }
79
+ return merged;
80
+ }
81
+
82
+ /**
83
+ * Builds HTML for a code line with precise AST-based highlighting
84
+ * Only highlights decision points
85
+ */
86
+ export function buildCodeLineHTML(
87
+ line,
88
+ escapeHtml,
89
+ decisionPointsOnLine
90
+ ) {
91
+ // Get decision point ranges from AST data
92
+ const decisionPointRanges = getDecisionPointRanges(decisionPointsOnLine, line);
93
+
94
+ // If no highlights, return plain text
95
+ if (decisionPointRanges.length === 0) {
96
+ return `<span class="code-line">${escapeHtml(line)}</span>`;
97
+ }
98
+
99
+ // Build HTML with highlighted segments
100
+ const lineLength = line.length;
101
+ const segments = [];
102
+ let pos = 0;
103
+
104
+ for (const range of decisionPointRanges) {
105
+ // Add unhighlighted gap before this range
106
+ if (range.start > pos) {
107
+ const gapText = line.substring(pos, range.start);
108
+ segments.push(`<span class="code-line">${escapeHtml(gapText)}</span>`);
109
+ }
110
+
111
+ // Add highlighted decision point range
112
+ const rangeText = line.substring(range.start, range.end);
113
+ segments.push(`<span class="code-line decision-point-line">${escapeHtml(rangeText)}</span>`);
114
+
115
+ pos = range.end;
116
+ }
117
+
118
+ // Add any remaining unhighlighted text
119
+ if (pos < lineLength) {
120
+ const remainingText = line.substring(pos, lineLength);
121
+ segments.push(`<span class="code-line">${escapeHtml(remainingText)}</span>`);
122
+ }
123
+
124
+ return segments.join('');
125
+ }
126
+
127
+ export function generateLineRowHTML(
128
+ line,
129
+ index,
130
+ lineToFunction,
131
+ lineToDecisionPoint,
132
+ getComplexityLevel,
133
+ escapeHtml,
134
+ languageClass = 'lang-js'
135
+ ) {
136
+ const lineNum = index + 1;
137
+ const func = lineToFunction.get(lineNum);
138
+ const decisionPointsOnLine = lineToDecisionPoint.get(lineNum) || [];
139
+ const complexityAnnotation = generateComplexityAnnotation(
140
+ func,
141
+ getComplexityLevel,
142
+ escapeHtml
143
+ );
144
+ const { classAttr } =
145
+ determineLineClasses(
146
+ decisionPointsOnLine
147
+ );
148
+
149
+ const codeLineHTML = buildCodeLineHTML(
150
+ line,
151
+ escapeHtml,
152
+ decisionPointsOnLine
153
+ );
154
+
155
+ return `<tr${classAttr} data-line="${lineNum}">
156
+ <td class="line-count quiet"><a name='L${lineNum}'></a><a href='#L${lineNum}'>${lineNum}</a></td>
157
+ <td class="line-coverage quiet">${complexityAnnotation}</td>
158
+ <td class="text"><pre class="prettyprint ${languageClass}">${codeLineHTML}</pre></td>
159
+ </tr>`;
160
+ }