@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,346 @@
1
+ /**
2
+ * Generates HTML row for a folder
3
+ * @param {Object} folder - Folder object
4
+ * @returns {string} HTML row string
5
+ */
6
+ function generateFolderRow(folder) {
7
+ // Helper function to format percentage (2 decimals if needed)
8
+ const formatPercentage = (numerator, denominator) => {
9
+ if (denominator === 0) return '0%';
10
+ const percentage = (numerator / denominator) * 100;
11
+ // Whole number: no decimals; otherwise 2 decimal places
12
+ return percentage % 1 === 0
13
+ ? `${percentage}%`
14
+ : `${percentage.toFixed(2)}%`;
15
+ };
16
+
17
+ // Calculate accurate percentage (not the pre-rounded value)
18
+ const percentageValue = folder.totalFunctions > 0
19
+ ? (folder.withinThreshold / folder.totalFunctions) * 100
20
+ : 100;
21
+ const percentageDisplay = formatPercentage(
22
+ folder.withinThreshold,
23
+ folder.totalFunctions
24
+ );
25
+ // Use raw value for bar width calculation
26
+ const percentageForBar = percentageValue;
27
+
28
+ const level =
29
+ percentageValue >= 80
30
+ ? 'high'
31
+ : percentageValue >= 60
32
+ ? 'high'
33
+ : percentageValue >= 40
34
+ ? 'medium'
35
+ : 'low';
36
+ return `
37
+ <tr data-file="${folder.directory || '.'}"
38
+ data-functions="${folder.withinThreshold}/${folder.totalFunctions}"
39
+ data-percentage="${percentageValue}">
40
+ <td class="file">
41
+ <a href="${folder.directory ? folder.directory + '/index.html' : 'index.html'}">
42
+ ${folder.directory || '.'}
43
+ </a>
44
+ </td>
45
+ <td class="bar ${level}">
46
+ <div class="chart">
47
+ <div class="cover-fill ${level} ${percentageForBar === 100 ? 'cover-full' : ''}"
48
+ style="width: ${percentageForBar}%"></div>
49
+ <div class="cover-empty" style="width: ${100 - percentageForBar}%"></div>
50
+ </div>
51
+ </td>
52
+ <td class="pic">${percentageDisplay}</td>
53
+ <td class="pct">${folder.withinThreshold}/${folder.totalFunctions}</td>
54
+ </tr>
55
+ `;
56
+ }
57
+
58
+ /**
59
+ * Generates JavaScript code for main index page
60
+ * @returns {string} JavaScript code as string
61
+ */
62
+ function generateMainIndexScript() {
63
+ return `(function() {
64
+ const headers = document.querySelectorAll('.coverage-summary th[data-sort]');
65
+ let currentSort = { column: null, direction: 'asc' };
66
+
67
+ // Filter functionality
68
+ const fileSearch = document.getElementById('fileSearch');
69
+ if (fileSearch) {
70
+ fileSearch.addEventListener('input', function() {
71
+ const searchValue = this.value;
72
+ const tbody = document.querySelector('.coverage-summary tbody');
73
+ if (!tbody) return;
74
+ const rows = Array.from(tbody.querySelectorAll('tr'));
75
+
76
+ // Try to create a RegExp from the searchValue. If it fails (invalid regex),
77
+ // it will be treated as a plain text search
78
+ let searchRegex;
79
+ try {
80
+ searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
81
+ } catch (error) {
82
+ searchRegex = null;
83
+ }
84
+
85
+ rows.forEach(row => {
86
+ let isMatch = false;
87
+
88
+ if (searchRegex) {
89
+ // If a valid regex was created, use it for matching
90
+ isMatch = searchRegex.test(row.textContent);
91
+ } else {
92
+ // Otherwise, fall back to the original plain text search
93
+ isMatch = row.textContent.toLowerCase().includes(searchValue.toLowerCase());
94
+ }
95
+
96
+ row.style.display = isMatch ? '' : 'none';
97
+ });
98
+ });
99
+ }
100
+
101
+ function getSortValue(row, column) {
102
+ if (column === 'file') {
103
+ return row.getAttribute('data-file') || '';
104
+ }
105
+ if (column === 'functions') {
106
+ const parts = (row.getAttribute('data-functions') || '0/0').split('/');
107
+ return parseInt(parts[1] || 1, 10);
108
+ }
109
+ if (column === 'percentage') {
110
+ return parseFloat(row.getAttribute('data-percentage') || 0);
111
+ }
112
+ return 0;
113
+ }
114
+
115
+ function compareValues(aVal, bVal, direction, column) {
116
+ if (column === 'functions') {
117
+ if (direction === 'desc') {
118
+ return bVal > aVal ? 1 : bVal < aVal ? -1 : 0;
119
+ }
120
+ return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
121
+ }
122
+
123
+ if (typeof aVal === 'string') {
124
+ const comparison = aVal.localeCompare(bVal);
125
+ return direction === 'asc' ? comparison : -comparison;
126
+ }
127
+
128
+ if (direction === 'asc') {
129
+ return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
130
+ }
131
+ return aVal < bVal ? 1 : aVal > bVal ? -1 : 0;
132
+ }
133
+
134
+ headers.forEach(header => {
135
+ header.addEventListener('click', function() {
136
+ const column = this.getAttribute('data-sort');
137
+ const tbody = this.closest('table').querySelector('tbody');
138
+ const rows = Array.from(tbody.querySelectorAll('tr'));
139
+
140
+ if (currentSort.column === column) {
141
+ currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
142
+ } else {
143
+ currentSort.column = column;
144
+ currentSort.direction = (column === 'functions' || column === 'percentage') ? 'desc' : 'asc';
145
+ }
146
+
147
+ headers.forEach(h => {
148
+ // Remove sorted classes from all headers
149
+ h.classList.remove('sorted', 'sorted-desc');
150
+ // Add appropriate class to the clicked header
151
+ if (h === this) {
152
+ if (currentSort.direction === 'asc') {
153
+ h.classList.add('sorted');
154
+ } else {
155
+ h.classList.add('sorted-desc');
156
+ }
157
+ }
158
+ });
159
+
160
+ rows.sort((a, b) => {
161
+ const aVal = getSortValue(a, column);
162
+ const bVal = getSortValue(b, column);
163
+ return compareValues(aVal, bVal, currentSort.direction, column);
164
+ });
165
+
166
+ tbody.innerHTML = '';
167
+ rows.forEach(row => tbody.appendChild(row));
168
+ });
169
+ });
170
+
171
+ // Export functionality
172
+ const exportBtn = document.getElementById('exportBtn');
173
+ if (exportBtn) {
174
+ const exportMenu = document.createElement('div');
175
+ exportMenu.id = 'exportMenu';
176
+ exportMenu.style.cssText = 'display: none; position: absolute; top: 100%; left: 0; background: white; border: 1px solid #ccc; border-radius: 2px; padding: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 1000; margin-top: 4px; min-width: 240px;';
177
+ exportMenu.innerHTML = '<div style="display: flex; flex-direction: column; gap: 4px;">' +
178
+ '<a href="reports/function-names.all.txt" download style="padding: 4px 8px; text-decoration: none; color: #000; display: block;">All Functions (TXT)</a>' +
179
+ '<a href="reports/function-names.all-leaf.txt" download style="padding: 4px 8px; text-decoration: none; color: #000; display: block;">All Functions – leaf only (TXT)</a>' +
180
+ '<a href="reports/function-names.all.md" download style="padding: 4px 8px; text-decoration: none; color: #000; display: block;">All Functions (Markdown)</a>' +
181
+ '<hr style="margin: 4px 0; border: none; border-top: 1px solid #ccc;">' +
182
+ '<a href="reports/function-names-by-file.txt" download style="padding: 4px 8px; text-decoration: none; color: #000; display: block;">By Folder/File (TXT)</a>' +
183
+ '<a href="reports/function-names-by-file-leaf.txt" download style="padding: 4px 8px; text-decoration: none; color: #000; display: block;">By Folder/File – leaf only (TXT)</a>' +
184
+ '<a href="reports/function-names-by-file.md" download style="padding: 4px 8px; text-decoration: none; color: #000; display: block;">By Folder/File (Markdown)</a>' +
185
+ '<hr style="margin: 4px 0; border: none; border-top: 1px solid #ccc;">' +
186
+ '<a href="reports/file-names-alphabetical.txt" download style="padding: 4px 8px; text-decoration: none; color: #000; display: block;">File names – (TXT)</a>' +
187
+ '<a href="reports/file-names-alphabetical.md" download style="padding: 4px 8px; text-decoration: none; color: #000; display: block;">File names – (Markdown)</a>' +
188
+ '</div>';
189
+ exportBtn.parentElement.appendChild(exportMenu);
190
+
191
+ exportBtn.addEventListener('click', function(e) {
192
+ e.stopPropagation();
193
+ exportMenu.style.display = exportMenu.style.display === 'none' ? 'block' : 'none';
194
+ });
195
+
196
+ document.addEventListener('click', function(e) {
197
+ if (!exportBtn.contains(e.target) && !exportMenu.contains(e.target)) {
198
+ exportMenu.style.display = 'none';
199
+ }
200
+ });
201
+ }
202
+ })();`;
203
+ }
204
+
205
+ /**
206
+ * Generates the main index HTML page
207
+ * @param {Array} folders - Array of folder objects
208
+ * @param {number} allFunctionsCount - Total number of functions
209
+ * @param {Array} overThreshold - Array of functions over threshold
210
+ * @param {number} maxComplexity - Maximum complexity value
211
+ * @param {number} avgComplexity - Average complexity value
212
+ * @param {boolean} showAllInitially - Show all functions initially
213
+ * @param {number} _complexityThreshold - Complexity threshold from ESLint
214
+ * (unused, kept for API compatibility)
215
+ * @param {Object} decisionPointTotals - Object with controlFlow, expressions,
216
+ * functionParameters totals
217
+ * @param {number} withinThreshold - Number of functions within threshold
218
+ * @param {number} _withinThresholdPercentage - Percentage within threshold
219
+ * (unused, calculated from withinThreshold/allFunctionsCount)
220
+ * @returns {string} Full HTML document string
221
+ */
222
+ export function generateMainIndexHTML(
223
+ folders,
224
+ allFunctionsCount,
225
+ overThreshold,
226
+ maxComplexity,
227
+ avgComplexity,
228
+ _showAllInitially,
229
+ _complexityThreshold = 10,
230
+ decisionPointTotals = {
231
+ controlFlow: 0,
232
+ expressions: 0,
233
+ functionParameters: 0,
234
+ },
235
+ withinThreshold = 0,
236
+ _withinThresholdPercentage = 100
237
+ ) {
238
+ const { controlFlow, expressions, functionParameters } = decisionPointTotals;
239
+
240
+ // Helper function to format percentage (2 decimals if needed)
241
+ const formatPercentage = (numerator, denominator) => {
242
+ if (denominator === 0) return '0%';
243
+ const percentage = (numerator / denominator) * 100;
244
+ // Whole number: no decimals; otherwise 2 decimal places
245
+ return percentage % 1 === 0
246
+ ? `${percentage}%`
247
+ : `${percentage.toFixed(2)}%`;
248
+ };
249
+
250
+ // Functions: show bold % and fraction (can be < 100%)
251
+ // Control Flow / Expressions / Default Params: always 100% here
252
+ const functionsPercentage = formatPercentage(
253
+ withinThreshold,
254
+ allFunctionsCount
255
+ );
256
+
257
+ // Calculate level for status bar
258
+ const percentageValue = allFunctionsCount > 0
259
+ ? (withinThreshold / allFunctionsCount) * 100
260
+ : 100;
261
+ const level =
262
+ percentageValue >= 80
263
+ ? 'high'
264
+ : percentageValue >= 60
265
+ ? 'high'
266
+ : percentageValue >= 40
267
+ ? 'medium'
268
+ : 'low';
269
+
270
+ return `<!DOCTYPE html>
271
+ <html lang="en">
272
+ <head>
273
+ <meta charset="UTF-8">
274
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
275
+ <title>Complexity Report</title>
276
+ <link rel="stylesheet" href="shared.css" />
277
+ <style>
278
+ #exportMenu a:hover {
279
+ background-color: #f5f5f5;
280
+ border-radius: 2px;
281
+ }
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <div class="pad2">
286
+ <div class="header-row">
287
+ <h1>All files</h1>
288
+ <a href="about.html" class="about-link">About Cyclomatic Complexity</a>
289
+ </div>
290
+ <div class="clearfix">
291
+ <div class='fl pad1y space-right2'>
292
+ <span class="strong">${functionsPercentage}</span>
293
+ <span class="quiet">Functions</span>
294
+ <span class='fraction'>${withinThreshold}/${allFunctionsCount}</span>
295
+ </div>
296
+ <div class='fl pad1y space-right2'>
297
+ <span class="quiet">Control Flow</span>
298
+ <span class='fraction'>${controlFlow}</span>
299
+ </div>
300
+ <div class='fl pad1y space-right2'>
301
+ <span class="quiet">Expressions</span>
302
+ <span class='fraction'>${expressions}</span>
303
+ </div>
304
+ <div class='fl pad1y space-right2'>
305
+ <span class="quiet">Default Parameters</span>
306
+ <span class='fraction'>${functionParameters}</span>
307
+ </div>
308
+ </div>
309
+ <div class="quiet" style="margin-top: 14px; display: flex; align-items: center; gap: 15px;">
310
+ <span>Filter:</span>
311
+ <input type="search" id="fileSearch" style="flex: 0 0 auto; width: auto;">
312
+ <div style="position: relative;">
313
+ <button id="exportBtn" style="padding: 6px 12px; font-size: 13px; cursor: pointer; border: 1px solid var(--color-muted, #ccc); background: var(--color-bg, #fff); color: var(--color-text, #000); border-radius: 2px;">Export</button>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ <div class='status-line ${level}'></div>
318
+ <div class="pad2">
319
+ ${folders.length === 0
320
+ ? '<div class="pad2y"><div class="strong" style="color: rgb(77,146,33);">✓ No functions found.</div></div>'
321
+ : ` <div class="folder-view" id="folderView">
322
+ <table class="coverage-summary">
323
+ <thead>
324
+ <tr>
325
+ <th class="file" data-sort="file">File <span class="sorter"></span></th>
326
+ <th class="bar" data-sort="percentage" style="text-align: right;"><span class="sorter"></span></th>
327
+ <th class="pic" data-sort="percentage" style="text-align: left;">Functions <span class="sorter"></span></th>
328
+ <th class="pct" data-sort="functions" style="text-align: right;"><span class="sorter"></span></th>
329
+ </tr>
330
+ </thead>
331
+ <tbody>
332
+ ${folders.map(folder => generateFolderRow(folder)).join('')}
333
+ </tbody>
334
+ </table>
335
+ </div>`
336
+ }
337
+ </div>
338
+ <div class='footer quiet pad2 space-top1 center small'>
339
+ Complexity report generated by <a href="https://www.github.com/pythonidaer" target="_blank" rel="noopener noreferrer">pythonidaer</a> at ${new Date().toISOString()}
340
+ </div>
341
+ <script>
342
+ ${generateMainIndexScript()}
343
+ </script>
344
+ </body>
345
+ </html>`;
346
+ }