@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,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, '&#124;');
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, '&#124;');
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
+ }