@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,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
|
+
}
|