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