@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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-line ternary handling and decision point entry building.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getNodeLineRange } from './node-helpers.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns whether the node is a multi-line ConditionalExpression.
|
|
9
|
+
* @param {Object} node - AST node
|
|
10
|
+
* @param {{ startLine: number, endLine: number }} lineRange - Line range
|
|
11
|
+
* @param {Object|null} columnRange - Column range or null
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export function isMultiLineTernaryNode(node, lineRange, columnRange) {
|
|
15
|
+
return lineRange.startLine !== lineRange.endLine &&
|
|
16
|
+
Boolean(columnRange) &&
|
|
17
|
+
node.type === 'ConditionalExpression';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds line ranges for a multi-line node using AST loc data.
|
|
22
|
+
* @param {number} startLine - Start line (1-based)
|
|
23
|
+
* @param {number} endLine - End line (1-based)
|
|
24
|
+
* @param {Object} nodeLoc - AST node.loc object with start/end
|
|
25
|
+
* @param {string[]} lines - Source lines
|
|
26
|
+
* @returns {Array<{ line: number, column: number, endColumn: number }>}
|
|
27
|
+
*/
|
|
28
|
+
export function buildMultiLineTernaryLineRanges(
|
|
29
|
+
startLine,
|
|
30
|
+
endLine,
|
|
31
|
+
nodeLoc,
|
|
32
|
+
lines
|
|
33
|
+
) {
|
|
34
|
+
const lineRanges = [];
|
|
35
|
+
|
|
36
|
+
// Only highlight the first line using AST start column to end of line
|
|
37
|
+
const lineLength = lines[startLine - 1]?.length ?? 0;
|
|
38
|
+
lineRanges.push({
|
|
39
|
+
line: startLine,
|
|
40
|
+
column: nodeLoc.start.column,
|
|
41
|
+
endColumn: lineLength,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return lineRanges;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Builds one or more decision point entries for a node.
|
|
49
|
+
* Multi-line nodes get one entry with lines array (one per line).
|
|
50
|
+
* @param {Object} node - AST node
|
|
51
|
+
* @param {string} decisionType - Type string
|
|
52
|
+
* @param {number} functionLine - Function line
|
|
53
|
+
* @param {number} line - Report line
|
|
54
|
+
* @param {Object|null} columnRange - Column range or null
|
|
55
|
+
* @param {string[]} lines - Source code lines
|
|
56
|
+
* @returns {Array<Object>} Array of decision point objects to push
|
|
57
|
+
*/
|
|
58
|
+
export function buildDecisionPointEntries(
|
|
59
|
+
node,
|
|
60
|
+
decisionType,
|
|
61
|
+
functionLine,
|
|
62
|
+
line,
|
|
63
|
+
columnRange,
|
|
64
|
+
lines
|
|
65
|
+
) {
|
|
66
|
+
const lineRange = getNodeLineRange(node);
|
|
67
|
+
|
|
68
|
+
// Handle multi-line nodes (ternary, catch, if, etc.)
|
|
69
|
+
if (lineRange && lineRange.startLine !== lineRange.endLine) {
|
|
70
|
+
// Multi-line node: create ranges for each line based on AST loc
|
|
71
|
+
const lineRanges = buildMultiLineTernaryLineRanges(
|
|
72
|
+
lineRange.startLine,
|
|
73
|
+
lineRange.endLine,
|
|
74
|
+
node.loc,
|
|
75
|
+
lines
|
|
76
|
+
);
|
|
77
|
+
return [{ type: decisionType, line, functionLine, name: null, lines: lineRanges }];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Single-line node: use column range from AST
|
|
81
|
+
const base = { type: decisionType, line, functionLine, name: null };
|
|
82
|
+
const withCol = columnRange
|
|
83
|
+
? { ...base, column: columnRange.column, endColumn: columnRange.endColumn }
|
|
84
|
+
: base;
|
|
85
|
+
return [withCol];
|
|
86
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { getBaseFunctionName } from '../function-extraction/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a function name has a callback/nested suffix
|
|
5
|
+
* (e.g., "functionA (useEffect)" or "functionA → useEffect")
|
|
6
|
+
* This is used to identify functions that should be deduplicated
|
|
7
|
+
* @param {string} functionName - Function name to check
|
|
8
|
+
* @returns {boolean} True if the name has a callback suffix
|
|
9
|
+
*/
|
|
10
|
+
function hasCallbackSuffix(functionName) {
|
|
11
|
+
if (!functionName) return false;
|
|
12
|
+
return /\s*\([^)]+\)\s*$/.test(functionName) || /\s*→\s*[^→]+$/.test(functionName);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Finds the immediate parent function for a callback using function boundaries
|
|
17
|
+
* @param {Object} func - Function object
|
|
18
|
+
* @param {Map} functionBoundaries - Map of function boundaries (file-specific)
|
|
19
|
+
* @param {Array} fileFunctions - Functions in the same file
|
|
20
|
+
* @returns {Object|null} Parent function or null
|
|
21
|
+
*/
|
|
22
|
+
function findImmediateParentFunction(func, functionBoundaries, fileFunctions) {
|
|
23
|
+
const funcBoundary = functionBoundaries.get(func.line);
|
|
24
|
+
if (!funcBoundary) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Find all functions that contain this function (only within the same file)
|
|
29
|
+
const containingFunctions = Array.from(functionBoundaries.entries())
|
|
30
|
+
.filter(([fl, boundary]) => {
|
|
31
|
+
if (fl === func.line) return false;
|
|
32
|
+
// Function must be in the same file
|
|
33
|
+
const funcObj = fileFunctions.find(f => f.line === fl);
|
|
34
|
+
if (!funcObj) return false;
|
|
35
|
+
// Function must contain this callback
|
|
36
|
+
return boundary.start < funcBoundary.start && boundary.end > funcBoundary.end;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (containingFunctions.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find the smallest containing function (immediate parent)
|
|
44
|
+
// Sort by size (end - start) ascending, then by start descending
|
|
45
|
+
containingFunctions.sort((a, b) => {
|
|
46
|
+
const sizeA = a[1].end - a[1].start;
|
|
47
|
+
const sizeB = b[1].end - b[1].start;
|
|
48
|
+
if (sizeA !== sizeB) {
|
|
49
|
+
// Smaller size first (more immediate parent)
|
|
50
|
+
return sizeA - sizeB;
|
|
51
|
+
}
|
|
52
|
+
// If same size, prefer later start (more nested)
|
|
53
|
+
return b[1].start - a[1].start;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Get the immediate parent (smallest containing function)
|
|
57
|
+
const immediateParentLine = containingFunctions[0][0];
|
|
58
|
+
return fileFunctions.find(f => f.line === immediateParentLine) || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the rightmost/leaf segment of a display name.
|
|
63
|
+
* Framework-agnostic - matches function-hierarchy.js logic.
|
|
64
|
+
* @param {string} displayName - Full or hierarchical display name
|
|
65
|
+
* @returns {string} Leaf segment: last parenthetical content or whole
|
|
66
|
+
*/
|
|
67
|
+
function getLeafName(displayName) {
|
|
68
|
+
if (!displayName || typeof displayName !== 'string') return displayName || '';
|
|
69
|
+
const match = displayName.match(/\(([^)]+)\)\s*$/);
|
|
70
|
+
return match ? match[1] : displayName;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Checks if a name is valid for use in hierarchical display (not a placeholder).
|
|
75
|
+
* @param {string} name - Leaf or base name
|
|
76
|
+
* @returns {boolean} True if valid
|
|
77
|
+
*/
|
|
78
|
+
function isValidNameForHierarchy(name) {
|
|
79
|
+
return name && name !== 'unknown' && name !== 'anonymous';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Builds a full hierarchical function name by recursively finding parents
|
|
84
|
+
* Uses file-specific boundaries (like HTML report) for accurate detection
|
|
85
|
+
* @param {Object} func - Function object
|
|
86
|
+
* @param {Map} fileBoundaries - Map of function boundaries for this file
|
|
87
|
+
* @param {Array} fileFunctions - All functions in the same file
|
|
88
|
+
* @param {Set} visited - Set to track visited (prevents infinite loops)
|
|
89
|
+
* @returns {string} Full hierarchical name
|
|
90
|
+
*/
|
|
91
|
+
export function buildHierarchicalFunctionName(
|
|
92
|
+
func,
|
|
93
|
+
fileBoundaries,
|
|
94
|
+
fileFunctions,
|
|
95
|
+
visited = new Set()
|
|
96
|
+
) {
|
|
97
|
+
let displayName = func.functionName || 'unknown';
|
|
98
|
+
|
|
99
|
+
// Prevent infinite loops
|
|
100
|
+
const funcKey = `${func.file}:${func.line}`;
|
|
101
|
+
if (visited.has(funcKey)) {
|
|
102
|
+
return displayName;
|
|
103
|
+
}
|
|
104
|
+
visited.add(funcKey);
|
|
105
|
+
|
|
106
|
+
if (!fileBoundaries || !fileFunctions) {
|
|
107
|
+
return displayName;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Find the actual immediate parent using boundaries
|
|
111
|
+
// (regardless of what the name says)
|
|
112
|
+
const immediateParentFunc = findImmediateParentFunction(
|
|
113
|
+
func,
|
|
114
|
+
fileBoundaries,
|
|
115
|
+
fileFunctions
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!immediateParentFunc) {
|
|
119
|
+
// No parent found, return name as-is
|
|
120
|
+
return displayName;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parentHierarchicalName = buildHierarchicalFunctionName(
|
|
124
|
+
immediateParentFunc,
|
|
125
|
+
fileBoundaries,
|
|
126
|
+
fileFunctions,
|
|
127
|
+
new Set(visited)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const parentBaseName = getBaseFunctionName(parentHierarchicalName);
|
|
131
|
+
if (!isValidNameForHierarchy(parentBaseName)) {
|
|
132
|
+
return displayName;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Framework-agnostic: use whatever extraction gave us
|
|
136
|
+
// (method, hook, handler, variable name).
|
|
137
|
+
const leafName = getLeafName(displayName);
|
|
138
|
+
if (isValidNameForHierarchy(leafName)) {
|
|
139
|
+
return `${parentHierarchicalName} → ${leafName}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return displayName;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Checks if a function is nested within another function
|
|
147
|
+
* @param {Object} func - Function to check
|
|
148
|
+
* @param {Object} funcBoundary - Function's boundary
|
|
149
|
+
* @param {Array} allFunctions - All functions array
|
|
150
|
+
* @param {Map} functionBoundaries - Map of function boundaries
|
|
151
|
+
* @returns {boolean} True if function is nested
|
|
152
|
+
*/
|
|
153
|
+
function isFunctionNested(func, funcBoundary, allFunctions, functionBoundaries) {
|
|
154
|
+
for (const otherFunc of allFunctions) {
|
|
155
|
+
if (otherFunc.line === func.line) continue;
|
|
156
|
+
|
|
157
|
+
// Only check nesting within the same file
|
|
158
|
+
if (otherFunc.file !== func.file) continue;
|
|
159
|
+
|
|
160
|
+
const otherBoundary = functionBoundaries.get(otherFunc.line);
|
|
161
|
+
if (!otherBoundary) continue;
|
|
162
|
+
|
|
163
|
+
// If otherFunc contains this func, it's nested
|
|
164
|
+
// Use strict < and > to ensure it's actually nested (not overlapping)
|
|
165
|
+
if (
|
|
166
|
+
otherBoundary.start < funcBoundary.start &&
|
|
167
|
+
otherBoundary.end > funcBoundary.end
|
|
168
|
+
) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Adds a function to top-level list with deduplication (no boundary case)
|
|
177
|
+
* @param {Object} func - Function object
|
|
178
|
+
* @param {string} functionName - Function name
|
|
179
|
+
* @param {Array} topLevel - Top-level functions array
|
|
180
|
+
* @param {Set} seenBaseNames - Set of seen base names
|
|
181
|
+
*/
|
|
182
|
+
function addTopLevelFunctionNoBoundary(func, functionName, topLevel, seenBaseNames) {
|
|
183
|
+
const baseName = getBaseFunctionName(functionName);
|
|
184
|
+
const key = `${func.file}:${baseName}`;
|
|
185
|
+
|
|
186
|
+
// Only deduplicate if this function has a callback suffix
|
|
187
|
+
// Standalone names like "useCallback callback" or "anonymous" don't have suffixes
|
|
188
|
+
if (hasCallbackSuffix(functionName)) {
|
|
189
|
+
// This is a callback variant - only include if we haven't seen the base name
|
|
190
|
+
if (!seenBaseNames.has(key)) {
|
|
191
|
+
seenBaseNames.add(key);
|
|
192
|
+
// Create a new function object with the base name for display
|
|
193
|
+
topLevel.push({
|
|
194
|
+
...func,
|
|
195
|
+
functionName: baseName,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// Standalone name - deduplicate by exact name + file to avoid duplicates
|
|
200
|
+
const exactKey = `${func.file}:${functionName}`;
|
|
201
|
+
if (!seenBaseNames.has(exactKey)) {
|
|
202
|
+
seenBaseNames.add(exactKey);
|
|
203
|
+
// Also mark the base name as seen to prevent callback variants from being added
|
|
204
|
+
seenBaseNames.add(key);
|
|
205
|
+
topLevel.push(func);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Adds a function to top-level list with deduplication (has boundary case)
|
|
212
|
+
* @param {Object} func - Function object
|
|
213
|
+
* @param {string} functionName - Function name
|
|
214
|
+
* @param {Array} topLevel - Top-level functions array
|
|
215
|
+
* @param {Set} seenBaseNames - Set of seen base names
|
|
216
|
+
*/
|
|
217
|
+
function addTopLevelFunctionWithBoundary(func, functionName, topLevel, seenBaseNames) {
|
|
218
|
+
const baseName = getBaseFunctionName(functionName);
|
|
219
|
+
const key = `${func.file}:${baseName}`;
|
|
220
|
+
|
|
221
|
+
// Only deduplicate if this function has a callback suffix
|
|
222
|
+
if (hasCallbackSuffix(functionName)) {
|
|
223
|
+
// This is a callback variant - only include if we haven't seen the base name
|
|
224
|
+
if (!seenBaseNames.has(key)) {
|
|
225
|
+
seenBaseNames.add(key);
|
|
226
|
+
// Create a new function object with the base name for display
|
|
227
|
+
topLevel.push({
|
|
228
|
+
...func,
|
|
229
|
+
functionName: baseName,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
// Standalone name or base function - deduplicate by exact name + file
|
|
234
|
+
const exactKey = `${func.file}:${functionName}`;
|
|
235
|
+
if (!seenBaseNames.has(exactKey)) {
|
|
236
|
+
seenBaseNames.add(exactKey);
|
|
237
|
+
// Also mark the base name as seen to prevent callback variants from being added
|
|
238
|
+
seenBaseNames.add(key);
|
|
239
|
+
topLevel.push(func);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Gets top-level functions only (no nested functions)
|
|
246
|
+
* Deduplicates by base function name so "functionA" and
|
|
247
|
+
* "functionA (useEffect callback)" only show "functionA" once.
|
|
248
|
+
* Standalone names like "useCallback callback" and "anonymous"
|
|
249
|
+
* are kept as-is.
|
|
250
|
+
* @param {Array} allFunctions - All functions array
|
|
251
|
+
* @param {Map} functionBoundaries - Map of function boundaries
|
|
252
|
+
* @returns {Array} Top-level functions only, deduplicated
|
|
253
|
+
*/
|
|
254
|
+
export function getTopLevelFunctions(allFunctions, functionBoundaries) {
|
|
255
|
+
const topLevel = [];
|
|
256
|
+
// Track seen base names to deduplicate
|
|
257
|
+
const seenBaseNames = new Set();
|
|
258
|
+
|
|
259
|
+
for (const func of allFunctions) {
|
|
260
|
+
const functionName = func.functionName || 'unknown';
|
|
261
|
+
const funcBoundary = functionBoundaries.get(func.line);
|
|
262
|
+
|
|
263
|
+
// If no boundary found, assume top-level (boundary detection failed)
|
|
264
|
+
if (!funcBoundary) {
|
|
265
|
+
addTopLevelFunctionNoBoundary(func, functionName, topLevel, seenBaseNames);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check if this function is contained by any other function
|
|
270
|
+
const isNested = isFunctionNested(
|
|
271
|
+
func,
|
|
272
|
+
funcBoundary,
|
|
273
|
+
allFunctions,
|
|
274
|
+
functionBoundaries
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (!isNested) {
|
|
278
|
+
addTopLevelFunctionWithBoundary(func, functionName, topLevel, seenBaseNames);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return topLevel;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Groups functions by folder/file structure
|
|
287
|
+
* @param {Array} functions - Functions to group
|
|
288
|
+
* @param {Function} getDirectory - Function to get directory from file path
|
|
289
|
+
* @returns {Map} Map of directory -> file -> functions
|
|
290
|
+
*/
|
|
291
|
+
export function groupFunctionsByFolder(functions, getDirectory) {
|
|
292
|
+
const folderMap = new Map();
|
|
293
|
+
|
|
294
|
+
for (const func of functions) {
|
|
295
|
+
const dir = getDirectory(func.file);
|
|
296
|
+
if (!folderMap.has(dir)) {
|
|
297
|
+
folderMap.set(dir, new Map());
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const fileMap = folderMap.get(dir);
|
|
301
|
+
if (!fileMap.has(func.file)) {
|
|
302
|
+
fileMap.set(func.file, []);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fileMap.get(func.file).push(func);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return folderMap;
|
|
309
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
generateAllFunctionsTXT,
|
|
5
|
+
generateAllFunctionsLeafOnlyTXT,
|
|
6
|
+
generateFunctionsByFolderTXT,
|
|
7
|
+
generateFunctionsByFolderLeafOnlyTXT,
|
|
8
|
+
generateFileNamesAlphabeticalTXT,
|
|
9
|
+
} from './txt-exports.js';
|
|
10
|
+
import {
|
|
11
|
+
generateAllFunctionsMD,
|
|
12
|
+
generateFunctionsByFolderMD,
|
|
13
|
+
generateFileNamesAlphabeticalMD,
|
|
14
|
+
} from './md-exports.js';
|
|
15
|
+
import { findFunctionBoundaries } from '../function-boundaries/index.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates all export files (TXT, MD) for complexity report
|
|
19
|
+
* @param {Array} allFunctions - All functions array
|
|
20
|
+
* @param {string} projectRoot - Project root directory
|
|
21
|
+
* @param {string} exportDir - Directory to write export files to
|
|
22
|
+
* @returns {Object} Object with generated file paths
|
|
23
|
+
*/
|
|
24
|
+
export function generateAllExports(allFunctions, projectRoot, exportDir) {
|
|
25
|
+
// Create export directory if it doesn't exist
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(exportDir, { recursive: true });
|
|
28
|
+
} catch {
|
|
29
|
+
// Directory might already exist, ignore
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const generatedFiles = [];
|
|
33
|
+
|
|
34
|
+
// Build file-specific boundaries map (like HTML report does)
|
|
35
|
+
// Map of filePath -> Map of line -> boundary
|
|
36
|
+
const fileBoundariesMap = new Map();
|
|
37
|
+
const fileToFunctions = new Map();
|
|
38
|
+
|
|
39
|
+
// Group functions by file
|
|
40
|
+
allFunctions.forEach(func => {
|
|
41
|
+
if (!fileToFunctions.has(func.file)) {
|
|
42
|
+
fileToFunctions.set(func.file, []);
|
|
43
|
+
}
|
|
44
|
+
fileToFunctions.get(func.file).push(func);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Build boundaries for each file
|
|
48
|
+
for (const [filePath, functions] of fileToFunctions.entries()) {
|
|
49
|
+
const fullPath = resolve(projectRoot, filePath);
|
|
50
|
+
if (!existsSync(fullPath)) continue;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const sourceCode = readFileSync(fullPath, 'utf-8');
|
|
54
|
+
const boundaries = findFunctionBoundaries(sourceCode, functions);
|
|
55
|
+
fileBoundariesMap.set(filePath, boundaries);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn(`Warning: Could not process ${filePath} for export boundaries:`, error.message);
|
|
58
|
+
// Create empty boundaries map for this file
|
|
59
|
+
fileBoundariesMap.set(filePath, new Map());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Generate all functions (including callbacks) exports
|
|
64
|
+
const allFunctionsTXT = generateAllFunctionsTXT(
|
|
65
|
+
allFunctions,
|
|
66
|
+
fileBoundariesMap,
|
|
67
|
+
fileToFunctions
|
|
68
|
+
);
|
|
69
|
+
const allFunctionsTXTPath = resolve(exportDir, 'function-names.all.txt');
|
|
70
|
+
writeFileSync(allFunctionsTXTPath, allFunctionsTXT, 'utf-8');
|
|
71
|
+
generatedFiles.push(allFunctionsTXTPath);
|
|
72
|
+
|
|
73
|
+
const allFunctionsLeafOnlyTXT = generateAllFunctionsLeafOnlyTXT(
|
|
74
|
+
allFunctions,
|
|
75
|
+
fileBoundariesMap,
|
|
76
|
+
fileToFunctions
|
|
77
|
+
);
|
|
78
|
+
const allFunctionsLeafOnlyTXTPath = resolve(
|
|
79
|
+
exportDir,
|
|
80
|
+
'function-names.all-leaf.txt'
|
|
81
|
+
);
|
|
82
|
+
writeFileSync(
|
|
83
|
+
allFunctionsLeafOnlyTXTPath,
|
|
84
|
+
allFunctionsLeafOnlyTXT,
|
|
85
|
+
'utf-8'
|
|
86
|
+
);
|
|
87
|
+
generatedFiles.push(allFunctionsLeafOnlyTXTPath);
|
|
88
|
+
|
|
89
|
+
const allFunctionsMD = generateAllFunctionsMD(
|
|
90
|
+
allFunctions,
|
|
91
|
+
fileBoundariesMap,
|
|
92
|
+
fileToFunctions
|
|
93
|
+
);
|
|
94
|
+
const allFunctionsMDPath = resolve(exportDir, 'function-names.all.md');
|
|
95
|
+
writeFileSync(allFunctionsMDPath, allFunctionsMD, 'utf-8');
|
|
96
|
+
generatedFiles.push(allFunctionsMDPath);
|
|
97
|
+
|
|
98
|
+
// Generate functions by folder exports
|
|
99
|
+
const byFolderTXT = generateFunctionsByFolderTXT(
|
|
100
|
+
allFunctions,
|
|
101
|
+
fileBoundariesMap,
|
|
102
|
+
fileToFunctions
|
|
103
|
+
);
|
|
104
|
+
const byFolderTXTPath = resolve(exportDir, 'function-names-by-file.txt');
|
|
105
|
+
writeFileSync(byFolderTXTPath, byFolderTXT, 'utf-8');
|
|
106
|
+
generatedFiles.push(byFolderTXTPath);
|
|
107
|
+
|
|
108
|
+
const byFolderLeafOnlyTXT = generateFunctionsByFolderLeafOnlyTXT(
|
|
109
|
+
allFunctions,
|
|
110
|
+
fileBoundariesMap,
|
|
111
|
+
fileToFunctions
|
|
112
|
+
);
|
|
113
|
+
const byFolderLeafOnlyTXTPath = resolve(
|
|
114
|
+
exportDir,
|
|
115
|
+
'function-names-by-file-leaf.txt'
|
|
116
|
+
);
|
|
117
|
+
writeFileSync(byFolderLeafOnlyTXTPath, byFolderLeafOnlyTXT, 'utf-8');
|
|
118
|
+
generatedFiles.push(byFolderLeafOnlyTXTPath);
|
|
119
|
+
|
|
120
|
+
const byFolderMD = generateFunctionsByFolderMD(
|
|
121
|
+
allFunctions,
|
|
122
|
+
fileBoundariesMap,
|
|
123
|
+
fileToFunctions
|
|
124
|
+
);
|
|
125
|
+
const byFolderMDPath = resolve(exportDir, 'function-names-by-file.md');
|
|
126
|
+
writeFileSync(byFolderMDPath, byFolderMD, 'utf-8');
|
|
127
|
+
generatedFiles.push(byFolderMDPath);
|
|
128
|
+
|
|
129
|
+
const fileNamesAlphabeticalTXT = generateFileNamesAlphabeticalTXT(fileToFunctions);
|
|
130
|
+
const fileNamesAlphabeticalTXTPath = resolve(exportDir, 'file-names-alphabetical.txt');
|
|
131
|
+
writeFileSync(fileNamesAlphabeticalTXTPath, fileNamesAlphabeticalTXT, 'utf-8');
|
|
132
|
+
generatedFiles.push(fileNamesAlphabeticalTXTPath);
|
|
133
|
+
|
|
134
|
+
const fileNamesAlphabeticalMD = generateFileNamesAlphabeticalMD(fileToFunctions);
|
|
135
|
+
const fileNamesAlphabeticalMDPath = resolve(exportDir, 'file-names-alphabetical.md');
|
|
136
|
+
writeFileSync(fileNamesAlphabeticalMDPath, fileNamesAlphabeticalMD, 'utf-8');
|
|
137
|
+
generatedFiles.push(fileNamesAlphabeticalMDPath);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
generatedFiles,
|
|
141
|
+
exportDir,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { buildHierarchicalFunctionName, groupFunctionsByFolder } from './helpers.js';
|
|
2
|
+
import { getDirectory } from '../function-extraction/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates Markdown export for all functions including callbacks (alphabetically)
|
|
6
|
+
* @param {Array} allFunctions - All functions array
|
|
7
|
+
* @param {Map} fileBoundariesMap - Map of filePath -> Map of line -> boundary
|
|
8
|
+
* @param {Map} fileToFunctions - Map of filePath -> functions array
|
|
9
|
+
* @returns {string} Markdown string
|
|
10
|
+
*/
|
|
11
|
+
export function generateAllFunctionsMD(
|
|
12
|
+
allFunctions,
|
|
13
|
+
fileBoundariesMap,
|
|
14
|
+
fileToFunctions
|
|
15
|
+
) {
|
|
16
|
+
// Build hierarchical names for all functions using file-specific boundaries
|
|
17
|
+
const functionsWithHierarchy = allFunctions.map(func => {
|
|
18
|
+
const fileBoundaries = fileBoundariesMap.get(func.file) || new Map();
|
|
19
|
+
const fileFunctions = fileToFunctions.get(func.file) || [];
|
|
20
|
+
return {
|
|
21
|
+
...func,
|
|
22
|
+
hierarchicalName: buildHierarchicalFunctionName(
|
|
23
|
+
func,
|
|
24
|
+
fileBoundaries,
|
|
25
|
+
fileFunctions
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Sort alphabetically by hierarchical name
|
|
31
|
+
functionsWithHierarchy.sort((a, b) => {
|
|
32
|
+
const nameA = (a.hierarchicalName || 'unknown').toLowerCase();
|
|
33
|
+
const nameB = (b.hierarchicalName || 'unknown').toLowerCase();
|
|
34
|
+
return nameA.localeCompare(nameB);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const tableRows = functionsWithHierarchy.map(func => {
|
|
38
|
+
const name = (func.hierarchicalName || '').replace(/\|/g, '|');
|
|
39
|
+
const complexity = parseInt(func.complexity, 10);
|
|
40
|
+
const location = `${func.file}:${func.line}`;
|
|
41
|
+
return `| ${name} | ${complexity} | ${location} |`;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const lines = [
|
|
45
|
+
'# All Functions Including Callbacks',
|
|
46
|
+
'',
|
|
47
|
+
`**Total:** ${functionsWithHierarchy.length} functions`,
|
|
48
|
+
`**Generated:** ${new Date().toISOString()}`,
|
|
49
|
+
'',
|
|
50
|
+
'## Functions (Alphabetical)',
|
|
51
|
+
'',
|
|
52
|
+
'| Function | Complexity | Location |',
|
|
53
|
+
'| --- | --- | --- |',
|
|
54
|
+
...tableRows,
|
|
55
|
+
'',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generates Markdown export for functions organized by folder/file
|
|
63
|
+
* @param {Array} allFunctions - All functions array
|
|
64
|
+
* @param {Map} fileBoundariesMap - Map of filePath -> Map of line -> boundary
|
|
65
|
+
* @param {Map} fileToFunctions - Map of filePath -> functions array
|
|
66
|
+
* @returns {string} Markdown string
|
|
67
|
+
*/
|
|
68
|
+
export function generateFunctionsByFolderMD(
|
|
69
|
+
allFunctions,
|
|
70
|
+
fileBoundariesMap,
|
|
71
|
+
fileToFunctions
|
|
72
|
+
) {
|
|
73
|
+
const folderMap = groupFunctionsByFolder(allFunctions, getDirectory);
|
|
74
|
+
|
|
75
|
+
const lines = [
|
|
76
|
+
'# Functions by Folder/File',
|
|
77
|
+
'',
|
|
78
|
+
`**Total folders:** ${folderMap.size}`,
|
|
79
|
+
`**Total functions:** ${allFunctions.length}`,
|
|
80
|
+
`**Generated:** ${new Date().toISOString()}`,
|
|
81
|
+
'',
|
|
82
|
+
'## Structure',
|
|
83
|
+
'',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Sort folders alphabetically
|
|
87
|
+
const sortedFolders = Array.from(folderMap.entries()).sort((a, b) =>
|
|
88
|
+
a[0].localeCompare(b[0])
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
for (const [folder, fileMap] of sortedFolders) {
|
|
92
|
+
const folderDisplay = folder || '(root)';
|
|
93
|
+
lines.push(`### ${folderDisplay}/`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
|
|
96
|
+
// Sort files alphabetically
|
|
97
|
+
const sortedFiles = Array.from(fileMap.entries()).sort((a, b) =>
|
|
98
|
+
a[0].localeCompare(b[0])
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
for (const [file, functions] of sortedFiles) {
|
|
102
|
+
const fileName = file.split('/').pop();
|
|
103
|
+
lines.push(`#### \`${fileName}\``);
|
|
104
|
+
lines.push('');
|
|
105
|
+
|
|
106
|
+
// Get file-specific boundaries and functions
|
|
107
|
+
const fileBoundaries = fileBoundariesMap.get(file) || new Map();
|
|
108
|
+
const fileFunctions = fileToFunctions.get(file) || [];
|
|
109
|
+
|
|
110
|
+
// Build hierarchical names and sort alphabetically
|
|
111
|
+
const functionsWithHierarchy = functions.map(func => ({
|
|
112
|
+
...func,
|
|
113
|
+
hierarchicalName: buildHierarchicalFunctionName(
|
|
114
|
+
func,
|
|
115
|
+
fileBoundaries,
|
|
116
|
+
fileFunctions
|
|
117
|
+
),
|
|
118
|
+
})).sort((a, b) => {
|
|
119
|
+
const nameA = (a.hierarchicalName || 'unknown').toLowerCase();
|
|
120
|
+
const nameB = (b.hierarchicalName || 'unknown').toLowerCase();
|
|
121
|
+
return nameA.localeCompare(nameB);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
lines.push('| Function | Complexity | Location |');
|
|
125
|
+
lines.push('| --- | --- | --- |');
|
|
126
|
+
for (const func of functionsWithHierarchy) {
|
|
127
|
+
const name = (func.hierarchicalName || '').replace(/\|/g, '|');
|
|
128
|
+
const complexity = parseInt(func.complexity, 10);
|
|
129
|
+
lines.push(`| ${name} | ${complexity} | line ${func.line} |`);
|
|
130
|
+
}
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push('');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generates Markdown export of all file names (paths) in alphabetical order
|
|
141
|
+
* @param {Map} fileToFunctions - Map of filePath -> functions array
|
|
142
|
+
* @returns {string} Markdown string
|
|
143
|
+
*/
|
|
144
|
+
export function generateFileNamesAlphabeticalMD(fileToFunctions) {
|
|
145
|
+
const sortedFiles = Array.from(fileToFunctions.keys()).sort(
|
|
146
|
+
(a, b) => a.localeCompare(b)
|
|
147
|
+
);
|
|
148
|
+
const lines = [
|
|
149
|
+
'# File Names (Alphabetical)',
|
|
150
|
+
'',
|
|
151
|
+
`**Total files:** ${sortedFiles.length}`,
|
|
152
|
+
`**Generated:** ${new Date().toISOString()}`,
|
|
153
|
+
'',
|
|
154
|
+
'## Files',
|
|
155
|
+
'',
|
|
156
|
+
...sortedFiles.map(file => `- \`${file.replace(/`/g, '\\`')}\``),
|
|
157
|
+
'',
|
|
158
|
+
];
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|