@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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breakdown and FCB (Function Complexity Breakdown) logic for file page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects which columns are completely empty across all functions
|
|
7
|
+
*/
|
|
8
|
+
export function detectEmptyColumns(functions, functionBreakdowns, columnStructure) {
|
|
9
|
+
const emptyColumns = new Set();
|
|
10
|
+
const allColumnKeys = columnStructure.groups.flatMap(group =>
|
|
11
|
+
group.columns.map(col => col.key)
|
|
12
|
+
);
|
|
13
|
+
allColumnKeys.forEach(columnKey => {
|
|
14
|
+
let hasAnyValue = false;
|
|
15
|
+
functions.forEach(func => {
|
|
16
|
+
const breakdown = functionBreakdowns.get(func.line);
|
|
17
|
+
if (breakdown && breakdown.breakdown) {
|
|
18
|
+
const value = breakdown.breakdown[columnKey] || 0;
|
|
19
|
+
if (value > 0) hasAnyValue = true;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
if (!hasAnyValue) emptyColumns.add(columnKey);
|
|
23
|
+
});
|
|
24
|
+
return emptyColumns;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Defines the breakdown column structure with groupings.
|
|
29
|
+
*/
|
|
30
|
+
export function getBreakdownColumnStructure(variant = 'classic') {
|
|
31
|
+
const controlFlowBase = [
|
|
32
|
+
{ key: 'if', label: 'if' },
|
|
33
|
+
{ key: 'else if', label: 'else if' },
|
|
34
|
+
{ key: 'for', label: 'for' },
|
|
35
|
+
{ key: 'for...of', label: 'for...of' },
|
|
36
|
+
{ key: 'for...in', label: 'for...in' },
|
|
37
|
+
{ key: 'while', label: 'while' },
|
|
38
|
+
{ key: 'do...while', label: 'do...while' },
|
|
39
|
+
...(variant === 'modified' ? [{ key: 'switch', label: 'switch' }] : [{ key: 'case', label: 'case' }]),
|
|
40
|
+
{ key: 'catch', label: 'catch' },
|
|
41
|
+
];
|
|
42
|
+
return {
|
|
43
|
+
groups: [
|
|
44
|
+
{ name: 'Control Flow', columns: controlFlowBase },
|
|
45
|
+
{
|
|
46
|
+
name: 'Expressions',
|
|
47
|
+
columns: [
|
|
48
|
+
{ key: 'ternary', label: '?:' },
|
|
49
|
+
{ key: '&&', label: '&&' },
|
|
50
|
+
{ key: '||', label: '||' },
|
|
51
|
+
{ key: '??', label: '??' },
|
|
52
|
+
{ key: '?.', label: '?.' },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'Function Parameters',
|
|
57
|
+
columns: [{ key: 'default parameter', label: 'default parameter' }],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
baseColumn: { key: 'base', label: 'base' },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Finds the boundary for a function
|
|
66
|
+
*/
|
|
67
|
+
export function findBoundaryForFunction(functionLine, functionBoundaries) {
|
|
68
|
+
let boundary = functionBoundaries.get(functionLine);
|
|
69
|
+
if (!boundary) {
|
|
70
|
+
for (const [, b] of functionBoundaries.entries()) {
|
|
71
|
+
if ((functionLine >= b.start && functionLine <= b.end) ||
|
|
72
|
+
(functionLine < b.start && b.start === functionLine + 1)) {
|
|
73
|
+
boundary = b;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return boundary;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Finds the breakdown line for a function
|
|
83
|
+
*/
|
|
84
|
+
export function findBreakdownLine(functionLine) {
|
|
85
|
+
return functionLine;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Logs complexity mismatches for debugging
|
|
90
|
+
*/
|
|
91
|
+
export function logComplexityMismatch(func, breakdown, _functionBoundaries) {
|
|
92
|
+
const actualComplexity = parseInt(func.complexity, 10);
|
|
93
|
+
const calculatedTotal = breakdown.calculatedTotal;
|
|
94
|
+
if (Math.abs(calculatedTotal - actualComplexity) > 1) {
|
|
95
|
+
console.warn(`Complexity mismatch for ${func.functionName} at line ${func.line}: ESLint reports ${actualComplexity}, calculated ${calculatedTotal}`);
|
|
96
|
+
if (breakdown.decisionPoints && breakdown.decisionPoints.length > 0) {
|
|
97
|
+
console.warn(` Decision points found:`, breakdown.decisionPoints.map(dp => `${dp.type} at line ${dp.line}`).join(', '));
|
|
98
|
+
} else {
|
|
99
|
+
console.warn(` Decision points found: (none)`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Calculates complexity breakdowns for all functions
|
|
106
|
+
*/
|
|
107
|
+
export function calculateFunctionBreakdowns(
|
|
108
|
+
functions,
|
|
109
|
+
functionBoundaries,
|
|
110
|
+
decisionPoints,
|
|
111
|
+
calculateComplexityBreakdown
|
|
112
|
+
) {
|
|
113
|
+
const functionBreakdowns = new Map();
|
|
114
|
+
functions.forEach(func => {
|
|
115
|
+
findBoundaryForFunction(func.line, functionBoundaries);
|
|
116
|
+
const breakdownLine = findBreakdownLine(func.line);
|
|
117
|
+
const breakdown = calculateComplexityBreakdown(breakdownLine, decisionPoints, 1);
|
|
118
|
+
logComplexityMismatch(func, breakdown, functionBoundaries);
|
|
119
|
+
functionBreakdowns.set(func.line, breakdown);
|
|
120
|
+
});
|
|
121
|
+
return functionBreakdowns;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Calculates decision point totals from function breakdowns
|
|
126
|
+
*/
|
|
127
|
+
export function calculateDecisionPointTotalsFromBreakdowns(
|
|
128
|
+
functionBreakdowns
|
|
129
|
+
) {
|
|
130
|
+
const controlFlowTypes = [
|
|
131
|
+
'if',
|
|
132
|
+
'else if',
|
|
133
|
+
'for',
|
|
134
|
+
'for...of',
|
|
135
|
+
'for...in',
|
|
136
|
+
'while',
|
|
137
|
+
'do...while',
|
|
138
|
+
'switch',
|
|
139
|
+
'case',
|
|
140
|
+
'catch',
|
|
141
|
+
];
|
|
142
|
+
const expressionTypes = ['ternary', '&&', '||', '??', '?.'];
|
|
143
|
+
const functionParameterTypes = ['default parameter'];
|
|
144
|
+
let controlFlowTotal = 0;
|
|
145
|
+
let expressionsTotal = 0;
|
|
146
|
+
let functionParametersTotal = 0;
|
|
147
|
+
functionBreakdowns.forEach((breakdown) => {
|
|
148
|
+
const breakdownObj = breakdown.breakdown || {};
|
|
149
|
+
controlFlowTypes.forEach((type) => {
|
|
150
|
+
controlFlowTotal += breakdownObj[type] || 0;
|
|
151
|
+
});
|
|
152
|
+
expressionTypes.forEach((type) => {
|
|
153
|
+
expressionsTotal += breakdownObj[type] || 0;
|
|
154
|
+
});
|
|
155
|
+
functionParameterTypes.forEach((type) => {
|
|
156
|
+
functionParametersTotal += breakdownObj[type] || 0;
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
controlFlow: controlFlowTotal,
|
|
161
|
+
expressions: expressionsTotal,
|
|
162
|
+
functionParameters: functionParametersTotal,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generates summary section HTML
|
|
168
|
+
*/
|
|
169
|
+
export function generateSummarySection(
|
|
170
|
+
decisionPointTotals,
|
|
171
|
+
totalFunctions,
|
|
172
|
+
withinThreshold,
|
|
173
|
+
_withinThresholdPercentage
|
|
174
|
+
) {
|
|
175
|
+
const { controlFlow, expressions, functionParameters } = decisionPointTotals;
|
|
176
|
+
const formatPercentage = (num, den) => (den === 0 ? '0%' : ((num / den) * 100) % 1 === 0 ? `${(num / den) * 100}%` : `${((num / den) * 100).toFixed(2)}%`);
|
|
177
|
+
const functionsPercentage = formatPercentage(withinThreshold, totalFunctions);
|
|
178
|
+
return `
|
|
179
|
+
<div class="clearfix">
|
|
180
|
+
<div class='fl pad1y space-right2'>
|
|
181
|
+
<span class="strong">${functionsPercentage}</span>
|
|
182
|
+
<span class="quiet">Functions</span>
|
|
183
|
+
<span class='fraction'>${withinThreshold}/${totalFunctions}</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div class='fl pad1y space-right2'>
|
|
186
|
+
<span class="quiet">Control Flow</span>
|
|
187
|
+
<span class='fraction'>${controlFlow}</span>
|
|
188
|
+
</div>
|
|
189
|
+
<div class='fl pad1y space-right2'>
|
|
190
|
+
<span class="quiet">Expressions</span>
|
|
191
|
+
<span class='fraction'>${expressions}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div class='fl pad1y space-right2'>
|
|
194
|
+
<span class="quiet">Default Parameters</span>
|
|
195
|
+
<span class='fraction'>${functionParameters}</span>
|
|
196
|
+
</div>
|
|
197
|
+
</div>`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Calculates file-level statistics
|
|
202
|
+
*/
|
|
203
|
+
export function calculateFileStatistics(
|
|
204
|
+
functions,
|
|
205
|
+
complexityThreshold = 10
|
|
206
|
+
) {
|
|
207
|
+
const totalFunctions = functions.length;
|
|
208
|
+
const withinThreshold = functions.filter((f) =>
|
|
209
|
+
parseInt(f.complexity, 10) <= complexityThreshold
|
|
210
|
+
).length;
|
|
211
|
+
const maxComplexity = functions.length > 0
|
|
212
|
+
? Math.max(...functions.map((f) => parseInt(f.complexity, 10)))
|
|
213
|
+
: 0;
|
|
214
|
+
const avgComplexity = functions.length > 0
|
|
215
|
+
? Math.round(
|
|
216
|
+
functions.reduce((sum, f) => sum + parseInt(f.complexity, 10), 0) /
|
|
217
|
+
functions.length
|
|
218
|
+
)
|
|
219
|
+
: 0;
|
|
220
|
+
const percentage = totalFunctions > 0
|
|
221
|
+
? Math.round((withinThreshold / totalFunctions) * 100)
|
|
222
|
+
: 100;
|
|
223
|
+
const level =
|
|
224
|
+
percentage >= 80
|
|
225
|
+
? 'high'
|
|
226
|
+
: percentage >= 60
|
|
227
|
+
? 'high'
|
|
228
|
+
: percentage >= 40
|
|
229
|
+
? 'medium'
|
|
230
|
+
: 'low';
|
|
231
|
+
return {
|
|
232
|
+
totalFunctions,
|
|
233
|
+
withinThreshold,
|
|
234
|
+
maxComplexity,
|
|
235
|
+
avgComplexity,
|
|
236
|
+
percentage,
|
|
237
|
+
level,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Computes display options for the breakdown section
|
|
243
|
+
*/
|
|
244
|
+
export function getBreakdownSectionOptions(
|
|
245
|
+
totalBreakdownCols,
|
|
246
|
+
hideLinesInitially,
|
|
247
|
+
hideTableInitially,
|
|
248
|
+
initialShowAllColumns,
|
|
249
|
+
hideHighlightsInitially
|
|
250
|
+
) {
|
|
251
|
+
const colspanWithLines = 3 + totalBreakdownCols;
|
|
252
|
+
const colspanWithoutLines = 2 + totalBreakdownCols;
|
|
253
|
+
return {
|
|
254
|
+
colspanWithLines,
|
|
255
|
+
colspanWithoutLines,
|
|
256
|
+
initialColspan: hideLinesInitially ? colspanWithoutLines : colspanWithLines,
|
|
257
|
+
tableClass: hideLinesInitially ? 'complexity-breakdown-table hide-lines' : 'complexity-breakdown-table',
|
|
258
|
+
showAllColumnsChecked: initialShowAllColumns ? 'checked' : '',
|
|
259
|
+
showLinesChecked: hideLinesInitially ? '' : 'checked',
|
|
260
|
+
showTableChecked: hideTableInitially ? '' : 'checked',
|
|
261
|
+
showHighlightsChecked: hideHighlightsInitially ? '' : 'checked',
|
|
262
|
+
tableDisplay: hideTableInitially ? 'none' : 'table',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Generates breakdown section HTML
|
|
268
|
+
*/
|
|
269
|
+
export function generateBreakdownSectionHTML(
|
|
270
|
+
functions,
|
|
271
|
+
initialColumns,
|
|
272
|
+
breakdownItems,
|
|
273
|
+
columnStructure,
|
|
274
|
+
initialShowAllColumns,
|
|
275
|
+
hideTableInitially,
|
|
276
|
+
hideLinesInitially = false,
|
|
277
|
+
hideHighlightsInitially = false
|
|
278
|
+
) {
|
|
279
|
+
if (functions.length === 0) return '';
|
|
280
|
+
const opts = getBreakdownSectionOptions(
|
|
281
|
+
initialColumns.totalBreakdownCols,
|
|
282
|
+
hideLinesInitially,
|
|
283
|
+
hideTableInitially,
|
|
284
|
+
initialShowAllColumns,
|
|
285
|
+
hideHighlightsInitially
|
|
286
|
+
);
|
|
287
|
+
const groupHeadersHTML = initialColumns.visibleGroups.map(group => {
|
|
288
|
+
if (group.columns.length === 0) return '';
|
|
289
|
+
return `<th colspan="${group.columns.length}" class="breakdown-group-header" data-group="${group.name}" data-total-cols="${group.totalColumns}">${group.name}</th>`;
|
|
290
|
+
}).filter(Boolean).join('');
|
|
291
|
+
const colHeadersHTML = initialColumns.visibleGroups.map(group =>
|
|
292
|
+
group.columns.map(col =>
|
|
293
|
+
`<th class="breakdown-col-header sortable" data-column-key="${col.key}" onclick="sortTable('${col.key}')" id="sort-${col.key}-header">${col.label} <span class="sort-indicator" id="sort-${col.key}-indicator"></span></th>`
|
|
294
|
+
).join('')
|
|
295
|
+
).join('');
|
|
296
|
+
return `
|
|
297
|
+
<div class="complexity-breakdown">
|
|
298
|
+
<div class="quiet" style="display: flex; align-items: center; gap: 15px; margin-top: 14px;">
|
|
299
|
+
<div>Filter: <input type="search" id="breakdown-search" oninput="filterFunctions(this.value)"></div>
|
|
300
|
+
<label style="margin: 0; font-weight: normal;"><input type="checkbox" id="breakdown-show-all-columns" onchange="toggleEmptyColumns()" ${opts.showAllColumnsChecked}> Show All Columns</label>
|
|
301
|
+
<label style="margin: 0; font-weight: normal;"><input type="checkbox" id="breakdown-show-lines" onchange="toggleLineColumn()" ${opts.showLinesChecked}> Show Lines</label>
|
|
302
|
+
<label style="margin: 0; font-weight: normal;"><input type="checkbox" id="breakdown-show-table" onchange="toggleTableVisibility()" ${opts.showTableChecked}> Show Table</label>
|
|
303
|
+
<label style="margin: 0; font-weight: normal;"><input type="checkbox" id="breakdown-show-highlights" onchange="toggleHighlights()" ${opts.showHighlightsChecked}> Show Highlights</label>
|
|
304
|
+
</div>
|
|
305
|
+
<table class="${opts.tableClass}" id="complexity-breakdown-table" style="display: ${opts.tableDisplay};" data-colspan-with-lines="${opts.colspanWithLines}" data-colspan-without-lines="${opts.colspanWithoutLines}">
|
|
306
|
+
<thead id="complexity-breakdown-thead">
|
|
307
|
+
<tr><th colspan="${opts.initialColspan}" class="breakdown-header" id="breakdown-header-span">Function Complexity Breakdown</th></tr>
|
|
308
|
+
<tr id="breakdown-group-headers-row">
|
|
309
|
+
<th rowspan="2" class="breakdown-function-header sortable" onclick="sortTable('function')" id="sort-function-header">Function (base = 1) <span class="sort-indicator" id="sort-function-indicator"></span></th>
|
|
310
|
+
<th rowspan="2" class="breakdown-line-header breakdown-line-column sortable" onclick="sortTable('line')" id="sort-line-header">Line <span class="sort-indicator" id="sort-line-indicator"></span></th>
|
|
311
|
+
<th rowspan="2" class="breakdown-complexity-header sortable" onclick="sortTable('complexity')" id="sort-complexity-header">Complexity <span class="sort-indicator" id="sort-complexity-indicator"></span></th>
|
|
312
|
+
${groupHeadersHTML}
|
|
313
|
+
</tr>
|
|
314
|
+
<tr id="breakdown-col-headers-row">${colHeadersHTML}</tr>
|
|
315
|
+
</thead>
|
|
316
|
+
<tbody id="complexity-breakdown-tbody">
|
|
317
|
+
${breakdownItems}
|
|
318
|
+
<tr id="no-matches-row" style="display: none;"><td colspan="${opts.initialColspan}" class="no-matches-message">No functions match the current filter.</td></tr>
|
|
319
|
+
</tbody>
|
|
320
|
+
</table>
|
|
321
|
+
</div>
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generates statistics HTML section
|
|
327
|
+
*/
|
|
328
|
+
export function _generateStatisticsHTML(
|
|
329
|
+
totalFunctions,
|
|
330
|
+
withinThreshold,
|
|
331
|
+
maxComplexity,
|
|
332
|
+
avgComplexity
|
|
333
|
+
) {
|
|
334
|
+
const maxComplexityHTML = maxComplexity > 0
|
|
335
|
+
? `
|
|
336
|
+
<div class='fl pad1y space-right2'>
|
|
337
|
+
<span class="strong">${maxComplexity} </span>
|
|
338
|
+
<span class="quiet">Max Complexity</span>
|
|
339
|
+
</div>
|
|
340
|
+
<div class='fl pad1y space-right2'>
|
|
341
|
+
<span class="strong">${avgComplexity} </span>
|
|
342
|
+
<span class="quiet">Avg Complexity</span>
|
|
343
|
+
</div>`
|
|
344
|
+
: '';
|
|
345
|
+
return maxComplexityHTML;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Prepares file-level data for HTML generation
|
|
350
|
+
*/
|
|
351
|
+
export function prepareFileLevelData(
|
|
352
|
+
functions,
|
|
353
|
+
functionBreakdowns,
|
|
354
|
+
complexityThreshold
|
|
355
|
+
) {
|
|
356
|
+
const stats = calculateFileStatistics(functions, complexityThreshold);
|
|
357
|
+
const decisionPointTotals =
|
|
358
|
+
calculateDecisionPointTotalsFromBreakdowns(functionBreakdowns);
|
|
359
|
+
const withinThresholdPercentage = stats.totalFunctions > 0
|
|
360
|
+
? Math.round((stats.withinThreshold / stats.totalFunctions) * 100)
|
|
361
|
+
: 100;
|
|
362
|
+
return { ...stats, decisionPointTotals, withinThresholdPercentage };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Prepares breakdown column structure
|
|
367
|
+
*/
|
|
368
|
+
export function prepareBreakdownColumns(
|
|
369
|
+
functions,
|
|
370
|
+
functionBreakdowns,
|
|
371
|
+
showAllColumnsInitially,
|
|
372
|
+
variant = 'classic'
|
|
373
|
+
) {
|
|
374
|
+
const columnStructure = getBreakdownColumnStructure(variant);
|
|
375
|
+
const emptyColumns = detectEmptyColumns(
|
|
376
|
+
functions,
|
|
377
|
+
functionBreakdowns,
|
|
378
|
+
columnStructure
|
|
379
|
+
);
|
|
380
|
+
const initialColumns = buildVisibleColumns(
|
|
381
|
+
columnStructure,
|
|
382
|
+
emptyColumns,
|
|
383
|
+
showAllColumnsInitially
|
|
384
|
+
);
|
|
385
|
+
return { columnStructure, emptyColumns, initialColumns };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Builds visible column structure
|
|
390
|
+
*/
|
|
391
|
+
export function buildVisibleColumns(
|
|
392
|
+
columnStructure,
|
|
393
|
+
emptyColumns,
|
|
394
|
+
showAll
|
|
395
|
+
) {
|
|
396
|
+
const visibleGroups = columnStructure.groups.map((group) => {
|
|
397
|
+
const visibleColumns = showAll
|
|
398
|
+
? group.columns
|
|
399
|
+
: group.columns.filter((col) => !emptyColumns.has(col.key));
|
|
400
|
+
return {
|
|
401
|
+
name: group.name,
|
|
402
|
+
columns: visibleColumns,
|
|
403
|
+
totalColumns: group.columns.length,
|
|
404
|
+
visibleColumns: visibleColumns.length,
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
const totalBreakdownCols = visibleGroups.reduce(
|
|
408
|
+
(sum, group) => sum + group.columns.length,
|
|
409
|
+
0
|
|
410
|
+
);
|
|
411
|
+
return { visibleGroups, totalBreakdownCols };
|
|
412
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data maps for file page (line-to-function, line-to-decision-point)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates line-to-function map
|
|
7
|
+
* @param {Array} functions - Functions array
|
|
8
|
+
* @returns {Map} Line to function map
|
|
9
|
+
*/
|
|
10
|
+
export function createLineToFunctionMap(functions) {
|
|
11
|
+
const lineToFunction = new Map();
|
|
12
|
+
functions.forEach(func => {
|
|
13
|
+
lineToFunction.set(func.line, func);
|
|
14
|
+
});
|
|
15
|
+
return lineToFunction;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates decision point line map for highlighting.
|
|
20
|
+
* Expands multi-line DPs: when dp.lines exists, adds one entry per
|
|
21
|
+
* line with that line's column range.
|
|
22
|
+
* @param {Array} decisionPoints - Decision points array
|
|
23
|
+
* @returns {Map} Line to decision points map
|
|
24
|
+
*/
|
|
25
|
+
export function createDecisionPointLineMap(decisionPoints) {
|
|
26
|
+
const lineToDecisionPoint = new Map();
|
|
27
|
+
decisionPoints.forEach(dp => {
|
|
28
|
+
if (dp.lines && Array.isArray(dp.lines)) {
|
|
29
|
+
dp.lines.forEach(({ line: lineNum, column, endColumn }) => {
|
|
30
|
+
if (!lineToDecisionPoint.has(lineNum)) {
|
|
31
|
+
lineToDecisionPoint.set(lineNum, []);
|
|
32
|
+
}
|
|
33
|
+
lineToDecisionPoint.get(lineNum).push({
|
|
34
|
+
type: dp.type,
|
|
35
|
+
line: lineNum,
|
|
36
|
+
functionLine: dp.functionLine,
|
|
37
|
+
name: dp.name,
|
|
38
|
+
column,
|
|
39
|
+
endColumn,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
if (!lineToDecisionPoint.has(dp.line)) {
|
|
44
|
+
lineToDecisionPoint.set(dp.line, []);
|
|
45
|
+
}
|
|
46
|
+
lineToDecisionPoint.get(dp.line).push(dp);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return lineToDecisionPoint;
|
|
50
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility helpers for file page generation
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detects language from file extension for syntax highlighting
|
|
8
|
+
* @param {string} filePath - File path
|
|
9
|
+
* @returns {string} Language class for prettify (e.g., 'lang-js', 'lang-ts')
|
|
10
|
+
*/
|
|
11
|
+
export function detectLanguage(filePath) {
|
|
12
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
13
|
+
const langMap = {
|
|
14
|
+
'js': 'lang-js',
|
|
15
|
+
'jsx': 'lang-js',
|
|
16
|
+
'ts': 'lang-ts',
|
|
17
|
+
'tsx': 'lang-ts',
|
|
18
|
+
'css': 'lang-css',
|
|
19
|
+
'html': 'lang-html',
|
|
20
|
+
'json': 'lang-json',
|
|
21
|
+
'md': 'lang-md',
|
|
22
|
+
'py': 'lang-py',
|
|
23
|
+
'java': 'lang-java',
|
|
24
|
+
'c': 'lang-c',
|
|
25
|
+
'cpp': 'lang-cpp',
|
|
26
|
+
'cs': 'lang-cs',
|
|
27
|
+
'rb': 'lang-rb',
|
|
28
|
+
'php': 'lang-php',
|
|
29
|
+
'go': 'lang-go',
|
|
30
|
+
'rs': 'lang-rs',
|
|
31
|
+
'sh': 'lang-sh',
|
|
32
|
+
'sql': 'lang-sql',
|
|
33
|
+
};
|
|
34
|
+
return langMap[ext] || 'lang-js';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calculates relative path to prettify files based on directory depth
|
|
39
|
+
* @param {string} filePath - Relative file path (e.g., 'src/components/Button.tsx')
|
|
40
|
+
* @returns {string} Relative path to prettify files (e.g., '../../prettify.css')
|
|
41
|
+
*/
|
|
42
|
+
export function getPrettifyRelativePath(filePath) {
|
|
43
|
+
const fileDir = filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : '';
|
|
44
|
+
if (!fileDir) return '';
|
|
45
|
+
const depth = fileDir.split('/').length;
|
|
46
|
+
return '../'.repeat(depth);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns indent length in character units (tabs expanded to 2 spaces).
|
|
51
|
+
* @param {string} line - Source line
|
|
52
|
+
* @returns {number} Indent in characters
|
|
53
|
+
*/
|
|
54
|
+
export function getIndentChars(line) {
|
|
55
|
+
const m = line && line.match(/^(\s*)/);
|
|
56
|
+
if (!m) return 0;
|
|
57
|
+
return m[1].replace(/\t/g, ' ').length;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reads source file and returns code and lines
|
|
62
|
+
* @param {string} fullPath - Absolute path to file
|
|
63
|
+
* @param {string} filePath - Relative path for error messages
|
|
64
|
+
* @returns {{ sourceCode: string, sourceLines: string[] }}
|
|
65
|
+
*/
|
|
66
|
+
export function readSourceFile(fullPath, filePath) {
|
|
67
|
+
let sourceCode = '';
|
|
68
|
+
let sourceLines = [];
|
|
69
|
+
try {
|
|
70
|
+
if (existsSync(fullPath)) {
|
|
71
|
+
sourceCode = readFileSync(fullPath, 'utf-8');
|
|
72
|
+
sourceLines = sourceCode.split('\n');
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.warn(`Warning: Could not read source file ${filePath}:`, error.message);
|
|
76
|
+
}
|
|
77
|
+
return { sourceCode, sourceLines };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns paths for file page assets
|
|
82
|
+
* @param {string} filePath - Relative file path
|
|
83
|
+
* @param {string} fileDir - Directory part of file path
|
|
84
|
+
* @returns {Object} Paths: backLink, folderIndexPath, aboutPath,
|
|
85
|
+
* prettifyCssPath, prettifyJsPath, sharedCssPath, fileCssPath
|
|
86
|
+
*/
|
|
87
|
+
export function getFilePagePaths(filePath, fileDir) {
|
|
88
|
+
const depth = fileDir ? fileDir.split('/').length : 0;
|
|
89
|
+
const prefix = depth > 0 ? '../'.repeat(depth) : '';
|
|
90
|
+
const prettifyPath = getPrettifyRelativePath(filePath);
|
|
91
|
+
return {
|
|
92
|
+
backLink: prefix ? `${prefix}index.html` : 'index.html',
|
|
93
|
+
folderIndexPath: fileDir ? 'index.html' : 'index.html',
|
|
94
|
+
aboutPath: prefix ? `${prefix}about.html` : 'about.html',
|
|
95
|
+
prettifyCssPath: prettifyPath ? `${prettifyPath}prettify.css` : 'prettify.css',
|
|
96
|
+
prettifyJsPath: prettifyPath ? `${prettifyPath}prettify.js` : 'prettify.js',
|
|
97
|
+
sharedCssPath: prettifyPath ? `${prettifyPath}shared.css` : 'shared.css',
|
|
98
|
+
fileCssPath: prettifyPath ? `${prettifyPath}file.css` : 'file.css',
|
|
99
|
+
};
|
|
100
|
+
}
|