@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,427 @@
|
|
|
1
|
+
import { getBaseFunctionName } from './function-extraction/index.js';
|
|
2
|
+
|
|
3
|
+
// escapeHtml will be imported from html-generators.js
|
|
4
|
+
// Called from html-generators.js to avoid circular dependency
|
|
5
|
+
let escapeHtml = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sets the escapeHtml function
|
|
9
|
+
* Called from html-generators.js to avoid circular dependency
|
|
10
|
+
* @param {Function} fn - escapeHtml function
|
|
11
|
+
*/
|
|
12
|
+
export function setEscapeHtml(fn) {
|
|
13
|
+
escapeHtml = fn;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Finds the immediate parent function for a callback
|
|
18
|
+
* @param {Object} func - Function object
|
|
19
|
+
* @param {Map} functionBoundaries - Map of function boundaries
|
|
20
|
+
* @param {Array} sortedFunctions - Sorted array of all functions
|
|
21
|
+
* @returns {Object|null} Parent function or null
|
|
22
|
+
*/
|
|
23
|
+
function findImmediateParentFunction(func, functionBoundaries, sortedFunctions) {
|
|
24
|
+
const funcBoundary = functionBoundaries.get(func.line);
|
|
25
|
+
if (!funcBoundary) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const containingFunctions = Array.from(functionBoundaries.entries())
|
|
30
|
+
.filter(([fl, boundary]) =>
|
|
31
|
+
fl !== func.line
|
|
32
|
+
&& boundary.start < funcBoundary.start
|
|
33
|
+
&& boundary.end >= funcBoundary.end
|
|
34
|
+
)
|
|
35
|
+
.sort((a, b) => b[1].start - a[1].start);
|
|
36
|
+
|
|
37
|
+
if (containingFunctions.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const immediateParentLine = containingFunctions[0][0];
|
|
42
|
+
return sortedFunctions.find((f) => f.line === immediateParentLine) || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the rightmost/leaf segment of a display name
|
|
47
|
+
* (framework-agnostic). Extraction (function-extraction.js) provides
|
|
48
|
+
* names from AST: method names (filter, sort, flatMap), hook names
|
|
49
|
+
* (useEffect, useCallback), handler labels (onClick handler), or
|
|
50
|
+
* variable/function names. We use that as the leaf; no whitelist of
|
|
51
|
+
* "callback types" is needed.
|
|
52
|
+
* @param {string} displayName - Full or hierarchical display name
|
|
53
|
+
* @returns {string} Leaf segment: last parenthetical content, or whole
|
|
54
|
+
*/
|
|
55
|
+
function getLeafName(displayName) {
|
|
56
|
+
if (!displayName || typeof displayName !== 'string') return displayName || '';
|
|
57
|
+
const match = displayName.match(/\(([^)]+)\)\s*$/);
|
|
58
|
+
return match ? match[1] : displayName;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Checks if a name is valid for use in hierarchical display (not a placeholder).
|
|
63
|
+
* @param {string} name - Leaf or base name
|
|
64
|
+
* @returns {boolean} True if valid
|
|
65
|
+
*/
|
|
66
|
+
function isValidNameForHierarchy(name) {
|
|
67
|
+
return name && name !== 'unknown' && name !== 'anonymous';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Checks if a function name indicates it's a cleanup callback
|
|
72
|
+
* Function names come from AST-based extraction, so this check is framework-agnostic
|
|
73
|
+
* @param {string} functionName - Function name to check
|
|
74
|
+
* @returns {boolean} True if it's a cleanup callback
|
|
75
|
+
*/
|
|
76
|
+
function isCleanupCallback(functionName) {
|
|
77
|
+
if (!functionName) return false;
|
|
78
|
+
return functionName.includes('return callback') || functionName === 'return';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Builds hierarchical display name using function boundaries
|
|
83
|
+
* (framework-agnostic). Recursively prepends parent names so nested
|
|
84
|
+
* functions show as "Parent (leaf)". The leaf name is whatever
|
|
85
|
+
* function-extraction.js provided from the AST (method name, hook,
|
|
86
|
+
* handler, variable name). No whitelist of callback types—works with
|
|
87
|
+
* any framework or API (filter, sort, flatMap, then, useEffect, etc.).
|
|
88
|
+
* @param {Object} func - Function object
|
|
89
|
+
* @param {Map} functionBoundaries - Map of function boundaries
|
|
90
|
+
* @param {Array} sortedFunctions - Sorted array of all functions
|
|
91
|
+
* @param {Set} visited - Set to track visited (prevents infinite loops)
|
|
92
|
+
* @returns {string} Full hierarchical display name
|
|
93
|
+
*/
|
|
94
|
+
function fixFunctionNameForCallback(
|
|
95
|
+
func,
|
|
96
|
+
functionBoundaries,
|
|
97
|
+
sortedFunctions,
|
|
98
|
+
visited = new Set()
|
|
99
|
+
) {
|
|
100
|
+
const displayName = func.functionName || 'unknown';
|
|
101
|
+
|
|
102
|
+
// Prevent infinite loops
|
|
103
|
+
const funcKey = `${func.file}:${func.line}`;
|
|
104
|
+
if (visited.has(funcKey)) {
|
|
105
|
+
return displayName;
|
|
106
|
+
}
|
|
107
|
+
visited.add(funcKey);
|
|
108
|
+
|
|
109
|
+
if (!functionBoundaries) {
|
|
110
|
+
return displayName;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Find the actual immediate parent using boundaries
|
|
114
|
+
const immediateParentFunc = findImmediateParentFunction(
|
|
115
|
+
func,
|
|
116
|
+
functionBoundaries,
|
|
117
|
+
sortedFunctions
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (!immediateParentFunc) {
|
|
121
|
+
// No parent found, return name as-is
|
|
122
|
+
return displayName;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// CRITICAL FIX: Cleanup callbacks should never be parents
|
|
126
|
+
// A cleanup callback is the return value of a function, so it can't
|
|
127
|
+
// contain other functions. If the parent is a cleanup callback, skip
|
|
128
|
+
// hierarchical naming to prevent incorrect nesting
|
|
129
|
+
if (isCleanupCallback(immediateParentFunc.functionName)) {
|
|
130
|
+
// Return the name as-is (cleanup callbacks are terminal - they don't have children)
|
|
131
|
+
return displayName;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Recursively build the parent's hierarchical name
|
|
135
|
+
const parentHierarchicalName = fixFunctionNameForCallback(
|
|
136
|
+
immediateParentFunc,
|
|
137
|
+
functionBoundaries,
|
|
138
|
+
sortedFunctions,
|
|
139
|
+
new Set(visited)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const parentBaseName = getBaseFunctionName(parentHierarchicalName);
|
|
143
|
+
if (!isValidNameForHierarchy(parentBaseName)) {
|
|
144
|
+
return displayName;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Framework-agnostic: use whatever name extraction gave us
|
|
148
|
+
// (method, hook, handler, variable name). No whitelist—any method
|
|
149
|
+
// (filter, sort, flatMap, then, etc.) or framework callback works.
|
|
150
|
+
const leafName = getLeafName(displayName);
|
|
151
|
+
if (isValidNameForHierarchy(leafName)) {
|
|
152
|
+
return `${parentHierarchicalName} → ${leafName}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return displayName;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets default column structure (used when not provided)
|
|
160
|
+
* @returns {Object} Column configuration
|
|
161
|
+
*/
|
|
162
|
+
function getDefaultColumnStructure() {
|
|
163
|
+
return {
|
|
164
|
+
groups: [
|
|
165
|
+
{
|
|
166
|
+
name: 'Control Flow',
|
|
167
|
+
columns: [
|
|
168
|
+
{ key: 'if', label: 'if' },
|
|
169
|
+
{ key: 'else if', label: 'else if' },
|
|
170
|
+
{ key: 'for', label: 'for' },
|
|
171
|
+
{ key: 'for...of', label: 'for...of' },
|
|
172
|
+
{ key: 'for...in', label: 'for...in' },
|
|
173
|
+
{ key: 'while', label: 'while' },
|
|
174
|
+
{ key: 'do...while', label: 'do...while' },
|
|
175
|
+
{ key: 'switch', label: 'switch' },
|
|
176
|
+
{ key: 'case', label: 'case' },
|
|
177
|
+
{ key: 'catch', label: 'catch' },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'Expressions',
|
|
182
|
+
columns: [
|
|
183
|
+
{ key: 'ternary', label: '?:' },
|
|
184
|
+
{ key: '&&', label: '&&' },
|
|
185
|
+
{ key: '||', label: '||' },
|
|
186
|
+
{ key: '??', label: '??' },
|
|
187
|
+
{ key: '?.', label: '?.' },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'Function Parameters',
|
|
192
|
+
columns: [
|
|
193
|
+
{ key: 'default parameter', label: 'default parameter' },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
baseColumn: { key: 'base', label: 'base' },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parses hierarchical display name into segments.
|
|
203
|
+
* Supports both "Parent → child → grandchild" (arrow) and
|
|
204
|
+
* "Parent (child) (grandchild)" (paren) formats.
|
|
205
|
+
* @param {string} displayName - Full hierarchical name
|
|
206
|
+
* @returns {string[]} Array of segment names
|
|
207
|
+
*/
|
|
208
|
+
function parseHierarchySegments(displayName) {
|
|
209
|
+
if (!displayName || typeof displayName !== 'string') {
|
|
210
|
+
return [displayName || ''];
|
|
211
|
+
}
|
|
212
|
+
if (displayName.includes(' → ')) {
|
|
213
|
+
return displayName
|
|
214
|
+
.split(/\s*→\s*/)
|
|
215
|
+
.map((s) => s.trim())
|
|
216
|
+
.filter(Boolean);
|
|
217
|
+
}
|
|
218
|
+
const parts = displayName.split(/\s*\(\s*/);
|
|
219
|
+
return parts
|
|
220
|
+
.map((s) => s.replace(/\s*\)\s*$/, '').trim())
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Renders function name for FCB: arrows between segments,
|
|
226
|
+
* last segment bold (highlights the function for this row).
|
|
227
|
+
* "Parent (child) (grandchild)" → "Parent → child → grandchild"
|
|
228
|
+
* with last in .function-name-leaf.
|
|
229
|
+
* @param {string} displayName - Full hierarchical display name
|
|
230
|
+
* @returns {string} HTML for the function-name cell content
|
|
231
|
+
*/
|
|
232
|
+
function formatFunctionNameDisplay(displayName) {
|
|
233
|
+
const segments = parseHierarchySegments(displayName);
|
|
234
|
+
if (segments.length === 0) return '';
|
|
235
|
+
const last = escapeHtml(segments[segments.length - 1]);
|
|
236
|
+
const rest = segments.slice(0, -1).map((s) => escapeHtml(s)).join(' → ');
|
|
237
|
+
const leafSpan = `<span class="function-name-leaf">${last}</span>`;
|
|
238
|
+
return rest ? `${rest} → ${leafSpan}` : leafSpan;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generates HTML for a function table row with individual breakdown
|
|
243
|
+
* columns
|
|
244
|
+
* @param {string} displayName - Function display name
|
|
245
|
+
* @param {number} lineNumber - Start line number (1-based)
|
|
246
|
+
* @param {number} complexity - Function complexity
|
|
247
|
+
* @param {Object} breakdownData - Breakdown data object
|
|
248
|
+
* @param {Object} columnStructure - Column structure configuration
|
|
249
|
+
* @param {Set} emptyColumns - Set of column keys that are empty
|
|
250
|
+
* @param {Array} visibleColumns - Array of visible column objects
|
|
251
|
+
* @param {number} [functionStart] - Function start line
|
|
252
|
+
* @param {number} [functionEnd] - Function end line
|
|
253
|
+
* @param {number} [complexityThreshold] - Linter threshold
|
|
254
|
+
* @returns {string} HTML string for table row
|
|
255
|
+
*/
|
|
256
|
+
function generateFunctionRowHTML(
|
|
257
|
+
displayName,
|
|
258
|
+
lineNumber,
|
|
259
|
+
complexity,
|
|
260
|
+
breakdownData,
|
|
261
|
+
columnStructure,
|
|
262
|
+
emptyColumns,
|
|
263
|
+
visibleColumns,
|
|
264
|
+
functionStart,
|
|
265
|
+
functionEnd,
|
|
266
|
+
complexityThreshold
|
|
267
|
+
) {
|
|
268
|
+
const startLine =
|
|
269
|
+
functionStart !== null && functionStart !== undefined
|
|
270
|
+
? functionStart
|
|
271
|
+
: lineNumber;
|
|
272
|
+
const endLine =
|
|
273
|
+
functionEnd !== null && functionEnd !== undefined
|
|
274
|
+
? functionEnd
|
|
275
|
+
: lineNumber;
|
|
276
|
+
// Generate cells only for visible columns (base = 1 is not shown as
|
|
277
|
+
// a column; see header "Function (base = 1)")
|
|
278
|
+
const breakdownCells = visibleColumns.map((col) => {
|
|
279
|
+
const value = breakdownData[col.key] || 0;
|
|
280
|
+
// Display "-" instead of 0 for better readability
|
|
281
|
+
const displayValue = value === 0 ? '-' : value;
|
|
282
|
+
// Add empty class for styling when value is "-"
|
|
283
|
+
const emptyClass = value === 0 ? ' breakdown-value-empty' : '';
|
|
284
|
+
return `<td class="breakdown-value${emptyClass}" data-column-key="${col.key}">${displayValue}</td>`;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const isAboveThreshold =
|
|
288
|
+
typeof complexityThreshold === 'number' &&
|
|
289
|
+
complexity > complexityThreshold;
|
|
290
|
+
const complexityCellClass = isAboveThreshold
|
|
291
|
+
? 'complexity-value complexity-above-threshold'
|
|
292
|
+
: 'complexity-value';
|
|
293
|
+
const functionNameHTML = formatFunctionNameDisplay(displayName);
|
|
294
|
+
return ` <tr class="breakdown-function-row" data-line="${lineNumber}" data-function-start="${startLine}" data-function-end="${endLine}" role="button" tabindex="0" title="Click to jump to function in code; click again to clear">
|
|
295
|
+
<td class="function-name">${functionNameHTML}</td>
|
|
296
|
+
<td class="breakdown-line-value breakdown-line-column" data-line="${lineNumber}">${lineNumber}</td>
|
|
297
|
+
<td class="${complexityCellClass}"><span class="complexity-number">${complexity}</span></td>
|
|
298
|
+
${breakdownCells.join('')}
|
|
299
|
+
</tr>`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Formats all functions in scannable, unambiguous format (one per line)
|
|
304
|
+
* Shows only what ESLint counts for cyclomatic complexity
|
|
305
|
+
* Groups functions by base name, showing the highest complexity version
|
|
306
|
+
* @param {Array} functions - Array of function objects
|
|
307
|
+
* @param {Map} functionBoundaries - Map of functionLine -> { start, end }
|
|
308
|
+
* @param {Map} functionBreakdowns - Map of functionLine -> breakdown
|
|
309
|
+
* @param {string} _sourceCode - Source code (unused, kept for API compat)
|
|
310
|
+
* @param {Object} [columnStructure] - Column structure config
|
|
311
|
+
* @param {Set} [emptyColumns] - Set of column keys that are empty
|
|
312
|
+
* @param {boolean} [showAllColumns] - Show all columns including empty
|
|
313
|
+
* @param {number} [complexityThreshold] - Linter threshold
|
|
314
|
+
* @returns {string} Formatted HTML string
|
|
315
|
+
*/
|
|
316
|
+
export function formatFunctionHierarchy(
|
|
317
|
+
functions,
|
|
318
|
+
functionBoundaries,
|
|
319
|
+
functionBreakdowns,
|
|
320
|
+
_sourceCode,
|
|
321
|
+
columnStructure,
|
|
322
|
+
emptyColumns,
|
|
323
|
+
showAllColumns = false,
|
|
324
|
+
complexityThreshold
|
|
325
|
+
) {
|
|
326
|
+
if (!escapeHtml) {
|
|
327
|
+
throw new Error('escapeHtml not set. Call setEscapeHtml() first.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (functions.length === 0) return '';
|
|
331
|
+
|
|
332
|
+
// Use default column structure if not provided
|
|
333
|
+
// (for backward compatibility with tests)
|
|
334
|
+
const structure = columnStructure || getDefaultColumnStructure();
|
|
335
|
+
|
|
336
|
+
// Use empty set if not provided (for backward compatibility with tests)
|
|
337
|
+
const emptyCols = emptyColumns || new Set();
|
|
338
|
+
|
|
339
|
+
// Build visible columns list based on showAllColumns flag
|
|
340
|
+
const visibleColumns = structure.groups.flatMap((group) => {
|
|
341
|
+
if (showAllColumns) {
|
|
342
|
+
return group.columns;
|
|
343
|
+
}
|
|
344
|
+
return group.columns.filter((col) => !emptyCols.has(col.key));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Show each function exactly as ESLint reports it, but deduplicate by line number
|
|
348
|
+
// This ensures the breakdown matches the inline code annotations
|
|
349
|
+
const lineToFunction = new Map();
|
|
350
|
+
|
|
351
|
+
functions.forEach((func) => {
|
|
352
|
+
const line = func.line;
|
|
353
|
+
const existing = lineToFunction.get(line);
|
|
354
|
+
|
|
355
|
+
// If multiple functions on same line, keep the highest complexity
|
|
356
|
+
// (this handles edge cases where ESLint might report multiple)
|
|
357
|
+
if (
|
|
358
|
+
!existing ||
|
|
359
|
+
parseInt(func.complexity, 10) > parseInt(existing.complexity, 10)
|
|
360
|
+
) {
|
|
361
|
+
lineToFunction.set(line, func);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Show each function separately, but group functions with the same name
|
|
366
|
+
// and line number. This ensures functions with the same name but
|
|
367
|
+
// different line numbers are shown separately. This matches what users
|
|
368
|
+
// see in the code view annotations
|
|
369
|
+
const functionGroups = new Map();
|
|
370
|
+
|
|
371
|
+
Array.from(lineToFunction.values()).forEach((func) => {
|
|
372
|
+
// Use file + function name + line number as key to ensure uniqueness
|
|
373
|
+
// This allows multiple functions with the same name (e.g.,
|
|
374
|
+
// "addEventListener callback" on different lines) to be shown
|
|
375
|
+
// separately, each with their own breakdown
|
|
376
|
+
const key = `${func.file}:${func.functionName}:${func.line}`;
|
|
377
|
+
|
|
378
|
+
const existing = functionGroups.get(key);
|
|
379
|
+
if (!existing) {
|
|
380
|
+
functionGroups.set(key, func);
|
|
381
|
+
} else {
|
|
382
|
+
// If somehow we have duplicate key, keep the one with higher complexity
|
|
383
|
+
if (parseInt(func.complexity, 10) > parseInt(existing.complexity, 10)) {
|
|
384
|
+
functionGroups.set(key, func);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Sort by line number to match code order
|
|
390
|
+
const sortedFunctions = Array.from(functionGroups.values()).sort(
|
|
391
|
+
(a, b) => a.line - b.line
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const lines = [];
|
|
395
|
+
|
|
396
|
+
// Format each function on one line with individual breakdown columns
|
|
397
|
+
sortedFunctions.forEach((func) => {
|
|
398
|
+
const complexity = parseInt(func.complexity, 10);
|
|
399
|
+
const breakdown = functionBreakdowns.get(func.line);
|
|
400
|
+
const breakdownData = breakdown ? breakdown.breakdown : {};
|
|
401
|
+
const displayName = fixFunctionNameForCallback(
|
|
402
|
+
func,
|
|
403
|
+
functionBoundaries,
|
|
404
|
+
sortedFunctions
|
|
405
|
+
);
|
|
406
|
+
const boundary = functionBoundaries
|
|
407
|
+
? functionBoundaries.get(func.line)
|
|
408
|
+
: null;
|
|
409
|
+
const functionStart = boundary ? boundary.start : null;
|
|
410
|
+
const functionEnd = boundary ? boundary.end : null;
|
|
411
|
+
const rowHTML = generateFunctionRowHTML(
|
|
412
|
+
displayName,
|
|
413
|
+
func.line,
|
|
414
|
+
complexity,
|
|
415
|
+
breakdownData,
|
|
416
|
+
structure,
|
|
417
|
+
emptyCols,
|
|
418
|
+
visibleColumns,
|
|
419
|
+
functionStart,
|
|
420
|
+
functionEnd,
|
|
421
|
+
complexityThreshold
|
|
422
|
+
);
|
|
423
|
+
lines.push(rowHTML);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
return lines.join('\n');
|
|
427
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the standalone "About Cyclomatic Complexity" page (complexity/about.html).
|
|
3
|
+
* Barebones, Istanbul-style. Examples live on a separate page.
|
|
4
|
+
* @returns {string} Full HTML document string
|
|
5
|
+
*/
|
|
6
|
+
export function generateAboutPageHTML() {
|
|
7
|
+
return `<!DOCTYPE html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="UTF-8">
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
12
|
+
<title>About Cyclomatic Complexity</title>
|
|
13
|
+
<link rel="stylesheet" href="shared.css" />
|
|
14
|
+
<style>
|
|
15
|
+
body { padding: 20px; line-height: 1.4; }
|
|
16
|
+
.back-link { display: inline-block; margin-bottom: 12px; }
|
|
17
|
+
h1 { margin-top: 1rem; }
|
|
18
|
+
h2 { font-size: 16px; margin: 2rem 0 6px 0; }
|
|
19
|
+
.about-subheading { margin-top: 2rem; }
|
|
20
|
+
.about-last-p { margin-bottom: 2rem; }
|
|
21
|
+
.summary-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin: 1rem 0 1rem 0; }
|
|
22
|
+
@media (max-width: 640px) { .summary-two-col { grid-template-columns: 1fr; } }
|
|
23
|
+
.summary-col { min-width: 0; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<a href="index.html" class="back-link">← Back to complexity report</a>
|
|
28
|
+
<h1>About Cyclomatic Complexity</h1>
|
|
29
|
+
<p>Cyclomatic complexity measures the number of linearly independent execution paths through a function. It is calculated as a base value of 1 plus the number of decision points. Values above 10 typically indicate code that is harder to test, reason about, and safely modify, and should prompt refactoring. Cyclomatic complexity measures structural branching, not runtime performance or algorithmic efficiency.</p>
|
|
30
|
+
<h2>About This Report</h2>
|
|
31
|
+
<p>This complexity report extends <strong>ESLint's <code>complexity</code> rule</strong> and uses your project's chosen variant (<code>classic</code> or <code>modified</code>; see below). The report analyzes whatever files your ESLint config lints—commonly <code>*.ts</code>, <code>*.tsx</code>, and <code>*.js</code>. Single-file component formats (e.g. Vue <code>.vue</code>, Svelte <code>.svelte</code>) are only included if ESLint is configured to lint them. The lists below show examples of which constructs are counted and which are not.</p>
|
|
32
|
+
<div class="summary-two-col">
|
|
33
|
+
<div class="summary-col">
|
|
34
|
+
<h3>Counted</h3>
|
|
35
|
+
<ul>
|
|
36
|
+
<li><strong>Callable units</strong> — complexity is measured per function, method, or arrow function</li>
|
|
37
|
+
<li><strong>Statements</strong> — such as: <code>if</code>, <code>else if</code>, <code>switch</code> (in <code>modified</code>)</li>
|
|
38
|
+
<li><strong>Clauses</strong> — such as: <code>catch</code>, <code>case</code> (in <code>classic</code>)</li>
|
|
39
|
+
<li><strong>Loops</strong> — such as: <code>for</code>, <code>for...of</code>, <code>for...in</code>, <code>while</code>, <code>do...while</code></li>
|
|
40
|
+
<li><strong>Ternary</strong> —<code>? :</code></li>
|
|
41
|
+
<li><strong>Logical</strong> and — <code>&&</code></li>
|
|
42
|
+
<li><strong>Logical</strong> or — <code>||</code></li>
|
|
43
|
+
<li><strong>Optional chaining</strong> — <code>?.</code></li>
|
|
44
|
+
<li><strong>Nullish coalescing</strong> — <code>??</code></li>
|
|
45
|
+
<li><strong>Default parameters</strong></li>
|
|
46
|
+
</ul>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="summary-col">
|
|
49
|
+
<h3>Not counted</h3>
|
|
50
|
+
<ul>
|
|
51
|
+
<li><strong>Top-level module code</strong> (code outside of functions); runs once at load and is not a callable unit</li>
|
|
52
|
+
<li><strong>Sequential statements</strong> (assignments, calls, declarations)</li>
|
|
53
|
+
<li><code>else</code> — does not introduce a new decision path by itself</li>
|
|
54
|
+
<li><code>try</code> — does not add complexity by itself; each <code>catch</code> adds one decision path</li>
|
|
55
|
+
<li>JSX without conditionals</li>
|
|
56
|
+
<li>Hook calls (only callbacks count)</li>
|
|
57
|
+
<li>TypeScript types, interfaces, generics</li>
|
|
58
|
+
<li>Literals, destructuring; arithmetic, strings</li>
|
|
59
|
+
<li>Property access; method calls</li>
|
|
60
|
+
<li><strong>Early exits</strong> (<code>return</code>, <code>break</code>, <code>continue</code>) — do not add complexity unless conditional</li>
|
|
61
|
+
<li><strong>Nesting depth</strong> — nesting itself does not increase cyclomatic complexity; only decision points do, and nested functions are measured independently with their own base complexity.</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<h2>Classic vs modified variant</h2>
|
|
66
|
+
<p>ESLint's <code>complexity</code> rule supports two variants. This report uses the variant from your project's config.</p>
|
|
67
|
+
<ul>
|
|
68
|
+
<li><strong>Classic</strong> — Each <code>case</code> adds +1. So a switch with 5 cases adds 5 to complexity.</li>
|
|
69
|
+
<li><strong>Modified</strong> — The whole <code>switch</code> adds +1 regardless of how many cases. So a switch with 5 cases adds 1 to complexity.</li>
|
|
70
|
+
</ul>
|
|
71
|
+
<h2 class="about-subheading">Cyclomatic vs. Cognitive Complexity</h2>
|
|
72
|
+
<p class="about-last-p">While cyclomatic complexity measures the number of independent execution paths in a function to estimate testing effort and structural risk, cognitive complexity is designed to reflect how difficult code is for a human to read and understand. Cognitive complexity increases with control flow and nesting that disrupts linear reading (such as conditionals, loops, and early exits), but intentionally does not track boolean expressions, default parameters, or short-circuit operators. By focusing on mental load rather than path count, cognitive complexity better highlights code that is technically correct but unnecessarily hard to reason about or maintain.</p>
|
|
73
|
+
</body>
|
|
74
|
+
</html>`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boundary maps for file page borders
|
|
3
|
+
*/
|
|
4
|
+
import { getIndentChars } from './file-helpers.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds a map from each line number to the innermost function span containing it.
|
|
8
|
+
* @param {Map} functionBoundaries - Map of function lines to boundary objects
|
|
9
|
+
* @param {number} sourceLinesLength - Total number of source lines
|
|
10
|
+
* @param {string[]} sourceLines - Source lines (1-based index)
|
|
11
|
+
* @returns {Object} Plain object: line number -> { start, end, indent }
|
|
12
|
+
*/
|
|
13
|
+
export function buildLineToSpan(
|
|
14
|
+
functionBoundaries,
|
|
15
|
+
sourceLinesLength,
|
|
16
|
+
sourceLines
|
|
17
|
+
) {
|
|
18
|
+
const lineToSpan = {};
|
|
19
|
+
const boundaries = [...functionBoundaries.values()].map((b) => ({
|
|
20
|
+
start: b.start,
|
|
21
|
+
end: b.end,
|
|
22
|
+
}));
|
|
23
|
+
for (let L = 1; L <= sourceLinesLength; L += 1) {
|
|
24
|
+
const containing = boundaries.filter(
|
|
25
|
+
({ start, end }) => start <= L && L <= end
|
|
26
|
+
);
|
|
27
|
+
if (containing.length === 0) continue;
|
|
28
|
+
const innermost = containing.reduce((best, cur) =>
|
|
29
|
+
(cur.end - cur.start) < (best.end - best.start) ? cur : best
|
|
30
|
+
);
|
|
31
|
+
const startLine = sourceLines[innermost.start - 1];
|
|
32
|
+
const indent = typeof startLine === 'string' ? getIndentChars(startLine) : 0;
|
|
33
|
+
lineToSpan[L] = { start: innermost.start, end: innermost.end, indent };
|
|
34
|
+
}
|
|
35
|
+
return lineToSpan;
|
|
36
|
+
}
|