@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,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character-level parsing: comments, strings, regex, brace counting.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles escape sequences in strings
|
|
7
|
+
*/
|
|
8
|
+
export function handleEscapeSequence(char, inString, escapeNext) {
|
|
9
|
+
if (escapeNext) return { escapeNext: false, shouldContinue: true };
|
|
10
|
+
if (char === '\\' && inString) return { escapeNext: true, shouldContinue: true };
|
|
11
|
+
return { escapeNext: false, shouldContinue: false };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a single-line comment starts
|
|
16
|
+
*/
|
|
17
|
+
export function isSingleLineCommentStart(
|
|
18
|
+
char,
|
|
19
|
+
nextChar,
|
|
20
|
+
inString,
|
|
21
|
+
inRegex,
|
|
22
|
+
inMultiLineComment
|
|
23
|
+
) {
|
|
24
|
+
return (
|
|
25
|
+
char === '/' &&
|
|
26
|
+
nextChar === '/' &&
|
|
27
|
+
!inString &&
|
|
28
|
+
!inRegex &&
|
|
29
|
+
!inMultiLineComment
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks if a multi-line comment starts
|
|
35
|
+
*/
|
|
36
|
+
export function isMultiLineCommentStart(
|
|
37
|
+
char,
|
|
38
|
+
nextChar,
|
|
39
|
+
inString,
|
|
40
|
+
inRegex,
|
|
41
|
+
inSingleLineComment
|
|
42
|
+
) {
|
|
43
|
+
return (
|
|
44
|
+
char === '/' &&
|
|
45
|
+
nextChar === '*' &&
|
|
46
|
+
!inString &&
|
|
47
|
+
!inRegex &&
|
|
48
|
+
!inSingleLineComment
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks if a multi-line comment ends
|
|
54
|
+
*/
|
|
55
|
+
export function isMultiLineCommentEnd(char, nextChar, inMultiLineComment) {
|
|
56
|
+
return char === '*' && nextChar === '/' && inMultiLineComment;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Handles comment detection and state updates
|
|
61
|
+
*/
|
|
62
|
+
export function handleComments(
|
|
63
|
+
char,
|
|
64
|
+
nextChar,
|
|
65
|
+
inString,
|
|
66
|
+
inRegex,
|
|
67
|
+
inSingleLineComment,
|
|
68
|
+
inMultiLineComment
|
|
69
|
+
) {
|
|
70
|
+
if (
|
|
71
|
+
isSingleLineCommentStart(
|
|
72
|
+
char,
|
|
73
|
+
nextChar,
|
|
74
|
+
inString,
|
|
75
|
+
inRegex,
|
|
76
|
+
inMultiLineComment
|
|
77
|
+
)
|
|
78
|
+
) {
|
|
79
|
+
return {
|
|
80
|
+
inSingleLineComment: true,
|
|
81
|
+
inMultiLineComment: false,
|
|
82
|
+
shouldBreak: true,
|
|
83
|
+
shouldContinue: false,
|
|
84
|
+
skipNext: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
isMultiLineCommentStart(
|
|
89
|
+
char,
|
|
90
|
+
nextChar,
|
|
91
|
+
inString,
|
|
92
|
+
inRegex,
|
|
93
|
+
inSingleLineComment
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
return {
|
|
97
|
+
inSingleLineComment: false,
|
|
98
|
+
inMultiLineComment: true,
|
|
99
|
+
shouldBreak: false,
|
|
100
|
+
shouldContinue: true,
|
|
101
|
+
skipNext: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (isMultiLineCommentEnd(char, nextChar, inMultiLineComment)) {
|
|
105
|
+
return {
|
|
106
|
+
inSingleLineComment: false,
|
|
107
|
+
inMultiLineComment: false,
|
|
108
|
+
shouldBreak: false,
|
|
109
|
+
shouldContinue: true,
|
|
110
|
+
skipNext: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (inSingleLineComment || inMultiLineComment) {
|
|
114
|
+
return {
|
|
115
|
+
inSingleLineComment,
|
|
116
|
+
inMultiLineComment,
|
|
117
|
+
shouldBreak: false,
|
|
118
|
+
shouldContinue: true,
|
|
119
|
+
skipNext: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
inSingleLineComment: false,
|
|
124
|
+
inMultiLineComment: false,
|
|
125
|
+
shouldBreak: false,
|
|
126
|
+
shouldContinue: false,
|
|
127
|
+
skipNext: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handles string literal detection
|
|
133
|
+
*/
|
|
134
|
+
export function handleStringLiterals(char, inRegex, inString, stringChar) {
|
|
135
|
+
if ((char === '"' || char === "'") && !inRegex) {
|
|
136
|
+
if (!inString) return { inString: true, stringChar: char };
|
|
137
|
+
if (char === stringChar) return { inString: false, stringChar: null };
|
|
138
|
+
}
|
|
139
|
+
return { inString, stringChar };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Detects if a slash character is the start of a regex pattern
|
|
144
|
+
*/
|
|
145
|
+
export function isRegexStart(line, j, _prevChar) {
|
|
146
|
+
const beforeSlash = line.substring(Math.max(0, j - 2), j).trim();
|
|
147
|
+
return (
|
|
148
|
+
beforeSlash === '' ||
|
|
149
|
+
beforeSlash.endsWith('=') ||
|
|
150
|
+
beforeSlash.endsWith('(') ||
|
|
151
|
+
beforeSlash.endsWith('[') ||
|
|
152
|
+
beforeSlash.endsWith(',') ||
|
|
153
|
+
/^\s*$/.test(beforeSlash)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function couldBeRegexStart(char, prevChar, inRegex, inString) {
|
|
158
|
+
return (
|
|
159
|
+
char === '/' &&
|
|
160
|
+
prevChar !== '/' &&
|
|
161
|
+
prevChar !== '*' &&
|
|
162
|
+
!inRegex &&
|
|
163
|
+
!inString
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function couldBeRegexEnd(char, nextChar, inRegex) {
|
|
168
|
+
return char === '/' && inRegex && nextChar !== '/' && nextChar !== '*';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handles regex detection
|
|
173
|
+
*/
|
|
174
|
+
export function handleRegexDetection(
|
|
175
|
+
char,
|
|
176
|
+
prevChar,
|
|
177
|
+
nextChar,
|
|
178
|
+
line,
|
|
179
|
+
j,
|
|
180
|
+
inRegex,
|
|
181
|
+
inString
|
|
182
|
+
) {
|
|
183
|
+
if (couldBeRegexStart(char, prevChar, inRegex, inString)) {
|
|
184
|
+
if (isRegexStart(line, j, prevChar)) return true;
|
|
185
|
+
}
|
|
186
|
+
if (couldBeRegexEnd(char, nextChar, inRegex)) return false;
|
|
187
|
+
return inRegex;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Creates a result object with updated state
|
|
192
|
+
*/
|
|
193
|
+
export function createBracesResult(
|
|
194
|
+
openBraces,
|
|
195
|
+
closeBraces,
|
|
196
|
+
state,
|
|
197
|
+
updatedState,
|
|
198
|
+
shouldBreak,
|
|
199
|
+
shouldContinue,
|
|
200
|
+
skipNext
|
|
201
|
+
) {
|
|
202
|
+
return {
|
|
203
|
+
openBraces,
|
|
204
|
+
closeBraces,
|
|
205
|
+
state: { ...state, ...updatedState },
|
|
206
|
+
shouldBreak,
|
|
207
|
+
shouldContinue,
|
|
208
|
+
skipNext
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Processes escape sequence
|
|
214
|
+
*/
|
|
215
|
+
export function processEscapeSequence(
|
|
216
|
+
char,
|
|
217
|
+
inString,
|
|
218
|
+
escapeNext,
|
|
219
|
+
state,
|
|
220
|
+
openBraces,
|
|
221
|
+
closeBraces
|
|
222
|
+
) {
|
|
223
|
+
const escapeResult = handleEscapeSequence(char, inString, escapeNext);
|
|
224
|
+
if (escapeResult.shouldContinue) {
|
|
225
|
+
return {
|
|
226
|
+
result: createBracesResult(
|
|
227
|
+
openBraces,
|
|
228
|
+
closeBraces,
|
|
229
|
+
state,
|
|
230
|
+
{ escapeNext: escapeResult.escapeNext },
|
|
231
|
+
false,
|
|
232
|
+
true,
|
|
233
|
+
false
|
|
234
|
+
),
|
|
235
|
+
escapeNext: escapeResult.escapeNext
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return { result: null, escapeNext: escapeResult.escapeNext };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Handles comment processing
|
|
243
|
+
*/
|
|
244
|
+
export function processCommentHandling(
|
|
245
|
+
char,
|
|
246
|
+
nextChar,
|
|
247
|
+
inString,
|
|
248
|
+
inRegex,
|
|
249
|
+
inSingleLineComment,
|
|
250
|
+
inMultiLineComment,
|
|
251
|
+
escapeNext,
|
|
252
|
+
state,
|
|
253
|
+
openBraces,
|
|
254
|
+
closeBraces
|
|
255
|
+
) {
|
|
256
|
+
const commentResult = handleComments(
|
|
257
|
+
char,
|
|
258
|
+
nextChar,
|
|
259
|
+
inString,
|
|
260
|
+
inRegex,
|
|
261
|
+
inSingleLineComment,
|
|
262
|
+
inMultiLineComment
|
|
263
|
+
);
|
|
264
|
+
if (commentResult.shouldBreak) {
|
|
265
|
+
return {
|
|
266
|
+
result: createBracesResult(
|
|
267
|
+
openBraces,
|
|
268
|
+
closeBraces,
|
|
269
|
+
state,
|
|
270
|
+
{
|
|
271
|
+
inSingleLineComment: commentResult.inSingleLineComment,
|
|
272
|
+
inMultiLineComment: commentResult.inMultiLineComment,
|
|
273
|
+
escapeNext
|
|
274
|
+
},
|
|
275
|
+
true,
|
|
276
|
+
false,
|
|
277
|
+
false
|
|
278
|
+
),
|
|
279
|
+
inSingleLineComment: commentResult.inSingleLineComment,
|
|
280
|
+
inMultiLineComment: commentResult.inMultiLineComment
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (commentResult.shouldContinue) {
|
|
284
|
+
return {
|
|
285
|
+
result: createBracesResult(
|
|
286
|
+
openBraces,
|
|
287
|
+
closeBraces,
|
|
288
|
+
state,
|
|
289
|
+
{
|
|
290
|
+
inSingleLineComment: commentResult.inSingleLineComment,
|
|
291
|
+
inMultiLineComment: commentResult.inMultiLineComment,
|
|
292
|
+
escapeNext
|
|
293
|
+
},
|
|
294
|
+
false,
|
|
295
|
+
true,
|
|
296
|
+
commentResult.skipNext
|
|
297
|
+
),
|
|
298
|
+
inSingleLineComment: commentResult.inSingleLineComment,
|
|
299
|
+
inMultiLineComment: commentResult.inMultiLineComment
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
result: null,
|
|
304
|
+
inSingleLineComment: commentResult.inSingleLineComment,
|
|
305
|
+
inMultiLineComment: commentResult.inMultiLineComment
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Handles string literal processing
|
|
311
|
+
*/
|
|
312
|
+
export function processStringLiteralHandling(
|
|
313
|
+
char,
|
|
314
|
+
inRegex,
|
|
315
|
+
inString,
|
|
316
|
+
stringChar,
|
|
317
|
+
escapeNext,
|
|
318
|
+
state,
|
|
319
|
+
openBraces,
|
|
320
|
+
closeBraces
|
|
321
|
+
) {
|
|
322
|
+
const stringResult = handleStringLiterals(char, inRegex, inString, stringChar);
|
|
323
|
+
if (
|
|
324
|
+
stringResult.inString !== inString ||
|
|
325
|
+
stringResult.stringChar !== stringChar
|
|
326
|
+
) {
|
|
327
|
+
return {
|
|
328
|
+
result: createBracesResult(
|
|
329
|
+
openBraces,
|
|
330
|
+
closeBraces,
|
|
331
|
+
state,
|
|
332
|
+
{
|
|
333
|
+
inString: stringResult.inString,
|
|
334
|
+
stringChar: stringResult.stringChar,
|
|
335
|
+
escapeNext
|
|
336
|
+
},
|
|
337
|
+
false,
|
|
338
|
+
true,
|
|
339
|
+
false
|
|
340
|
+
),
|
|
341
|
+
inString: stringResult.inString,
|
|
342
|
+
stringChar: stringResult.stringChar
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
result: null,
|
|
347
|
+
inString: stringResult.inString,
|
|
348
|
+
stringChar: stringResult.stringChar,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Processes a single character in the line to count braces
|
|
354
|
+
*/
|
|
355
|
+
export function processCharacterForBraces(
|
|
356
|
+
char,
|
|
357
|
+
prevChar,
|
|
358
|
+
nextChar,
|
|
359
|
+
line,
|
|
360
|
+
j,
|
|
361
|
+
state,
|
|
362
|
+
openBraces,
|
|
363
|
+
closeBraces
|
|
364
|
+
) {
|
|
365
|
+
let {
|
|
366
|
+
inRegex,
|
|
367
|
+
inString,
|
|
368
|
+
inSingleLineComment,
|
|
369
|
+
inMultiLineComment,
|
|
370
|
+
stringChar,
|
|
371
|
+
escapeNext,
|
|
372
|
+
} = state;
|
|
373
|
+
|
|
374
|
+
const escapeHandling = processEscapeSequence(
|
|
375
|
+
char,
|
|
376
|
+
inString,
|
|
377
|
+
escapeNext,
|
|
378
|
+
state,
|
|
379
|
+
openBraces,
|
|
380
|
+
closeBraces
|
|
381
|
+
);
|
|
382
|
+
if (escapeHandling.result) return escapeHandling.result;
|
|
383
|
+
escapeNext = escapeHandling.escapeNext;
|
|
384
|
+
|
|
385
|
+
const commentHandling = processCommentHandling(
|
|
386
|
+
char,
|
|
387
|
+
nextChar,
|
|
388
|
+
inString,
|
|
389
|
+
inRegex,
|
|
390
|
+
inSingleLineComment,
|
|
391
|
+
inMultiLineComment,
|
|
392
|
+
escapeNext,
|
|
393
|
+
state,
|
|
394
|
+
openBraces,
|
|
395
|
+
closeBraces
|
|
396
|
+
);
|
|
397
|
+
if (commentHandling.result) return commentHandling.result;
|
|
398
|
+
inSingleLineComment = commentHandling.inSingleLineComment;
|
|
399
|
+
inMultiLineComment = commentHandling.inMultiLineComment;
|
|
400
|
+
|
|
401
|
+
const stringHandling = processStringLiteralHandling(
|
|
402
|
+
char,
|
|
403
|
+
inRegex,
|
|
404
|
+
inString,
|
|
405
|
+
stringChar,
|
|
406
|
+
escapeNext,
|
|
407
|
+
state,
|
|
408
|
+
openBraces,
|
|
409
|
+
closeBraces
|
|
410
|
+
);
|
|
411
|
+
if (stringHandling.result) return stringHandling.result;
|
|
412
|
+
inString = stringHandling.inString;
|
|
413
|
+
stringChar = stringHandling.stringChar;
|
|
414
|
+
|
|
415
|
+
const newRegexState = handleRegexDetection(
|
|
416
|
+
char,
|
|
417
|
+
prevChar,
|
|
418
|
+
nextChar,
|
|
419
|
+
line,
|
|
420
|
+
j,
|
|
421
|
+
inRegex,
|
|
422
|
+
inString
|
|
423
|
+
);
|
|
424
|
+
if (newRegexState !== inRegex) {
|
|
425
|
+
return createBracesResult(
|
|
426
|
+
openBraces,
|
|
427
|
+
closeBraces,
|
|
428
|
+
state,
|
|
429
|
+
{
|
|
430
|
+
inRegex: newRegexState,
|
|
431
|
+
inString,
|
|
432
|
+
stringChar,
|
|
433
|
+
escapeNext
|
|
434
|
+
},
|
|
435
|
+
false,
|
|
436
|
+
true,
|
|
437
|
+
false
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
inRegex = newRegexState;
|
|
441
|
+
|
|
442
|
+
if (!inRegex && !inString) {
|
|
443
|
+
if (char === '{') openBraces += 1;
|
|
444
|
+
if (char === '}') closeBraces += 1;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return createBracesResult(
|
|
448
|
+
openBraces,
|
|
449
|
+
closeBraces,
|
|
450
|
+
state,
|
|
451
|
+
{ inRegex, inString, stringChar, escapeNext },
|
|
452
|
+
false,
|
|
453
|
+
false,
|
|
454
|
+
false
|
|
455
|
+
);
|
|
456
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST parsing and traversal for function extraction.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { parse } from '@typescript-eslint/typescript-estree';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses source code into AST
|
|
11
|
+
*/
|
|
12
|
+
export function parseAST(sourceCode, filePath) {
|
|
13
|
+
try {
|
|
14
|
+
const isTSX = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
|
|
15
|
+
return parse(sourceCode, {
|
|
16
|
+
sourceType: 'module',
|
|
17
|
+
ecmaVersion: 2020,
|
|
18
|
+
jsx: isTSX,
|
|
19
|
+
filePath: filePath,
|
|
20
|
+
comment: true,
|
|
21
|
+
loc: true,
|
|
22
|
+
range: true,
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getNodeLine(node) {
|
|
30
|
+
if (node.loc && node.loc.start) return node.loc.start.line;
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shouldSkipKey(key) {
|
|
35
|
+
return key === 'parent' || key === 'range' || key === 'loc' ||
|
|
36
|
+
key === 'leadingComments' || key === 'trailingComments';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function processArrayChildren(array, visit) {
|
|
40
|
+
array.forEach(item => {
|
|
41
|
+
if (item && typeof item === 'object' && item.type) visit(item);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function processChildNode(child, visit) {
|
|
46
|
+
if (child && typeof child === 'object' && child.type) visit(child);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function traverseAST(node, visit) {
|
|
50
|
+
if (!node || typeof node !== 'object') return;
|
|
51
|
+
visit(node);
|
|
52
|
+
for (const key in node) {
|
|
53
|
+
if (shouldSkipKey(key)) continue;
|
|
54
|
+
const child = node[key];
|
|
55
|
+
if (Array.isArray(child)) {
|
|
56
|
+
processArrayChildren(child, (item) => traverseAST(item, visit));
|
|
57
|
+
} else {
|
|
58
|
+
processChildNode(child, (item) => traverseAST(item, visit));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function findAllFunctions(ast) {
|
|
64
|
+
const functions = [];
|
|
65
|
+
const functionTypes = new Set([
|
|
66
|
+
'FunctionDeclaration',
|
|
67
|
+
'FunctionExpression',
|
|
68
|
+
'ArrowFunctionExpression',
|
|
69
|
+
'MethodDefinition',
|
|
70
|
+
]);
|
|
71
|
+
traverseAST(ast, (node) => {
|
|
72
|
+
if (functionTypes.has(node.type)) functions.push(node);
|
|
73
|
+
});
|
|
74
|
+
return functions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function findAllNodesByType(ast, nodeType) {
|
|
78
|
+
const results = [];
|
|
79
|
+
traverseAST(ast, (node) => {
|
|
80
|
+
if (node.type === nodeType) results.push(node);
|
|
81
|
+
});
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Finds the containing node that wraps a function node
|
|
87
|
+
*/
|
|
88
|
+
export function findContainingNode(funcNode, candidateNodes) {
|
|
89
|
+
if (!funcNode.range) return null;
|
|
90
|
+
let containingNode = null;
|
|
91
|
+
let smallestSize = Infinity;
|
|
92
|
+
for (const candidate of candidateNodes) {
|
|
93
|
+
if (!candidate.range) continue;
|
|
94
|
+
if (
|
|
95
|
+
candidate.range[0] < funcNode.range[0] &&
|
|
96
|
+
candidate.range[1] > funcNode.range[1]
|
|
97
|
+
) {
|
|
98
|
+
const size = candidate.range[1] - candidate.range[0];
|
|
99
|
+
if (size < smallestSize) {
|
|
100
|
+
containingNode = candidate;
|
|
101
|
+
smallestSize = size;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return containingNode;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function readFileIfExists(filePath, projectRoot) {
|
|
109
|
+
const fullPath = resolve(projectRoot, filePath);
|
|
110
|
+
if (!existsSync(fullPath)) return null;
|
|
111
|
+
return readFileSync(fullPath, 'utf-8');
|
|
112
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback context identification (CallExpression, JSXAttribute, ReturnStatement).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { findAllNodesByType, findAllFunctions, findContainingNode } from './ast-utils.js';
|
|
6
|
+
|
|
7
|
+
export function getCalleeCallbackName(callee) {
|
|
8
|
+
if (!callee) return null;
|
|
9
|
+
if (callee.type === 'MemberExpression' && callee.property) return callee.property.name || null;
|
|
10
|
+
if (callee.type === 'Identifier') return callee.name || null;
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function checkCallExpressionCallback(funcNode, ast) {
|
|
15
|
+
const callExpressions = findAllNodesByType(ast, 'CallExpression');
|
|
16
|
+
const newExpressions = findAllNodesByType(ast, 'NewExpression');
|
|
17
|
+
const containingNode = findContainingNode(funcNode, [
|
|
18
|
+
...callExpressions,
|
|
19
|
+
...newExpressions,
|
|
20
|
+
]);
|
|
21
|
+
if (containingNode && containingNode.callee) {
|
|
22
|
+
return getCalleeCallbackName(containingNode.callee);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function checkJSXAttributeCallback(funcNode, ast) {
|
|
28
|
+
const jsxAttributes = findAllNodesByType(ast, 'JSXAttribute');
|
|
29
|
+
const containingAttr = findContainingNode(funcNode, jsxAttributes);
|
|
30
|
+
if (containingAttr && containingAttr.name) {
|
|
31
|
+
const attrName = containingAttr.name.name;
|
|
32
|
+
if (attrName.startsWith('on') || attrName === 'ref') {
|
|
33
|
+
return attrName === 'ref' ? 'ref' : `${attrName} handler`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function checkReturnCallback(funcNode, ast) {
|
|
40
|
+
const returnStatements = findAllNodesByType(ast, 'ReturnStatement');
|
|
41
|
+
const containingReturn = findContainingNode(funcNode, returnStatements);
|
|
42
|
+
if (containingReturn) {
|
|
43
|
+
const isDirectReturnValue = containingReturn.argument === funcNode ||
|
|
44
|
+
(containingReturn.argument &&
|
|
45
|
+
containingReturn.argument.type === 'ArrowFunctionExpression' &&
|
|
46
|
+
containingReturn.argument === funcNode);
|
|
47
|
+
if (isDirectReturnValue) {
|
|
48
|
+
const allFunctions = findAllFunctions(ast);
|
|
49
|
+
const returnParent = findContainingNode(containingReturn, allFunctions);
|
|
50
|
+
if (returnParent) return 'return';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function identifyCallbackContext(funcNode, ast) {
|
|
57
|
+
if (!funcNode.range) return null;
|
|
58
|
+
const returnCallback = checkReturnCallback(funcNode, ast);
|
|
59
|
+
if (returnCallback) return returnCallback;
|
|
60
|
+
const jsxCallback = checkJSXAttributeCallback(funcNode, ast);
|
|
61
|
+
if (jsxCallback) return jsxCallback;
|
|
62
|
+
const callCallback = checkCallExpressionCallback(funcNode, ast);
|
|
63
|
+
if (callCallback) return callCallback;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint result processing and main extractFunctionName (AST + regex fallback).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileIfExists } from './ast-utils.js';
|
|
6
|
+
import { extractFunctionNameAST } from './extract-name-ast.js';
|
|
7
|
+
import { handleArrowFunctionExpression, handleFunctionDeclaration } from './extract-name-regex.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extracts function name: AST first, then regex fallback.
|
|
11
|
+
* Uses self-reference for regex fallback.
|
|
12
|
+
*/
|
|
13
|
+
export function extractFunctionName(
|
|
14
|
+
filePath,
|
|
15
|
+
lineNumber,
|
|
16
|
+
nodeType,
|
|
17
|
+
projectRoot
|
|
18
|
+
) {
|
|
19
|
+
const astName = extractFunctionNameAST(
|
|
20
|
+
filePath,
|
|
21
|
+
lineNumber,
|
|
22
|
+
nodeType,
|
|
23
|
+
projectRoot
|
|
24
|
+
);
|
|
25
|
+
if (astName) return astName;
|
|
26
|
+
try {
|
|
27
|
+
const fileContent = readFileIfExists(filePath, projectRoot);
|
|
28
|
+
if (!fileContent) return 'unknown';
|
|
29
|
+
const lines = fileContent.split('\n');
|
|
30
|
+
if (nodeType === 'ArrowFunctionExpression') {
|
|
31
|
+
return handleArrowFunctionExpression(
|
|
32
|
+
lines,
|
|
33
|
+
lineNumber,
|
|
34
|
+
filePath,
|
|
35
|
+
projectRoot,
|
|
36
|
+
extractFunctionName
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return handleFunctionDeclaration(lines, lineNumber);
|
|
40
|
+
} catch {
|
|
41
|
+
return 'unknown';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Processes a single complexity message and upserts into functionMap.
|
|
47
|
+
*/
|
|
48
|
+
export function processComplexityMessage(message, file, projectRoot, functionMap) {
|
|
49
|
+
if (message.ruleId !== 'complexity' || message.severity !== 1) return;
|
|
50
|
+
const complexityMatch = message.message.match(/complexity of (\d+)/i);
|
|
51
|
+
if (!complexityMatch) return;
|
|
52
|
+
const filePath = file.filePath.replace(projectRoot + '/', '');
|
|
53
|
+
const nodeType = message.nodeType || 'FunctionDeclaration';
|
|
54
|
+
const functionName = extractFunctionName(
|
|
55
|
+
filePath,
|
|
56
|
+
message.line,
|
|
57
|
+
nodeType,
|
|
58
|
+
projectRoot
|
|
59
|
+
);
|
|
60
|
+
const complexity = complexityMatch[1];
|
|
61
|
+
const key = `${filePath}:${functionName}:${message.line}`;
|
|
62
|
+
const existing = functionMap.get(key);
|
|
63
|
+
if (existing && complexity <= parseInt(existing.complexity, 10)) return;
|
|
64
|
+
functionMap.set(key, {
|
|
65
|
+
file: filePath,
|
|
66
|
+
line: message.line,
|
|
67
|
+
column: message.column || 1,
|
|
68
|
+
message: message.message,
|
|
69
|
+
complexity: complexity,
|
|
70
|
+
functionName: functionName,
|
|
71
|
+
nodeType: nodeType,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Processes ESLint results and extracts function complexity data
|
|
77
|
+
*/
|
|
78
|
+
export function extractFunctionsFromESLintResults(eslintResults, projectRoot) {
|
|
79
|
+
const functionMap = new Map();
|
|
80
|
+
eslintResults.forEach((file) => {
|
|
81
|
+
if (!file.messages) return;
|
|
82
|
+
file.messages.forEach((message) =>
|
|
83
|
+
processComplexityMessage(message, file, projectRoot, functionMap)
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
const allFunctions = [...functionMap.values()];
|
|
87
|
+
allFunctions.sort(
|
|
88
|
+
(a, b) => parseInt(b.complexity, 10) - parseInt(a.complexity, 10)
|
|
89
|
+
);
|
|
90
|
+
return allFunctions;
|
|
91
|
+
}
|