@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,262 @@
1
+ import { buildHierarchicalFunctionName, groupFunctionsByFolder } from './helpers.js';
2
+ import { getDirectory } from '../function-extraction/index.js';
3
+
4
+ /**
5
+ * Generates TXT 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} TXT string
10
+ */
11
+ export function generateAllFunctionsTXT(
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 lines = [
38
+ 'All Functions Including Callbacks (Alphabetical)',
39
+ '=================================================',
40
+ '',
41
+ `Total: ${functionsWithHierarchy.length} functions`,
42
+ `Generated: ${new Date().toISOString()}`,
43
+ '',
44
+ ...functionsWithHierarchy.map(func => func.hierarchicalName),
45
+ ];
46
+
47
+ return lines.join('\n');
48
+ }
49
+
50
+ /**
51
+ * Returns the leaf (innermost) part of a hierarchical name.
52
+ * e.g. "AgencyLogosComponent → useEffect → map" → "map"; "TopLevel" → "TopLevel"
53
+ * @param {string} hierarchicalName - Full name possibly with " → " segments
54
+ * @returns {string}
55
+ */
56
+ function getLeafNameFromHierarchy(hierarchicalName) {
57
+ if (!hierarchicalName || typeof hierarchicalName !== 'string') {
58
+ return hierarchicalName || '';
59
+ }
60
+ const idx = hierarchicalName.lastIndexOf(' → ');
61
+ return idx === -1
62
+ ? hierarchicalName.trim()
63
+ : hierarchicalName.slice(idx + 3).trim();
64
+ }
65
+
66
+ /**
67
+ * Generates TXT export for all functions with leaf names only
68
+ * (e.g. "map" instead of "Component → map")
69
+ * @param {Array} allFunctions - All functions array
70
+ * @param {Map} fileBoundariesMap - Map of filePath -> Map
71
+ * @param {Map} fileToFunctions - Map of filePath -> functions array
72
+ * @returns {string} TXT string
73
+ */
74
+ export function generateAllFunctionsLeafOnlyTXT(
75
+ allFunctions,
76
+ fileBoundariesMap,
77
+ fileToFunctions
78
+ ) {
79
+ const functionsWithHierarchy = allFunctions.map(func => {
80
+ const fileBoundaries = fileBoundariesMap.get(func.file) || new Map();
81
+ const fileFunctions = fileToFunctions.get(func.file) || [];
82
+ return {
83
+ ...func,
84
+ hierarchicalName: buildHierarchicalFunctionName(
85
+ func,
86
+ fileBoundaries,
87
+ fileFunctions
88
+ ),
89
+ };
90
+ });
91
+ functionsWithHierarchy.sort((a, b) => {
92
+ const leafA = getLeafNameFromHierarchy(
93
+ a.hierarchicalName || 'unknown'
94
+ ).toLowerCase();
95
+ const leafB = getLeafNameFromHierarchy(
96
+ b.hierarchicalName || 'unknown'
97
+ ).toLowerCase();
98
+ return leafA.localeCompare(leafB);
99
+ });
100
+ const lines = [
101
+ 'All Functions — Leaf Names Only (Alphabetical)',
102
+ '==============================================',
103
+ '',
104
+ `Total: ${functionsWithHierarchy.length} functions`,
105
+ `Generated: ${new Date().toISOString()}`,
106
+ '',
107
+ ...functionsWithHierarchy.map((func) =>
108
+ getLeafNameFromHierarchy(func.hierarchicalName)
109
+ ),
110
+ ];
111
+ return lines.join('\n');
112
+ }
113
+
114
+ /**
115
+ * Generates TXT export for functions organized by folder/file
116
+ * @param {Array} allFunctions - All functions array
117
+ * @param {Map} fileBoundariesMap - Map of filePath -> Map of line -> boundary
118
+ * @param {Map} fileToFunctions - Map of filePath -> functions array
119
+ * @returns {string} TXT string
120
+ */
121
+ export function generateFunctionsByFolderTXT(
122
+ allFunctions,
123
+ fileBoundariesMap,
124
+ fileToFunctions
125
+ ) {
126
+ const folderMap = groupFunctionsByFolder(allFunctions, getDirectory);
127
+
128
+ const lines = [
129
+ 'Functions by Folder/File',
130
+ '=======================',
131
+ '',
132
+ `Total folders: ${folderMap.size}`,
133
+ `Total functions: ${allFunctions.length}`,
134
+ `Generated: ${new Date().toISOString()}`,
135
+ '',
136
+ ];
137
+
138
+ // Sort folders alphabetically
139
+ const sortedFolders = Array.from(folderMap.entries()).sort((a, b) =>
140
+ a[0].localeCompare(b[0])
141
+ );
142
+
143
+ for (const [folder, fileMap] of sortedFolders) {
144
+ lines.push(`\n${folder}/`);
145
+ lines.push('─'.repeat(folder.length + 1));
146
+
147
+ // Sort files alphabetically
148
+ const sortedFiles = Array.from(fileMap.entries()).sort((a, b) =>
149
+ a[0].localeCompare(b[0])
150
+ );
151
+
152
+ for (const [file, functions] of sortedFiles) {
153
+ const fileName = file.split('/').pop();
154
+ lines.push(`\n ${fileName}`);
155
+
156
+ // Get file-specific boundaries and functions
157
+ const fileBoundaries = fileBoundariesMap.get(file) || new Map();
158
+ const fileFunctions = fileToFunctions.get(file) || [];
159
+
160
+ // Build hierarchical names and sort alphabetically
161
+ const functionsWithHierarchy = functions.map(func => ({
162
+ ...func,
163
+ hierarchicalName: buildHierarchicalFunctionName(
164
+ func,
165
+ fileBoundaries,
166
+ fileFunctions
167
+ ),
168
+ })).sort((a, b) => {
169
+ const nameA = (a.hierarchicalName || 'unknown').toLowerCase();
170
+ const nameB = (b.hierarchicalName || 'unknown').toLowerCase();
171
+ return nameA.localeCompare(nameB);
172
+ });
173
+
174
+ for (const func of functionsWithHierarchy) {
175
+ lines.push(` - ${func.hierarchicalName}`);
176
+ }
177
+ }
178
+ }
179
+
180
+ return lines.join('\n');
181
+ }
182
+
183
+ /**
184
+ * Generates TXT export for functions by folder/file with leaf names only
185
+ * @param {Array} allFunctions - All functions array
186
+ * @param {Map} fileBoundariesMap - Map of filePath -> Map
187
+ * @param {Map} fileToFunctions - Map of filePath -> functions array
188
+ * @returns {string} TXT string
189
+ */
190
+ export function generateFunctionsByFolderLeafOnlyTXT(
191
+ allFunctions,
192
+ fileBoundariesMap,
193
+ fileToFunctions
194
+ ) {
195
+ const folderMap = groupFunctionsByFolder(allFunctions, getDirectory);
196
+ const lines = [
197
+ 'Functions by Folder/File — Leaf Names Only',
198
+ '============================================',
199
+ '',
200
+ `Total folders: ${folderMap.size}`,
201
+ `Total functions: ${allFunctions.length}`,
202
+ `Generated: ${new Date().toISOString()}`,
203
+ '',
204
+ ];
205
+ const sortedFolders = Array.from(folderMap.entries()).sort((a, b) =>
206
+ a[0].localeCompare(b[0])
207
+ );
208
+ for (const [folder, fileMap] of sortedFolders) {
209
+ lines.push(`\n${folder}/`);
210
+ lines.push('─'.repeat(folder.length + 1));
211
+ const sortedFiles = Array.from(fileMap.entries()).sort((a, b) =>
212
+ a[0].localeCompare(b[0])
213
+ );
214
+ for (const [file, functions] of sortedFiles) {
215
+ const fileName = file.split('/').pop();
216
+ lines.push(`\n ${fileName}`);
217
+ const fileBoundaries = fileBoundariesMap.get(file) || new Map();
218
+ const fileFunctions = fileToFunctions.get(file) || [];
219
+ const functionsWithHierarchy = functions.map(func => ({
220
+ ...func,
221
+ hierarchicalName: buildHierarchicalFunctionName(
222
+ func,
223
+ fileBoundaries,
224
+ fileFunctions
225
+ ),
226
+ })).sort((a, b) => {
227
+ const leafA = getLeafNameFromHierarchy(
228
+ a.hierarchicalName || 'unknown'
229
+ ).toLowerCase();
230
+ const leafB = getLeafNameFromHierarchy(
231
+ b.hierarchicalName || 'unknown'
232
+ ).toLowerCase();
233
+ return leafA.localeCompare(leafB);
234
+ });
235
+ for (const func of functionsWithHierarchy) {
236
+ lines.push(` - ${getLeafNameFromHierarchy(func.hierarchicalName)}`);
237
+ }
238
+ }
239
+ }
240
+ return lines.join('\n');
241
+ }
242
+
243
+ /**
244
+ * Generates TXT export of all file names (paths) in alphabetical order
245
+ * @param {Map} fileToFunctions - Map of filePath -> functions array
246
+ * @returns {string} TXT string
247
+ */
248
+ export function generateFileNamesAlphabeticalTXT(fileToFunctions) {
249
+ const sortedFiles = Array.from(fileToFunctions.keys()).sort((a, b) =>
250
+ a.localeCompare(b)
251
+ );
252
+ const lines = [
253
+ 'File Names (Alphabetical)',
254
+ '=========================',
255
+ '',
256
+ `Total files: ${sortedFiles.length}`,
257
+ `Generated: ${new Date().toISOString()}`,
258
+ '',
259
+ ...sortedFiles,
260
+ ];
261
+ return lines.join('\n');
262
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Arrow function end detection: brace body and main dispatcher.
3
+ */
4
+
5
+ import { findArrowFunctionEndJSXReturn } from './arrow-jsx.js';
6
+ import { findArrowFunctionEndJSXAttribute } from './arrow-jsx.js';
7
+ import { findArrowFunctionEndObjectLiteral } from './arrow-object-literal.js';
8
+ import { findArrowFunctionEndSingleExpression } from './arrow-single-expr.js';
9
+
10
+ /**
11
+ * Handles arrow function with JSX return pattern
12
+ * @param {Array<string>} lines - Array of source code lines
13
+ * @param {number} i - Current line index
14
+ * @param {number} arrowIndex - Index of => in the line
15
+ * @param {number} functionLine - Reported line number (1-based)
16
+ * @param {Map} boundaries - Map to store boundaries
17
+ * @returns {{end, found, arrowFunctionHandled, arrowFunctionEndSet}|null}
18
+ */
19
+ export function handleJSXReturnPattern(
20
+ lines,
21
+ i,
22
+ arrowIndex,
23
+ functionLine,
24
+ boundaries
25
+ ) {
26
+ const jsxResult = findArrowFunctionEndJSXReturn(
27
+ lines,
28
+ i,
29
+ arrowIndex,
30
+ functionLine
31
+ );
32
+ if (jsxResult.found) {
33
+ boundaries.set(functionLine, { start: i + 1, end: jsxResult.end });
34
+ return {
35
+ end: jsxResult.end,
36
+ found: true,
37
+ arrowFunctionHandled: true,
38
+ arrowFunctionEndSet: true
39
+ };
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Checks if arrow function body is a single-line brace body
46
+ * with balanced braces. Example: .forEach((item) => { doSomething(); });
47
+ * @param {string} line - Current line content
48
+ * @param {number} arrowIndex - Index of => in the line
49
+ * @returns {boolean} True if single-line brace body with balanced braces
50
+ */
51
+ function isSingleLineBraceBody(line, arrowIndex) {
52
+ const afterArrow = line.substring(arrowIndex + 2);
53
+ const openBraces = (afterArrow.match(/{/g) || []).length;
54
+ const closeBraces = (afterArrow.match(/}/g) || []).length;
55
+
56
+ // Must have at least one brace, and braces must balance
57
+ if (openBraces === 0 || openBraces !== closeBraces) {
58
+ return false;
59
+ }
60
+
61
+ // Check for common single-line patterns:
62
+ // - Ends with }); (callback in method call)
63
+ // - Ends with }; (arrow function assignment)
64
+ // - Ends with }) (callback without semicolon)
65
+ const trimmed = afterArrow.trim();
66
+ return (
67
+ trimmed.endsWith('});') ||
68
+ trimmed.endsWith('};') ||
69
+ trimmed.endsWith('})')
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Handles arrow function with brace on same line
75
+ * @param {Array<string>} lines - Array of source code lines
76
+ * @param {number} i - Current line index
77
+ * @param {number} arrowIndex - Index of => in the line
78
+ * @param {number} functionLine - Reported line number (1-based)
79
+ * @param {Map} boundaries - Map to store boundaries
80
+ * @param {string} line - Current line content
81
+ * @returns {{end, found, arrowFunctionHandled, arrowFunctionEndSet,
82
+ * inFunctionBody, braceCount}|null} Result or null
83
+ */
84
+ export function handleBraceOnSameLine(
85
+ lines,
86
+ i,
87
+ arrowIndex,
88
+ functionLine,
89
+ boundaries,
90
+ line
91
+ ) {
92
+ const braceIndex = line.indexOf('{', arrowIndex);
93
+ if (braceIndex === -1) {
94
+ const jsxAttrResult = findArrowFunctionEndJSXAttribute(
95
+ lines,
96
+ i,
97
+ arrowIndex,
98
+ functionLine
99
+ );
100
+ if (jsxAttrResult.found) {
101
+ boundaries.set(functionLine, { start: i + 1, end: jsxAttrResult.end });
102
+ return {
103
+ end: jsxAttrResult.end,
104
+ found: true,
105
+ arrowFunctionHandled: true,
106
+ arrowFunctionEndSet: true,
107
+ inFunctionBody: false,
108
+ braceCount: 0
109
+ };
110
+ }
111
+ return null;
112
+ }
113
+
114
+ // CRITICAL FIX: Check if this is a single-line arrow function
115
+ // with balanced braces. This prevents sibling callbacks from being
116
+ // treated as nested. Example: .forEach((type) => { total += x; });
117
+ if (isSingleLineBraceBody(line, arrowIndex)) {
118
+ boundaries.set(functionLine, { start: i + 1, end: i + 1 });
119
+ return {
120
+ end: i + 1,
121
+ found: true,
122
+ arrowFunctionHandled: true,
123
+ arrowFunctionEndSet: true,
124
+ inFunctionBody: false,
125
+ braceCount: 0
126
+ };
127
+ }
128
+
129
+ const objLiteralResult = findArrowFunctionEndObjectLiteral(
130
+ lines,
131
+ i,
132
+ arrowIndex,
133
+ braceIndex,
134
+ functionLine
135
+ );
136
+ if (objLiteralResult.found) {
137
+ boundaries.set(functionLine, { start: i + 1, end: objLiteralResult.end });
138
+ return {
139
+ end: objLiteralResult.end,
140
+ found: true,
141
+ arrowFunctionHandled: true,
142
+ arrowFunctionEndSet: true,
143
+ inFunctionBody: false,
144
+ braceCount: 0
145
+ };
146
+ }
147
+ const openBraces = (line.match(/{/g) || []).length;
148
+ return {
149
+ end: functionLine,
150
+ found: false,
151
+ arrowFunctionHandled: true,
152
+ arrowFunctionEndSet: false,
153
+ inFunctionBody: true,
154
+ braceCount: openBraces
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Handles arrow function without brace on same line
160
+ * @param {Array<string>} lines - Array of source code lines
161
+ * @param {number} i - Current line index
162
+ * @param {number} arrowIndex - Index of => in the line
163
+ * @param {number} functionLine - Reported line number (1-based)
164
+ * @param {Map} boundaries - Map to store boundaries
165
+ * @returns {{end, found, arrowFunctionHandled, arrowFunctionEndSet,
166
+ * inFunctionBody, braceCount}} Result
167
+ */
168
+ export function handleNoBraceOnSameLine(
169
+ lines,
170
+ i,
171
+ arrowIndex,
172
+ functionLine,
173
+ boundaries
174
+ ) {
175
+ if (i + 1 < lines.length && lines[i + 1].trim().startsWith('{')) {
176
+ return {
177
+ end: functionLine,
178
+ found: false,
179
+ arrowFunctionHandled: true,
180
+ arrowFunctionEndSet: false,
181
+ inFunctionBody: true,
182
+ braceCount: 1
183
+ };
184
+ }
185
+ const jsxAttrResult = findArrowFunctionEndJSXAttribute(
186
+ lines,
187
+ i,
188
+ arrowIndex,
189
+ functionLine
190
+ );
191
+ if (jsxAttrResult.found) {
192
+ boundaries.set(functionLine, { start: i + 1, end: jsxAttrResult.end });
193
+ return {
194
+ end: jsxAttrResult.end,
195
+ found: true,
196
+ arrowFunctionHandled: true,
197
+ arrowFunctionEndSet: false,
198
+ inFunctionBody: false,
199
+ braceCount: 0
200
+ };
201
+ }
202
+ const singleExprResult = findArrowFunctionEndSingleExpression(
203
+ lines,
204
+ i,
205
+ arrowIndex,
206
+ functionLine
207
+ );
208
+ if (singleExprResult.found) {
209
+ boundaries.set(functionLine, { start: i + 1, end: singleExprResult.end });
210
+ return {
211
+ end: singleExprResult.end,
212
+ found: true,
213
+ arrowFunctionHandled: true,
214
+ arrowFunctionEndSet: false,
215
+ inFunctionBody: false,
216
+ braceCount: 0
217
+ };
218
+ }
219
+ return {
220
+ end: functionLine,
221
+ found: false,
222
+ arrowFunctionHandled: false,
223
+ arrowFunctionEndSet: false,
224
+ inFunctionBody: false,
225
+ braceCount: 0
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Finds the end line for an arrow function (main dispatcher)
231
+ * @param {Array<string>} lines - Array of source code lines
232
+ * @param {number} start - Start line number (1-based)
233
+ * @param {number} functionLine - Reported line number (1-based)
234
+ * @param {Map} boundaries - Map to store boundaries
235
+ * @returns {{end, found, arrowFunctionHandled, arrowFunctionEndSet,
236
+ * inFunctionBody, braceCount}} Result object
237
+ */
238
+ export function findArrowFunctionEnd(
239
+ lines,
240
+ start,
241
+ functionLine,
242
+ boundaries
243
+ ) {
244
+ let end = functionLine;
245
+ let arrowFunctionHandled = false;
246
+ let arrowFunctionEndSet = false;
247
+ let inFunctionBody = false;
248
+ let braceCount = 0;
249
+
250
+ for (let i = start - 1; i < lines.length; i += 1) {
251
+ const line = lines[i];
252
+ if (line.includes('=>')) {
253
+ const arrowIndex = line.indexOf('=>');
254
+ const afterArrow = line.substring(arrowIndex + 2).trim();
255
+
256
+ if (afterArrow.startsWith('(')) {
257
+ const jsxResult = handleJSXReturnPattern(
258
+ lines,
259
+ i,
260
+ arrowIndex,
261
+ functionLine,
262
+ boundaries
263
+ );
264
+ if (jsxResult) {
265
+ return { ...jsxResult, inFunctionBody, braceCount };
266
+ }
267
+ }
268
+
269
+ if (line.includes('{')) {
270
+ const braceResult = handleBraceOnSameLine(
271
+ lines,
272
+ i,
273
+ arrowIndex,
274
+ functionLine,
275
+ boundaries,
276
+ line
277
+ );
278
+ if (braceResult) {
279
+ return braceResult;
280
+ }
281
+ } else {
282
+ return handleNoBraceOnSameLine(
283
+ lines,
284
+ i,
285
+ arrowIndex,
286
+ functionLine,
287
+ boundaries
288
+ );
289
+ }
290
+ break;
291
+ }
292
+ }
293
+
294
+ return {
295
+ end,
296
+ found: false,
297
+ arrowFunctionHandled,
298
+ arrowFunctionEndSet,
299
+ inFunctionBody,
300
+ braceCount,
301
+ };
302
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Arrow function start and JSX return closing pattern helpers.
3
+ */
4
+
5
+ /**
6
+ * Finds the start line for an arrow function (multi-line arrow declarations).
7
+ * For multi-line arrows (e.g. const fn = (\n a,\n b\n) => {),
8
+ * returns the line where declaration starts (e.g. "const fn = ("),
9
+ * so the whole declaration can be highlighted, not just the "=>" line.
10
+ * @param {Array<string>} lines - Array of source code lines
11
+ * @param {number} functionLine - Reported line (1-based), typically "=>"
12
+ * @returns {number} Start line number (1-based)
13
+ */
14
+ export function findArrowFunctionStart(lines, functionLine) {
15
+ const lineIndex = functionLine - 1;
16
+ if (lineIndex < 0 || lineIndex >= lines.length) return functionLine;
17
+
18
+ const arrowLine = lines[lineIndex];
19
+ const hasArrow = arrowLine && arrowLine.includes('=>');
20
+ // Default: start on the line that has "=>"
21
+ let start = functionLine;
22
+
23
+ if (hasArrow) {
24
+ start = lineIndex + 1;
25
+ // Walk backward to find declaration start: first line with
26
+ // "= (" or "=(" (assignment to open paren).
27
+ // Stop when we hit a line containing "=>" so we don't attribute
28
+ // another function's declaration to this arrow.
29
+ for (let i = lineIndex - 1; i >= 0; i -= 1) {
30
+ const line = lines[i];
31
+ if (!line) continue;
32
+ if (line.includes('=>')) break;
33
+ if (/=\s*\(/.test(line)) {
34
+ start = i + 1;
35
+ break;
36
+ }
37
+ }
38
+ }
39
+
40
+ return start;
41
+ }
42
+
43
+ /**
44
+ * Checks if a closing paren matches the JSX return pattern ()) or )} followed by )
45
+ * @param {string} scanLine - Current line being scanned
46
+ * @param {number} k - Index of closing paren
47
+ * @param {number} j - Current line index
48
+ * @param {Array<string>} lines - All lines
49
+ * @returns {number|null} End line number if pattern matches, null otherwise
50
+ */
51
+ export function checkJSXReturnClosingPattern(scanLine, k, j, lines) {
52
+ const nextChar = k + 1 < scanLine.length ? scanLine[k + 1] : '';
53
+ if (nextChar === ')') {
54
+ return j + 1;
55
+ }
56
+ if (nextChar === '}' && j + 1 < lines.length) {
57
+ const nextLine = lines[j + 1];
58
+ if (nextLine.trim().startsWith(')')) {
59
+ return j + 2;
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Scans lines to find matching closing parens for JSX return pattern
67
+ * @param {Array<string>} lines - Array of source code lines
68
+ * @param {number} startLine - Start line index (0-based)
69
+ * @param {string} line - First line content
70
+ * @param {number} scanIndex - Starting character index
71
+ * @returns {number|null} End line number if found, null otherwise
72
+ */
73
+ export function scanForJSXReturnClosingParens(lines, startLine, line, scanIndex) {
74
+ let parenCount = 1;
75
+ for (let j = startLine; j < lines.length && j < startLine + 50; j += 1) {
76
+ const scanLine = j === startLine ? line.substring(scanIndex) : lines[j];
77
+ for (let k = 0; k < scanLine.length; k += 1) {
78
+ const char = scanLine[k];
79
+ if (char === '(') {
80
+ parenCount += 1;
81
+ } else if (char === ')') {
82
+ parenCount -= 1;
83
+ if (parenCount === 0) {
84
+ const endLine = checkJSXReturnClosingPattern(scanLine, k, j, lines);
85
+ if (endLine !== null) {
86
+ return endLine;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return null;
93
+ }