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