@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.
- package/CHANGELOG.md +122 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/assets/prettify.css +1 -0
- package/assets/prettify.js +2 -0
- package/assets/sort-arrow-sprite.png +0 -0
- package/complexity-breakdown.js +53 -0
- package/decision-points/ast-utils.js +127 -0
- package/decision-points/decision-type.js +92 -0
- package/decision-points/function-matching.js +185 -0
- package/decision-points/in-params.js +262 -0
- package/decision-points/index.js +6 -0
- package/decision-points/node-helpers.js +89 -0
- package/decision-points/parent-map.js +62 -0
- package/decision-points/parse-main.js +101 -0
- package/decision-points/ternary-multiline.js +86 -0
- package/export-generators/helpers.js +309 -0
- package/export-generators/index.js +143 -0
- package/export-generators/md-exports.js +160 -0
- package/export-generators/txt-exports.js +262 -0
- package/function-boundaries/arrow-brace-body.js +302 -0
- package/function-boundaries/arrow-helpers.js +93 -0
- package/function-boundaries/arrow-jsx.js +73 -0
- package/function-boundaries/arrow-object-literal.js +65 -0
- package/function-boundaries/arrow-single-expr.js +72 -0
- package/function-boundaries/brace-scanning.js +151 -0
- package/function-boundaries/index.js +67 -0
- package/function-boundaries/named-helpers.js +227 -0
- package/function-boundaries/parse-utils.js +456 -0
- package/function-extraction/ast-utils.js +112 -0
- package/function-extraction/extract-callback.js +65 -0
- package/function-extraction/extract-from-eslint.js +91 -0
- package/function-extraction/extract-name-ast.js +133 -0
- package/function-extraction/extract-name-regex.js +267 -0
- package/function-extraction/index.js +6 -0
- package/function-extraction/utils.js +29 -0
- package/function-hierarchy.js +427 -0
- package/html-generators/about.js +75 -0
- package/html-generators/file-boundary-builders.js +36 -0
- package/html-generators/file-breakdown.js +412 -0
- package/html-generators/file-data.js +50 -0
- package/html-generators/file-helpers.js +100 -0
- package/html-generators/file-javascript.js +430 -0
- package/html-generators/file-line-render.js +160 -0
- package/html-generators/file.css +370 -0
- package/html-generators/file.js +207 -0
- package/html-generators/folder.js +424 -0
- package/html-generators/index.js +6 -0
- package/html-generators/main-index.js +346 -0
- package/html-generators/shared.css +471 -0
- package/html-generators/utils.js +15 -0
- package/index.js +36 -0
- package/integration/eslint/index.js +94 -0
- package/integration/threshold/index.js +45 -0
- package/package.json +64 -0
- package/report/cli.js +58 -0
- 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"> </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
|
+
}
|