@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,471 @@
|
|
|
1
|
+
/* Shared CSS for Complexity Report HTML Pages */
|
|
2
|
+
|
|
3
|
+
/* Base Styles */
|
|
4
|
+
body, html {
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
height: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: Helvetica Neue, Helvetica, Arial;
|
|
12
|
+
font-size: 14px;
|
|
13
|
+
color: #333;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
*, *:after, *:before {
|
|
17
|
+
-webkit-box-sizing: border-box;
|
|
18
|
+
-moz-box-sizing: border-box;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Typography */
|
|
23
|
+
h1 {
|
|
24
|
+
font-size: 20px;
|
|
25
|
+
margin: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
h2 {
|
|
29
|
+
font-size: 14px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
h3 {
|
|
33
|
+
font-size: 14px;
|
|
34
|
+
margin: 12px 0 4px 0;
|
|
35
|
+
font-weight: bold;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
p {
|
|
39
|
+
margin: 4px 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ul {
|
|
43
|
+
margin: 4px 0;
|
|
44
|
+
padding-left: 20px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
li {
|
|
48
|
+
margin: 2px 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.small {
|
|
52
|
+
font-size: 12px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.strong {
|
|
56
|
+
font-weight: bold;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Links */
|
|
60
|
+
a {
|
|
61
|
+
color: #0074D9;
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
a:hover {
|
|
66
|
+
text-decoration: underline;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.about-link {
|
|
70
|
+
color: #0074D9;
|
|
71
|
+
text-decoration: none;
|
|
72
|
+
font-size: 14px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.about-link:hover {
|
|
76
|
+
text-decoration: underline;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.quiet {
|
|
80
|
+
color: #7f7f7f;
|
|
81
|
+
color: rgba(0,0,0,0.5);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.quiet a {
|
|
85
|
+
opacity: 0.7;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Code */
|
|
89
|
+
code {
|
|
90
|
+
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
91
|
+
font-size: 13px;
|
|
92
|
+
background-color: #f5f5f5;
|
|
93
|
+
padding: 2px 4px;
|
|
94
|
+
border-radius: 3px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pre {
|
|
98
|
+
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
99
|
+
margin: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
-moz-tab-size: 2;
|
|
102
|
+
-o-tab-size: 2;
|
|
103
|
+
tab-size: 2;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Spacing Utilities */
|
|
107
|
+
.pad1 {
|
|
108
|
+
padding: 10px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.pad1y {
|
|
112
|
+
padding: 10px 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.pad2 {
|
|
116
|
+
padding: 10px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.pad2x {
|
|
120
|
+
padding: 0 20px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.pad2y {
|
|
124
|
+
padding: 20px 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.space-top1 {
|
|
128
|
+
padding: 10px 0 0 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.space-left2 {
|
|
132
|
+
padding-left: 55px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.space-right2 {
|
|
136
|
+
padding-right: 20px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Layout */
|
|
140
|
+
.clearfix {
|
|
141
|
+
display: block;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.clearfix:after {
|
|
145
|
+
content: '';
|
|
146
|
+
display: block;
|
|
147
|
+
height: 0;
|
|
148
|
+
clear: both;
|
|
149
|
+
visibility: hidden;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.fl {
|
|
153
|
+
float: left;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.center {
|
|
157
|
+
text-align: center;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.header-row {
|
|
161
|
+
display: flex;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
align-items: center;
|
|
164
|
+
margin-bottom: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.header-row h1 {
|
|
168
|
+
margin: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Coverage Summary Table */
|
|
172
|
+
.coverage-summary {
|
|
173
|
+
border-collapse: collapse;
|
|
174
|
+
width: 100%;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.coverage-summary tr {
|
|
178
|
+
border-bottom: 1px solid #bbb;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.keyline-all {
|
|
182
|
+
border: 1px solid #ddd;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.coverage-summary td,
|
|
186
|
+
.coverage-summary th {
|
|
187
|
+
padding: 10px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.coverage-summary tbody tr {
|
|
191
|
+
background-color: rgb(230, 245, 208);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Row background colors based on complexity level (only for function rows, not folder rows) */
|
|
195
|
+
.coverage-summary tbody tr[data-complexity].high {
|
|
196
|
+
background-color: #fff4c2;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.coverage-summary tbody tr[data-complexity].medium,
|
|
200
|
+
.coverage-summary tbody tr[data-complexity].low {
|
|
201
|
+
background-color: #FCE1E5;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.coverage-summary tbody {
|
|
205
|
+
border: 1px solid #bbb;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.coverage-summary td {
|
|
209
|
+
border-right: 1px solid #bbb;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.coverage-summary td:last-child {
|
|
213
|
+
border-right: none;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.coverage-summary th {
|
|
217
|
+
text-align: left;
|
|
218
|
+
font-weight: normal;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
user-select: none;
|
|
222
|
+
border: none !important;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.coverage-summary th:hover {
|
|
226
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Sort arrow sprite (matching coverage report) */
|
|
230
|
+
.coverage-summary .sorter {
|
|
231
|
+
height: 10px;
|
|
232
|
+
width: 7px;
|
|
233
|
+
display: inline-block;
|
|
234
|
+
margin-left: 0.5em;
|
|
235
|
+
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.coverage-summary .sorted .sorter {
|
|
239
|
+
background-position: 0 -20px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.coverage-summary .sorted-desc .sorter {
|
|
243
|
+
background-position: 0 -10px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Footer */
|
|
247
|
+
.footer {
|
|
248
|
+
height: 48px;
|
|
249
|
+
padding: 20px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.coverage-summary th.file {
|
|
253
|
+
border-right: none !important;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.coverage-summary th.pct,
|
|
257
|
+
.coverage-summary th.abs,
|
|
258
|
+
.coverage-summary th.pic {
|
|
259
|
+
text-align: right;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.coverage-summary td.pct,
|
|
263
|
+
.coverage-summary td.abs {
|
|
264
|
+
text-align: right;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.coverage-summary td.file {
|
|
268
|
+
white-space: nowrap;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.coverage-summary td.pic {
|
|
272
|
+
min-width: 120px !important;
|
|
273
|
+
text-align: right;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.coverage-summary td.bar {
|
|
277
|
+
padding: 10px;
|
|
278
|
+
min-width: 120px !important;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Chart / Progress Bar */
|
|
282
|
+
.chart {
|
|
283
|
+
line-height: 0;
|
|
284
|
+
font-size: 0;
|
|
285
|
+
white-space: nowrap;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.cover-fill,
|
|
289
|
+
.cover-empty {
|
|
290
|
+
display: inline-block;
|
|
291
|
+
height: 12px;
|
|
292
|
+
vertical-align: top;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.cover-empty {
|
|
296
|
+
background: white;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.cover-full {
|
|
300
|
+
border-right: none !important;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Complexity Level Colors - Default (for homepage folder rows) */
|
|
304
|
+
.low .cover-fill {
|
|
305
|
+
background: #C21F39;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.low .chart {
|
|
309
|
+
border: 1px solid #C21F39;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.high .cover-fill {
|
|
313
|
+
background: rgb(77,146,33);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.high .chart {
|
|
317
|
+
border: 1px solid rgb(77,146,33);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.medium .cover-fill {
|
|
321
|
+
background: #f9cd0b;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.medium .chart {
|
|
325
|
+
border: 1px solid #f9cd0b;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Complexity Level Colors - Function rows only (for folder pages) */
|
|
329
|
+
.function-complexity-table .high .cover-fill {
|
|
330
|
+
background: #f9cd0b;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.function-complexity-table .high .chart {
|
|
334
|
+
border: 1px solid #f9cd0b;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.function-complexity-table .medium .cover-fill {
|
|
338
|
+
background: #C21F39;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.function-complexity-table .medium .chart {
|
|
342
|
+
border: 1px solid #C21F39;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.acceptable .cover-fill {
|
|
346
|
+
background: rgb(100,150,50);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.acceptable .chart {
|
|
350
|
+
border: 1px solid rgb(100,150,50);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.good .cover-fill {
|
|
354
|
+
background: rgb(120,160,70);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.good .chart {
|
|
358
|
+
border: 1px solid rgb(120,160,70);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.status-line {
|
|
362
|
+
height: 10px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.status-line.low {
|
|
366
|
+
background: #C21F39;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.status-line.high {
|
|
370
|
+
background: rgb(77,146,33);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.status-line.medium {
|
|
374
|
+
background: #f9cd0b;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* Complexity Values */
|
|
378
|
+
.complexity-value {
|
|
379
|
+
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.complexity-value.low {
|
|
383
|
+
color: #C21F39;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.complexity-value.medium {
|
|
387
|
+
color: #f9cd0b;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.complexity-value.high {
|
|
391
|
+
color: rgb(77,146,33);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.complexity-value.acceptable {
|
|
395
|
+
color: rgb(100,150,50);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.complexity-value.good {
|
|
399
|
+
color: rgb(120,160,70);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.fraction {
|
|
403
|
+
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
404
|
+
font-size: 10px;
|
|
405
|
+
color: #555;
|
|
406
|
+
background: #E8E8E8;
|
|
407
|
+
padding: 4px 5px;
|
|
408
|
+
border-radius: 3px;
|
|
409
|
+
vertical-align: middle;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Sort Icons */
|
|
413
|
+
.sort-icon {
|
|
414
|
+
display: inline-block;
|
|
415
|
+
width: 12px;
|
|
416
|
+
height: 12px;
|
|
417
|
+
margin-left: 5px;
|
|
418
|
+
color: #999;
|
|
419
|
+
opacity: 0.5;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.sort-icon.active {
|
|
423
|
+
color: #000;
|
|
424
|
+
opacity: 1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* Controls */
|
|
428
|
+
.controls {
|
|
429
|
+
margin: 20px 0;
|
|
430
|
+
padding: 15px;
|
|
431
|
+
background: #f5f5f5;
|
|
432
|
+
border-radius: 4px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.controls label {
|
|
436
|
+
margin-right: 15px;
|
|
437
|
+
font-weight: normal;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.controls input,
|
|
441
|
+
.controls select {
|
|
442
|
+
margin-left: 5px;
|
|
443
|
+
padding: 5px;
|
|
444
|
+
border: 1px solid #ddd;
|
|
445
|
+
border-radius: 3px;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Additional Color Classes */
|
|
449
|
+
.acceptable {
|
|
450
|
+
background: rgb(240,250,230);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.good {
|
|
454
|
+
background: rgb(245,252,240);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.acceptable .cover-fill {
|
|
458
|
+
background: rgb(100,150,50);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.good .cover-fill {
|
|
462
|
+
background: rgb(120,160,70);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.acceptable .chart {
|
|
466
|
+
border: 1px solid rgb(100,150,50);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.good .chart {
|
|
470
|
+
border: 1px solid rgb(120,160,70);
|
|
471
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes HTML special characters
|
|
3
|
+
* @param {string} text - Text to escape
|
|
4
|
+
* @returns {string} Escaped HTML
|
|
5
|
+
*/
|
|
6
|
+
export function escapeHtml(text) {
|
|
7
|
+
const map = {
|
|
8
|
+
'&': '&',
|
|
9
|
+
'<': '<',
|
|
10
|
+
'>': '>',
|
|
11
|
+
'"': '"',
|
|
12
|
+
"'": '''
|
|
13
|
+
};
|
|
14
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
15
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* complexity-report - AST-based cyclomatic complexity analyzer
|
|
3
|
+
*
|
|
4
|
+
* Public API for programmatic usage
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { generateComplexityReport } from './report/index.js';
|
|
8
|
+
|
|
9
|
+
// Export utility functions that might be useful for consumers
|
|
10
|
+
export { findESLintConfig, getComplexityVariant } from './integration/eslint/index.js';
|
|
11
|
+
export { getComplexityThreshold } from './integration/threshold/index.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Type definitions for API consumers (JSDoc)
|
|
15
|
+
*
|
|
16
|
+
* @typedef {Object} ComplexityReportOptions
|
|
17
|
+
* @property {string} [cwd] - Project root directory (defaults to process.cwd())
|
|
18
|
+
* @property {string} [outputDir] - Output directory for reports (defaults to 'complexity' under cwd)
|
|
19
|
+
* @property {boolean} [showAllInitially] - Show all functions initially in HTML report
|
|
20
|
+
* @property {boolean} [showAllColumnsInitially] - Show all breakdown columns initially
|
|
21
|
+
* @property {boolean} [hideTableInitially] - Hide breakdown table initially
|
|
22
|
+
* @property {boolean} [hideLinesInitially] - Hide line numbers initially
|
|
23
|
+
* @property {boolean} [hideHighlightsInitially] - Hide code highlights initially
|
|
24
|
+
* @property {boolean} [shouldExport] - Generate TXT/MD export files
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} ComplexityReportResult
|
|
27
|
+
* @property {Object} stats - Statistics about analyzed functions
|
|
28
|
+
* @property {number} stats.allFunctionsCount - Total number of functions analyzed
|
|
29
|
+
* @property {number} stats.maxComplexity - Highest complexity found
|
|
30
|
+
* @property {number} stats.avgComplexity - Average complexity
|
|
31
|
+
* @property {number} stats.withinThreshold - Number of functions within threshold
|
|
32
|
+
* @property {number} stats.withinThresholdPercentage - Percentage within threshold
|
|
33
|
+
* @property {Array} stats.overThreshold - Functions exceeding complexity threshold
|
|
34
|
+
* @property {Array} folders - Folder-level complexity data
|
|
35
|
+
* @property {string} complexityDir - Path to generated report directory
|
|
36
|
+
*/
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ESLint } from 'eslint';
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
const CONFIG_NAMES = ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs'];
|
|
6
|
+
const COMPLEXITY_DIR = 'complexity';
|
|
7
|
+
const REPORT_FILENAME = 'complexity-report.json';
|
|
8
|
+
|
|
9
|
+
/** @type {'classic' | 'modified'} ESLint complexity variant; see https://eslint.org/docs/latest/rules/complexity */
|
|
10
|
+
const DEFAULT_VARIANT = 'classic';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Finds the project's ESLint config file (flat config).
|
|
14
|
+
* ESLint 9+ looks for eslint.config.js, .mjs, .cjs in that order.
|
|
15
|
+
* @param {string} projectRoot - Root directory of the project
|
|
16
|
+
* @returns {string | null} Absolute path to config file, or null if not found
|
|
17
|
+
*/
|
|
18
|
+
export function findESLintConfig(projectRoot) {
|
|
19
|
+
for (const name of CONFIG_NAMES) {
|
|
20
|
+
const path = resolve(projectRoot, name);
|
|
21
|
+
if (existsSync(path)) return path;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads the complexity rule variant from the config file.
|
|
28
|
+
* Classic: each switch case +1; modified: whole switch +1.
|
|
29
|
+
* Defaults to "classic" if not found.
|
|
30
|
+
* See https://eslint.org/docs/latest/rules/complexity (option "variant").
|
|
31
|
+
* @param {string} configPath - Absolute path to eslint.config.js
|
|
32
|
+
* @returns {'classic' | 'modified'}
|
|
33
|
+
*/
|
|
34
|
+
export function getComplexityVariant(configPath) {
|
|
35
|
+
try {
|
|
36
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
37
|
+
if (/variant:\s*["']modified["']/.test(content)) return 'modified';
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
return DEFAULT_VARIANT;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Runs ESLint using the project's config with complexity overridden to max: 0
|
|
46
|
+
* so every function gets a complexity diagnostic. Writes the JSON report to
|
|
47
|
+
* complexity/complexity-report.json (same mental model as Vitest coverage/).
|
|
48
|
+
* @param {string} projectRoot - Root directory of the project
|
|
49
|
+
* @returns {Promise<Array>} ESLint results as JSON array (same shape as --format=json)
|
|
50
|
+
*/
|
|
51
|
+
export async function runESLintComplexityCheck(projectRoot) {
|
|
52
|
+
const configPath = findESLintConfig(projectRoot);
|
|
53
|
+
if (!configPath) {
|
|
54
|
+
const tried = CONFIG_NAMES.join(', ');
|
|
55
|
+
console.error(
|
|
56
|
+
`No ESLint flat config found. Tried: ${tried}. Add one at project root to run the complexity report.`
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const complexityDir = resolve(projectRoot, COMPLEXITY_DIR);
|
|
62
|
+
mkdirSync(complexityDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const reportPath = resolve(complexityDir, REPORT_FILENAME);
|
|
65
|
+
const variant = getComplexityVariant(configPath);
|
|
66
|
+
|
|
67
|
+
console.log('Running ESLint to collect complexity for all functions...');
|
|
68
|
+
try {
|
|
69
|
+
const eslint = new ESLint({
|
|
70
|
+
cwd: projectRoot,
|
|
71
|
+
overrideConfigFile: configPath,
|
|
72
|
+
ignorePatterns: [
|
|
73
|
+
'**/__tests__/**',
|
|
74
|
+
'**/*.test.{js,ts,tsx}',
|
|
75
|
+
'**/*.spec.{js,ts,tsx}',
|
|
76
|
+
],
|
|
77
|
+
overrideConfig: {
|
|
78
|
+
rules: {
|
|
79
|
+
complexity: ['warn', { max: 0, variant }],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const results = await eslint.lintFiles(['.']);
|
|
85
|
+
|
|
86
|
+
const json = JSON.stringify(results, null, 0);
|
|
87
|
+
writeFileSync(reportPath, json, 'utf-8');
|
|
88
|
+
|
|
89
|
+
return results;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Error running ESLint:', error.message);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reads the complexity threshold from eslint.config.js
|
|
6
|
+
* Returns the maximum threshold value found (different file types
|
|
7
|
+
* can have different thresholds)
|
|
8
|
+
* @param {string} projectRoot - Root directory of the project
|
|
9
|
+
* @returns {number} Maximum complexity threshold value
|
|
10
|
+
*/
|
|
11
|
+
export function getComplexityThreshold(projectRoot) {
|
|
12
|
+
const configPath = resolve(projectRoot, 'eslint.config.js');
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const configContent = readFileSync(configPath, 'utf-8');
|
|
16
|
+
|
|
17
|
+
// Extract all complexity max values using regex
|
|
18
|
+
// Pattern: complexity: ["warn", { max: <number>, variant: "classic" }]
|
|
19
|
+
const complexityMatches = configContent.match(/complexity:\s*\["warn",\s*\{\s*max:\s*(\d+)/g);
|
|
20
|
+
|
|
21
|
+
if (!complexityMatches || complexityMatches.length === 0) {
|
|
22
|
+
// Default to 10 if not found
|
|
23
|
+
console.warn('Could not find complexity threshold in eslint.config.js, defaulting to 10');
|
|
24
|
+
return 10;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Extract all max values
|
|
28
|
+
const maxValues = complexityMatches.map((match) => {
|
|
29
|
+
const valueMatch = match.match(/max:\s*(\d+)/);
|
|
30
|
+
return valueMatch ? parseInt(valueMatch[1], 10) : null;
|
|
31
|
+
}).filter((val) => val !== null);
|
|
32
|
+
|
|
33
|
+
if (maxValues.length === 0) {
|
|
34
|
+
console.warn('Could not parse complexity threshold values, defaulting to 10');
|
|
35
|
+
return 10;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Return the maximum threshold value (to be safe, use the highest)
|
|
39
|
+
const maxThreshold = Math.max(...maxValues);
|
|
40
|
+
return maxThreshold;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn(`Error reading eslint.config.js: ${error.message}, defaulting to 10`);
|
|
43
|
+
return 10;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pythonidaer/complexity-report",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "AST-based cyclomatic complexity analyzer with interactive HTML reports and detailed function breakdowns",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"complexity-report": "report/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"test:coverage": "vitest --coverage",
|
|
16
|
+
"report": "node report/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"report/",
|
|
21
|
+
"integration/",
|
|
22
|
+
"function-boundaries/",
|
|
23
|
+
"function-extraction/",
|
|
24
|
+
"decision-points/",
|
|
25
|
+
"html-generators/",
|
|
26
|
+
"export-generators/",
|
|
27
|
+
"assets/",
|
|
28
|
+
"complexity-breakdown.js",
|
|
29
|
+
"function-hierarchy.js",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"complexity",
|
|
36
|
+
"cyclomatic-complexity",
|
|
37
|
+
"eslint",
|
|
38
|
+
"code-quality",
|
|
39
|
+
"static-analysis",
|
|
40
|
+
"ast",
|
|
41
|
+
"javascript",
|
|
42
|
+
"typescript",
|
|
43
|
+
"code-metrics",
|
|
44
|
+
"complexity-report"
|
|
45
|
+
],
|
|
46
|
+
"author": "Johnny Hammond",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/Pythonidaer/complexity-report.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/Pythonidaer/complexity-report#readme",
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"eslint": "^9.0.0",
|
|
55
|
+
"@typescript-eslint/typescript-estree": "^8.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"vitest": "^2.0.0",
|
|
59
|
+
"@vitest/coverage-v8": "^2.0.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"eslint": ">=9.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|