@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,424 @@
1
+ /**
2
+ * Groups functions by base name and file, keeping the highest complexity version
3
+ * @param {Array} functions - Array of function objects
4
+ * @param {Function} getBaseFunctionName - Function to get base function name
5
+ * @returns {Array} Array of function objects with highest complexity per group
6
+ */
7
+ function groupFunctionsByBaseName(functions, getBaseFunctionName) {
8
+ const functionGroups = new Map();
9
+ functions.forEach(issue => {
10
+ const baseName = getBaseFunctionName(issue.functionName || 'unknown');
11
+ const key = `${issue.file}:${baseName}`;
12
+ const complexityNum = parseInt(issue.complexity, 10);
13
+
14
+ const existing = functionGroups.get(key);
15
+ if (!existing || complexityNum > parseInt(existing.complexity, 10)) {
16
+ functionGroups.set(key, issue);
17
+ }
18
+ });
19
+
20
+ return Array.from(functionGroups.values())
21
+ .sort((a, b) => parseInt(b.complexity, 10) - parseInt(a.complexity, 10));
22
+ }
23
+
24
+ /**
25
+ * Generates HTML row for a function
26
+ * @param {Object} issue - Function object
27
+ * @param {Function} getComplexityLevel - Function to get complexity level
28
+ * @param {Function} getBaseFunctionName - Function to get base function name
29
+ * @param {boolean} showAllInitially - Show all functions initially
30
+ * @returns {string} HTML row string
31
+ */
32
+ function generateFunctionRow(
33
+ issue,
34
+ getComplexityLevel,
35
+ getBaseFunctionName,
36
+ showAllInitially,
37
+ complexityThreshold = 10
38
+ ) {
39
+ const level = getComplexityLevel(issue.complexity);
40
+ const complexityNum = parseInt(issue.complexity, 10);
41
+ const isOverThreshold = complexityNum > complexityThreshold;
42
+ const maxComplexityForBar = Math.max(30, complexityNum);
43
+ const percentage = Math.min(100, (complexityNum / maxComplexityForBar) * 100);
44
+ const fileName = issue.file.split('/').pop();
45
+ const fileLinkPath = `${fileName}.html`;
46
+ const baseFunctionName = getBaseFunctionName(issue.functionName || 'unknown');
47
+
48
+ return `
49
+ <tr class="${level}"
50
+ data-over-threshold="${isOverThreshold}"
51
+ data-file="${issue.file}"
52
+ data-function="${baseFunctionName}"
53
+ data-complexity="${complexityNum}"
54
+ data-line="${issue.line}"
55
+ ${!showAllInitially && !isOverThreshold ? 'style="display: none;"' : ''}>
56
+ <td class="file"><a href="${fileLinkPath}">${issue.file}</a></td>
57
+ <td class="bar ${level}">
58
+ <div class="chart">
59
+ <div class="cover-fill ${level} ${percentage === 100 ? 'cover-full' : ''}"
60
+ style="width: ${percentage}%"></div>
61
+ <div class="cover-empty" style="width: ${100 - percentage}%"></div>
62
+ </div>
63
+ </td>
64
+ <td class="file">
65
+ <span style="font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 13px;">
66
+ ${baseFunctionName}
67
+ </span>
68
+ </td>
69
+ <td class="pct">
70
+ <span class="complexity-value ${level}">${complexityNum}</span>
71
+ </td>
72
+ <td class="abs">${issue.line}</td>
73
+ </tr>
74
+ `;
75
+ }
76
+
77
+ /**
78
+ * Generates the JavaScript code for folder page functionality
79
+ * @returns {string} JavaScript code as string
80
+ */
81
+ function generateFolderPageScript() {
82
+ return `(function() {
83
+ function initFilters() {
84
+ // Checkbox filter
85
+ const checkbox = document.getElementById('showAllFunctions');
86
+ const fileSearchInput = document.getElementById('fileSearch');
87
+ const table = document.querySelector('.coverage-summary.function-complexity-table');
88
+
89
+ if (!table) {
90
+ console.warn('Complexity table not found');
91
+ return;
92
+ }
93
+
94
+ const tbody = table.querySelector('tbody');
95
+ if (!tbody) {
96
+ console.warn('Table tbody not found');
97
+ return;
98
+ }
99
+
100
+ function applyFilters() {
101
+ const rows = Array.from(tbody.querySelectorAll('tr'));
102
+ const showAll = checkbox ? checkbox.checked : true;
103
+ const searchValue = fileSearchInput ? fileSearchInput.value : '';
104
+
105
+ // Try to create a RegExp from the searchValue
106
+ let searchRegex;
107
+ try {
108
+ searchRegex = searchValue ? new RegExp(searchValue, 'i') : null;
109
+ } catch (error) {
110
+ searchRegex = null;
111
+ }
112
+
113
+ rows.forEach(row => {
114
+ // Check if row matches search filter
115
+ let matchesSearch = true;
116
+ if (searchValue) {
117
+ if (searchRegex) {
118
+ matchesSearch = searchRegex.test(row.textContent);
119
+ } else {
120
+ matchesSearch = row.textContent.toLowerCase().includes(searchValue.toLowerCase());
121
+ }
122
+ }
123
+
124
+ // Check if row matches checkbox filter
125
+ const isOverThreshold = row.getAttribute('data-over-threshold') === 'true';
126
+ const matchesCheckbox = showAll || isOverThreshold;
127
+
128
+ // Show row only if it matches both filters
129
+ if (matchesSearch && matchesCheckbox) {
130
+ // Remove inline style attribute entirely if it only contains display:none
131
+ // Otherwise set display to empty string to show the row
132
+ if (row.getAttribute('style') === 'display: none;') {
133
+ row.removeAttribute('style');
134
+ } else {
135
+ row.style.display = '';
136
+ }
137
+ } else {
138
+ row.style.display = 'none';
139
+ }
140
+ });
141
+ }
142
+
143
+ if (checkbox) {
144
+ checkbox.addEventListener('change', applyFilters);
145
+ }
146
+
147
+ // Filter functionality
148
+ if (fileSearchInput) {
149
+ fileSearchInput.addEventListener('input', applyFilters);
150
+ }
151
+ }
152
+
153
+ // Initialize when DOM is ready
154
+ if (document.readyState === 'loading') {
155
+ document.addEventListener('DOMContentLoaded', initFilters);
156
+ } else {
157
+ initFilters();
158
+ }
159
+
160
+ // Sorting functionality
161
+ const headers = document.querySelectorAll('.coverage-summary th[data-sort]');
162
+ let currentSort = { column: null, direction: 'asc' };
163
+
164
+ function getSortValue(row, column) {
165
+ if (column === 'file') {
166
+ return row.getAttribute('data-file') || '';
167
+ }
168
+ if (column === 'function') {
169
+ return (row.getAttribute('data-function') || '').toLowerCase();
170
+ }
171
+ if (column === 'functions') {
172
+ const parts = (row.getAttribute('data-functions') || '0/0').split('/');
173
+ return parseInt(parts[1] || 1, 10);
174
+ }
175
+ if (column === 'complexity') {
176
+ return parseFloat(row.getAttribute('data-complexity') || 0);
177
+ }
178
+ if (column === 'line') {
179
+ return parseFloat(row.getAttribute('data-line') || 0);
180
+ }
181
+ return 0;
182
+ }
183
+
184
+ function compareSortValues(aVal, bVal, direction, column) {
185
+ if (column === 'functions') {
186
+ if (direction === 'desc') {
187
+ return bVal > aVal ? 1 : bVal < aVal ? -1 : 0;
188
+ }
189
+ return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
190
+ }
191
+
192
+ if (typeof aVal === 'string') {
193
+ const comparison = aVal.localeCompare(bVal);
194
+ return direction === 'asc' ? comparison : -comparison;
195
+ }
196
+
197
+ if (direction === 'asc') {
198
+ return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
199
+ }
200
+ return aVal < bVal ? 1 : aVal > bVal ? -1 : 0;
201
+ }
202
+
203
+ headers.forEach(header => {
204
+ header.addEventListener('click', function() {
205
+ const column = this.getAttribute('data-sort');
206
+ const tbody = this.closest('table').querySelector('tbody');
207
+ const rows = Array.from(tbody.querySelectorAll('tr'));
208
+
209
+ // Set sort column and direction
210
+ if (currentSort.column === column) {
211
+ currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
212
+ } else {
213
+ currentSort.column = column;
214
+ currentSort.direction = column === 'functions' ? 'desc' : 'asc';
215
+ }
216
+
217
+ // Update sort classes
218
+ headers.forEach(h => {
219
+ // Remove sorted classes from all headers
220
+ h.classList.remove('sorted', 'sorted-desc');
221
+ // Add appropriate class to the clicked header
222
+ if (h === this) {
223
+ if (currentSort.direction === 'asc') {
224
+ h.classList.add('sorted');
225
+ } else {
226
+ h.classList.add('sorted-desc');
227
+ }
228
+ }
229
+ });
230
+
231
+ // Sort rows
232
+ rows.sort((a, b) => {
233
+ const aVal = getSortValue(a, column);
234
+ const bVal = getSortValue(b, column);
235
+ return compareSortValues(aVal, bVal, currentSort.direction, column);
236
+ });
237
+
238
+ // Re-append sorted rows
239
+ tbody.innerHTML = '';
240
+ rows.forEach(row => tbody.appendChild(row));
241
+ });
242
+ });
243
+ })();`;
244
+ }
245
+
246
+ /**
247
+ * Generates summary section HTML (similar to main index)
248
+ * @param {Object} decisionPointTotals - Object with controlFlow, expressions,
249
+ * functionParameters totals
250
+ * @param {number} totalFunctions - Total number of functions
251
+ * @param {number} withinThreshold - Number of functions within threshold
252
+ * @param {number} withinThresholdPercentage - Percentage within threshold
253
+ * @returns {string} Summary section HTML
254
+ */
255
+ function generateSummarySection(
256
+ decisionPointTotals,
257
+ totalFunctions,
258
+ withinThreshold,
259
+ _withinThresholdPercentage
260
+ ) {
261
+ const { controlFlow, expressions, functionParameters } = decisionPointTotals;
262
+
263
+ // Helper function to format percentage (2 decimals if needed)
264
+ const formatPercentage = (numerator, denominator) => {
265
+ if (denominator === 0) return '0%';
266
+ const percentage = (numerator / denominator) * 100;
267
+ // Whole number: no decimals; otherwise 2 decimal places
268
+ return percentage % 1 === 0
269
+ ? `${percentage}%`
270
+ : `${percentage.toFixed(2)}%`;
271
+ };
272
+
273
+ // Functions: show bold % and fraction (can be < 100%)
274
+ // Control Flow / Expressions / Default Parameters: always 100% here
275
+ const functionsPercentage = formatPercentage(
276
+ withinThreshold,
277
+ totalFunctions
278
+ );
279
+
280
+ return `
281
+ <div class="clearfix">
282
+ <div class='fl pad1y space-right2'>
283
+ <span class="strong">${functionsPercentage}</span>
284
+ <span class="quiet">Functions</span>
285
+ <span class='fraction'>${withinThreshold}/${totalFunctions}</span>
286
+ </div>
287
+ <div class='fl pad1y space-right2'>
288
+ <span class="quiet">Control Flow</span>
289
+ <span class='fraction'>${controlFlow}</span>
290
+ </div>
291
+ <div class='fl pad1y space-right2'>
292
+ <span class="quiet">Expressions</span>
293
+ <span class='fraction'>${expressions}</span>
294
+ </div>
295
+ <div class='fl pad1y space-right2'>
296
+ <span class="quiet">Default Parameters</span>
297
+ <span class='fraction'>${functionParameters}</span>
298
+ </div>
299
+ </div>`;
300
+ }
301
+
302
+ /**
303
+ * Calculates status bar level based on percentage
304
+ * @param {number} percentageValue - Percentage value
305
+ * @returns {string} Level string
306
+ */
307
+ function calculateStatusLevel(percentageValue) {
308
+ if (percentageValue >= 80) return 'high';
309
+ if (percentageValue >= 60) return 'high';
310
+ if (percentageValue >= 40) return 'medium';
311
+ return 'low';
312
+ }
313
+
314
+ /**
315
+ * Generates relative path for shared.css
316
+ * @param {string} folderPath - Folder path
317
+ * @returns {string} Relative path to shared.css
318
+ */
319
+ function getSharedCssPath(folderPath) {
320
+ return folderPath ? '../'.repeat(folderPath.split('/').length) + 'shared.css' : 'shared.css';
321
+ }
322
+
323
+ /**
324
+ * Generates back link path
325
+ * @param {string} folderPath - Folder path
326
+ * @returns {string} Back link path
327
+ */
328
+ function getBackLink(folderPath) {
329
+ return folderPath ? '../'.repeat(folderPath.split('/').length) + 'index.html' : 'index.html';
330
+ }
331
+
332
+ export function generateFolderHTML(
333
+ folder,
334
+ allFolders,
335
+ showAllInitially,
336
+ getComplexityLevel,
337
+ getBaseFunctionName,
338
+ complexityThreshold = 10,
339
+ decisionPointTotals = {
340
+ controlFlow: 0,
341
+ expressions: 0,
342
+ functionParameters: 0,
343
+ }
344
+ ) {
345
+ const folderPath = folder.directory;
346
+ const backLink = getBackLink(folderPath);
347
+ const sharedCssPath = getSharedCssPath(folderPath);
348
+
349
+ // Generate summary section
350
+ const summarySection = generateSummarySection(
351
+ decisionPointTotals,
352
+ folder.totalFunctions,
353
+ folder.withinThreshold,
354
+ folder.percentage
355
+ );
356
+
357
+ // Calculate level for status bar
358
+ const percentageValue = folder.totalFunctions > 0
359
+ ? (folder.withinThreshold / folder.totalFunctions) * 100
360
+ : 100;
361
+ const level = calculateStatusLevel(percentageValue);
362
+
363
+ return `<!DOCTYPE html>
364
+ <html lang="en">
365
+ <head>
366
+ <meta charset="UTF-8">
367
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
368
+ <title>Complexity Report - ${folderPath || 'Root'}</title>
369
+ <link rel="stylesheet" href="${sharedCssPath}" />
370
+ </head>
371
+ <body>
372
+ <div class="pad2">
373
+ <div class="header-row">
374
+ <h1>${folderPath ? `<a href="${backLink}" style="color: #0074D9; text-decoration: none; font-weight: bold;">All files</a> <span style="font-weight: bold;">${folderPath}</span>` : 'All files'}</h1>
375
+ <a href="${folderPath ? '../'.repeat(folderPath.split('/').length) + 'about.html' : 'about.html'}" class="about-link">About Cyclomatic Complexity</a>
376
+ </div>
377
+ ${summarySection}
378
+ <div class="quiet" style="display: flex; align-items: center; gap: 15px; margin-top: 14px;">
379
+ <div>
380
+ Filter:
381
+ <input type="search" id="fileSearch">
382
+ </div>
383
+ <label style="margin: 0; font-weight: normal;">
384
+ <input type="checkbox" id="showAllFunctions" ${showAllInitially ? 'checked' : ''}>
385
+ Show all functions
386
+ </label>
387
+ </div>
388
+ </div>
389
+ <div class='status-line ${level}'></div>
390
+ <div class="pad2">
391
+ <table class="coverage-summary function-complexity-table">
392
+ <thead>
393
+ <tr>
394
+ <th class="file" data-sort="file">File <span class="sorter"></span></th>
395
+ <th class="bar" data-sort="complexity" style="text-align: right;"><span class="sorter"></span></th>
396
+ <th class="file" data-sort="function">Function <span class="sorter"></span></th>
397
+ <th class="pct" data-sort="complexity">Complexity <span class="sorter"></span></th>
398
+ <th class="abs" data-sort="line">Line <span class="sorter"></span></th>
399
+ </tr>
400
+ </thead>
401
+ <tbody>
402
+ ${groupFunctionsByBaseName(folder.functions, getBaseFunctionName)
403
+ .map((issue) =>
404
+ generateFunctionRow(
405
+ issue,
406
+ getComplexityLevel,
407
+ getBaseFunctionName,
408
+ showAllInitially,
409
+ complexityThreshold
410
+ )
411
+ )
412
+ .join('')}
413
+ </tbody>
414
+ </table>
415
+ </div>
416
+ <div class='footer quiet pad2 space-top1 center small'>
417
+ Complexity report generated by <a href="https://www.github.com/pythonidaer" target="_blank" rel="noopener noreferrer">pythonidaer</a> at ${new Date().toISOString()}
418
+ </div>
419
+ <script>
420
+ ${generateFolderPageScript()}
421
+ </script>
422
+ </body>
423
+ </html>`;
424
+ }
@@ -0,0 +1,6 @@
1
+ // Re-export all HTML generator functions from their respective modules
2
+ export { escapeHtml } from './utils.js';
3
+ export { generateAboutPageHTML } from './about.js';
4
+ export { generateMainIndexHTML } from './main-index.js';
5
+ export { generateFolderHTML } from './folder.js';
6
+ export { generateFileHTML } from './file.js';