@optave/codegraph 2.4.0 → 2.5.0

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.
@@ -0,0 +1,2056 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadConfig } from './config.js';
4
+ import { openReadonlyOrFail } from './db.js';
5
+ import { info } from './logger.js';
6
+ import { LANGUAGE_REGISTRY } from './parser.js';
7
+ import { isTestFile } from './queries.js';
8
+
9
+ // ─── Language-Specific Node Type Registry ─────────────────────────────────
10
+
11
+ const JS_TS_RULES = {
12
+ // Structural increments (cognitive +1, cyclomatic varies)
13
+ branchNodes: new Set([
14
+ 'if_statement',
15
+ 'else_clause',
16
+ 'switch_statement',
17
+ 'for_statement',
18
+ 'for_in_statement',
19
+ 'while_statement',
20
+ 'do_statement',
21
+ 'catch_clause',
22
+ 'ternary_expression',
23
+ ]),
24
+ // Cyclomatic-only: each case adds a path
25
+ caseNodes: new Set(['switch_case']),
26
+ // Logical operators: cognitive +1 per sequence change, cyclomatic +1 each
27
+ logicalOperators: new Set(['&&', '||', '??']),
28
+ logicalNodeType: 'binary_expression',
29
+ // Optional chaining: cyclomatic only
30
+ optionalChainType: 'optional_chain_expression',
31
+ // Nesting-sensitive: these increment nesting depth
32
+ nestingNodes: new Set([
33
+ 'if_statement',
34
+ 'switch_statement',
35
+ 'for_statement',
36
+ 'for_in_statement',
37
+ 'while_statement',
38
+ 'do_statement',
39
+ 'catch_clause',
40
+ 'ternary_expression',
41
+ ]),
42
+ // Function-like nodes (increase nesting when nested)
43
+ functionNodes: new Set([
44
+ 'function_declaration',
45
+ 'function_expression',
46
+ 'arrow_function',
47
+ 'method_definition',
48
+ 'generator_function',
49
+ 'generator_function_declaration',
50
+ ]),
51
+ // If/else pattern detection
52
+ ifNodeType: 'if_statement',
53
+ elseNodeType: 'else_clause',
54
+ elifNodeType: null,
55
+ elseViaAlternative: false,
56
+ switchLikeNodes: new Set(['switch_statement']),
57
+ };
58
+
59
+ const PYTHON_RULES = {
60
+ branchNodes: new Set([
61
+ 'if_statement',
62
+ 'elif_clause',
63
+ 'else_clause',
64
+ 'for_statement',
65
+ 'while_statement',
66
+ 'except_clause',
67
+ 'conditional_expression',
68
+ 'match_statement',
69
+ ]),
70
+ caseNodes: new Set(['case_clause']),
71
+ logicalOperators: new Set(['and', 'or']),
72
+ logicalNodeType: 'boolean_operator',
73
+ optionalChainType: null,
74
+ nestingNodes: new Set([
75
+ 'if_statement',
76
+ 'for_statement',
77
+ 'while_statement',
78
+ 'except_clause',
79
+ 'conditional_expression',
80
+ ]),
81
+ functionNodes: new Set(['function_definition', 'lambda']),
82
+ ifNodeType: 'if_statement',
83
+ elseNodeType: 'else_clause',
84
+ elifNodeType: 'elif_clause',
85
+ elseViaAlternative: false,
86
+ switchLikeNodes: new Set(['match_statement']),
87
+ };
88
+
89
+ const GO_RULES = {
90
+ branchNodes: new Set([
91
+ 'if_statement',
92
+ 'for_statement',
93
+ 'expression_switch_statement',
94
+ 'type_switch_statement',
95
+ 'select_statement',
96
+ ]),
97
+ caseNodes: new Set(['expression_case', 'type_case', 'default_case', 'communication_case']),
98
+ logicalOperators: new Set(['&&', '||']),
99
+ logicalNodeType: 'binary_expression',
100
+ optionalChainType: null,
101
+ nestingNodes: new Set([
102
+ 'if_statement',
103
+ 'for_statement',
104
+ 'expression_switch_statement',
105
+ 'type_switch_statement',
106
+ 'select_statement',
107
+ ]),
108
+ functionNodes: new Set(['function_declaration', 'method_declaration', 'func_literal']),
109
+ ifNodeType: 'if_statement',
110
+ elseNodeType: null,
111
+ elifNodeType: null,
112
+ elseViaAlternative: true,
113
+ switchLikeNodes: new Set(['expression_switch_statement', 'type_switch_statement']),
114
+ };
115
+
116
+ const RUST_RULES = {
117
+ branchNodes: new Set([
118
+ 'if_expression',
119
+ 'else_clause',
120
+ 'for_expression',
121
+ 'while_expression',
122
+ 'loop_expression',
123
+ 'if_let_expression',
124
+ 'while_let_expression',
125
+ 'match_expression',
126
+ ]),
127
+ caseNodes: new Set(['match_arm']),
128
+ logicalOperators: new Set(['&&', '||']),
129
+ logicalNodeType: 'binary_expression',
130
+ optionalChainType: null,
131
+ nestingNodes: new Set([
132
+ 'if_expression',
133
+ 'for_expression',
134
+ 'while_expression',
135
+ 'loop_expression',
136
+ 'if_let_expression',
137
+ 'while_let_expression',
138
+ 'match_expression',
139
+ ]),
140
+ functionNodes: new Set(['function_item', 'closure_expression']),
141
+ ifNodeType: 'if_expression',
142
+ elseNodeType: 'else_clause',
143
+ elifNodeType: null,
144
+ elseViaAlternative: false,
145
+ switchLikeNodes: new Set(['match_expression']),
146
+ };
147
+
148
+ const JAVA_RULES = {
149
+ branchNodes: new Set([
150
+ 'if_statement',
151
+ 'for_statement',
152
+ 'enhanced_for_statement',
153
+ 'while_statement',
154
+ 'do_statement',
155
+ 'catch_clause',
156
+ 'ternary_expression',
157
+ 'switch_expression',
158
+ ]),
159
+ caseNodes: new Set(['switch_label']),
160
+ logicalOperators: new Set(['&&', '||']),
161
+ logicalNodeType: 'binary_expression',
162
+ optionalChainType: null,
163
+ nestingNodes: new Set([
164
+ 'if_statement',
165
+ 'for_statement',
166
+ 'enhanced_for_statement',
167
+ 'while_statement',
168
+ 'do_statement',
169
+ 'catch_clause',
170
+ 'ternary_expression',
171
+ ]),
172
+ functionNodes: new Set(['method_declaration', 'constructor_declaration', 'lambda_expression']),
173
+ ifNodeType: 'if_statement',
174
+ elseNodeType: null,
175
+ elifNodeType: null,
176
+ elseViaAlternative: true,
177
+ switchLikeNodes: new Set(['switch_expression']),
178
+ };
179
+
180
+ const CSHARP_RULES = {
181
+ branchNodes: new Set([
182
+ 'if_statement',
183
+ 'else_clause',
184
+ 'for_statement',
185
+ 'for_each_statement',
186
+ 'while_statement',
187
+ 'do_statement',
188
+ 'catch_clause',
189
+ 'conditional_expression',
190
+ 'switch_statement',
191
+ ]),
192
+ caseNodes: new Set(['switch_section']),
193
+ logicalOperators: new Set(['&&', '||', '??']),
194
+ logicalNodeType: 'binary_expression',
195
+ optionalChainType: 'conditional_access_expression',
196
+ nestingNodes: new Set([
197
+ 'if_statement',
198
+ 'for_statement',
199
+ 'for_each_statement',
200
+ 'while_statement',
201
+ 'do_statement',
202
+ 'catch_clause',
203
+ 'conditional_expression',
204
+ 'switch_statement',
205
+ ]),
206
+ functionNodes: new Set([
207
+ 'method_declaration',
208
+ 'constructor_declaration',
209
+ 'lambda_expression',
210
+ 'local_function_statement',
211
+ ]),
212
+ ifNodeType: 'if_statement',
213
+ elseNodeType: 'else_clause',
214
+ elifNodeType: null,
215
+ elseViaAlternative: false,
216
+ switchLikeNodes: new Set(['switch_statement']),
217
+ };
218
+
219
+ const RUBY_RULES = {
220
+ branchNodes: new Set([
221
+ 'if',
222
+ 'elsif',
223
+ 'else',
224
+ 'unless',
225
+ 'case',
226
+ 'for',
227
+ 'while',
228
+ 'until',
229
+ 'rescue',
230
+ 'conditional',
231
+ ]),
232
+ caseNodes: new Set(['when']),
233
+ logicalOperators: new Set(['and', 'or', '&&', '||']),
234
+ logicalNodeType: 'binary',
235
+ optionalChainType: null,
236
+ nestingNodes: new Set(['if', 'unless', 'case', 'for', 'while', 'until', 'rescue', 'conditional']),
237
+ functionNodes: new Set(['method', 'singleton_method', 'lambda', 'do_block']),
238
+ ifNodeType: 'if',
239
+ elseNodeType: 'else',
240
+ elifNodeType: 'elsif',
241
+ elseViaAlternative: false,
242
+ switchLikeNodes: new Set(['case']),
243
+ };
244
+
245
+ const PHP_RULES = {
246
+ branchNodes: new Set([
247
+ 'if_statement',
248
+ 'else_if_clause',
249
+ 'else_clause',
250
+ 'for_statement',
251
+ 'foreach_statement',
252
+ 'while_statement',
253
+ 'do_statement',
254
+ 'catch_clause',
255
+ 'conditional_expression',
256
+ 'switch_statement',
257
+ ]),
258
+ caseNodes: new Set(['case_statement', 'default_statement']),
259
+ logicalOperators: new Set(['&&', '||', 'and', 'or', '??']),
260
+ logicalNodeType: 'binary_expression',
261
+ optionalChainType: 'nullsafe_member_access_expression',
262
+ nestingNodes: new Set([
263
+ 'if_statement',
264
+ 'for_statement',
265
+ 'foreach_statement',
266
+ 'while_statement',
267
+ 'do_statement',
268
+ 'catch_clause',
269
+ 'conditional_expression',
270
+ 'switch_statement',
271
+ ]),
272
+ functionNodes: new Set([
273
+ 'function_definition',
274
+ 'method_declaration',
275
+ 'anonymous_function_creation_expression',
276
+ 'arrow_function',
277
+ ]),
278
+ ifNodeType: 'if_statement',
279
+ elseNodeType: 'else_clause',
280
+ elifNodeType: 'else_if_clause',
281
+ elseViaAlternative: false,
282
+ switchLikeNodes: new Set(['switch_statement']),
283
+ };
284
+
285
+ export const COMPLEXITY_RULES = new Map([
286
+ ['javascript', JS_TS_RULES],
287
+ ['typescript', JS_TS_RULES],
288
+ ['tsx', JS_TS_RULES],
289
+ ['python', PYTHON_RULES],
290
+ ['go', GO_RULES],
291
+ ['rust', RUST_RULES],
292
+ ['java', JAVA_RULES],
293
+ ['c_sharp', CSHARP_RULES],
294
+ ['ruby', RUBY_RULES],
295
+ ['php', PHP_RULES],
296
+ ]);
297
+
298
+ // Extensions whose language has complexity rules — used to skip needless WASM init
299
+ const COMPLEXITY_EXTENSIONS = new Set();
300
+ for (const entry of LANGUAGE_REGISTRY) {
301
+ if (COMPLEXITY_RULES.has(entry.id)) {
302
+ for (const ext of entry.extensions) COMPLEXITY_EXTENSIONS.add(ext);
303
+ }
304
+ }
305
+
306
+ // ─── Halstead Operator/Operand Classification ────────────────────────────
307
+
308
+ const JS_TS_HALSTEAD = {
309
+ operatorLeafTypes: new Set([
310
+ // Arithmetic
311
+ '+',
312
+ '-',
313
+ '*',
314
+ '/',
315
+ '%',
316
+ '**',
317
+ // Assignment
318
+ '=',
319
+ '+=',
320
+ '-=',
321
+ '*=',
322
+ '/=',
323
+ '%=',
324
+ '**=',
325
+ '<<=',
326
+ '>>=',
327
+ '>>>=',
328
+ '&=',
329
+ '|=',
330
+ '^=',
331
+ '&&=',
332
+ '||=',
333
+ '??=',
334
+ // Comparison
335
+ '==',
336
+ '===',
337
+ '!=',
338
+ '!==',
339
+ '<',
340
+ '>',
341
+ '<=',
342
+ '>=',
343
+ // Logical
344
+ '&&',
345
+ '||',
346
+ '!',
347
+ '??',
348
+ // Bitwise
349
+ '&',
350
+ '|',
351
+ '^',
352
+ '~',
353
+ '<<',
354
+ '>>',
355
+ '>>>',
356
+ // Unary
357
+ '++',
358
+ '--',
359
+ // Keywords as operators
360
+ 'typeof',
361
+ 'instanceof',
362
+ 'new',
363
+ 'return',
364
+ 'throw',
365
+ 'yield',
366
+ 'await',
367
+ 'if',
368
+ 'else',
369
+ 'for',
370
+ 'while',
371
+ 'do',
372
+ 'switch',
373
+ 'case',
374
+ 'break',
375
+ 'continue',
376
+ 'try',
377
+ 'catch',
378
+ 'finally',
379
+ // Arrow, spread, ternary, access
380
+ '=>',
381
+ '...',
382
+ '?',
383
+ ':',
384
+ '.',
385
+ '?.',
386
+ // Delimiters counted as operators
387
+ ',',
388
+ ';',
389
+ ]),
390
+ operandLeafTypes: new Set([
391
+ 'identifier',
392
+ 'property_identifier',
393
+ 'shorthand_property_identifier',
394
+ 'shorthand_property_identifier_pattern',
395
+ 'number',
396
+ 'string_fragment',
397
+ 'regex_pattern',
398
+ 'true',
399
+ 'false',
400
+ 'null',
401
+ 'undefined',
402
+ 'this',
403
+ 'super',
404
+ 'private_property_identifier',
405
+ ]),
406
+ compoundOperators: new Set([
407
+ 'call_expression',
408
+ 'subscript_expression',
409
+ 'new_expression',
410
+ 'template_substitution',
411
+ ]),
412
+ skipTypes: new Set(['type_annotation', 'type_parameters', 'return_type', 'implements_clause']),
413
+ };
414
+
415
+ const PYTHON_HALSTEAD = {
416
+ operatorLeafTypes: new Set([
417
+ '+',
418
+ '-',
419
+ '*',
420
+ '/',
421
+ '%',
422
+ '**',
423
+ '//',
424
+ '=',
425
+ '+=',
426
+ '-=',
427
+ '*=',
428
+ '/=',
429
+ '%=',
430
+ '**=',
431
+ '//=',
432
+ '&=',
433
+ '|=',
434
+ '^=',
435
+ '<<=',
436
+ '>>=',
437
+ '==',
438
+ '!=',
439
+ '<',
440
+ '>',
441
+ '<=',
442
+ '>=',
443
+ 'and',
444
+ 'or',
445
+ 'not',
446
+ '&',
447
+ '|',
448
+ '^',
449
+ '~',
450
+ '<<',
451
+ '>>',
452
+ 'if',
453
+ 'else',
454
+ 'elif',
455
+ 'for',
456
+ 'while',
457
+ 'with',
458
+ 'try',
459
+ 'except',
460
+ 'finally',
461
+ 'raise',
462
+ 'return',
463
+ 'yield',
464
+ 'await',
465
+ 'pass',
466
+ 'break',
467
+ 'continue',
468
+ 'import',
469
+ 'from',
470
+ 'as',
471
+ 'in',
472
+ 'is',
473
+ 'lambda',
474
+ 'del',
475
+ '.',
476
+ ',',
477
+ ':',
478
+ '@',
479
+ '->',
480
+ ]),
481
+ operandLeafTypes: new Set([
482
+ 'identifier',
483
+ 'integer',
484
+ 'float',
485
+ 'string_content',
486
+ 'true',
487
+ 'false',
488
+ 'none',
489
+ ]),
490
+ compoundOperators: new Set(['call', 'subscript', 'attribute']),
491
+ skipTypes: new Set([]),
492
+ };
493
+
494
+ const GO_HALSTEAD = {
495
+ operatorLeafTypes: new Set([
496
+ '+',
497
+ '-',
498
+ '*',
499
+ '/',
500
+ '%',
501
+ '=',
502
+ ':=',
503
+ '+=',
504
+ '-=',
505
+ '*=',
506
+ '/=',
507
+ '%=',
508
+ '&=',
509
+ '|=',
510
+ '^=',
511
+ '<<=',
512
+ '>>=',
513
+ '==',
514
+ '!=',
515
+ '<',
516
+ '>',
517
+ '<=',
518
+ '>=',
519
+ '&&',
520
+ '||',
521
+ '!',
522
+ '&',
523
+ '|',
524
+ '^',
525
+ '~',
526
+ '<<',
527
+ '>>',
528
+ '&^',
529
+ '++',
530
+ '--',
531
+ 'if',
532
+ 'else',
533
+ 'for',
534
+ 'switch',
535
+ 'select',
536
+ 'case',
537
+ 'default',
538
+ 'return',
539
+ 'break',
540
+ 'continue',
541
+ 'goto',
542
+ 'fallthrough',
543
+ 'go',
544
+ 'defer',
545
+ 'range',
546
+ 'chan',
547
+ 'func',
548
+ 'var',
549
+ 'const',
550
+ 'type',
551
+ 'struct',
552
+ 'interface',
553
+ '.',
554
+ ',',
555
+ ';',
556
+ ':',
557
+ '<-',
558
+ ]),
559
+ operandLeafTypes: new Set([
560
+ 'identifier',
561
+ 'field_identifier',
562
+ 'package_identifier',
563
+ 'type_identifier',
564
+ 'int_literal',
565
+ 'float_literal',
566
+ 'imaginary_literal',
567
+ 'rune_literal',
568
+ 'interpreted_string_literal',
569
+ 'raw_string_literal',
570
+ 'true',
571
+ 'false',
572
+ 'nil',
573
+ 'iota',
574
+ ]),
575
+ compoundOperators: new Set(['call_expression', 'index_expression', 'selector_expression']),
576
+ skipTypes: new Set([]),
577
+ };
578
+
579
+ const RUST_HALSTEAD = {
580
+ operatorLeafTypes: new Set([
581
+ '+',
582
+ '-',
583
+ '*',
584
+ '/',
585
+ '%',
586
+ '=',
587
+ '+=',
588
+ '-=',
589
+ '*=',
590
+ '/=',
591
+ '%=',
592
+ '&=',
593
+ '|=',
594
+ '^=',
595
+ '<<=',
596
+ '>>=',
597
+ '==',
598
+ '!=',
599
+ '<',
600
+ '>',
601
+ '<=',
602
+ '>=',
603
+ '&&',
604
+ '||',
605
+ '!',
606
+ '&',
607
+ '|',
608
+ '^',
609
+ '<<',
610
+ '>>',
611
+ 'if',
612
+ 'else',
613
+ 'for',
614
+ 'while',
615
+ 'loop',
616
+ 'match',
617
+ 'return',
618
+ 'break',
619
+ 'continue',
620
+ 'let',
621
+ 'mut',
622
+ 'ref',
623
+ 'as',
624
+ 'in',
625
+ 'move',
626
+ 'fn',
627
+ 'struct',
628
+ 'enum',
629
+ 'trait',
630
+ 'impl',
631
+ 'pub',
632
+ 'mod',
633
+ 'use',
634
+ '.',
635
+ ',',
636
+ ';',
637
+ ':',
638
+ '::',
639
+ '=>',
640
+ '->',
641
+ '?',
642
+ ]),
643
+ operandLeafTypes: new Set([
644
+ 'identifier',
645
+ 'field_identifier',
646
+ 'type_identifier',
647
+ 'integer_literal',
648
+ 'float_literal',
649
+ 'string_content',
650
+ 'char_literal',
651
+ 'true',
652
+ 'false',
653
+ 'self',
654
+ 'Self',
655
+ ]),
656
+ compoundOperators: new Set(['call_expression', 'index_expression', 'field_expression']),
657
+ skipTypes: new Set([]),
658
+ };
659
+
660
+ const JAVA_HALSTEAD = {
661
+ operatorLeafTypes: new Set([
662
+ '+',
663
+ '-',
664
+ '*',
665
+ '/',
666
+ '%',
667
+ '=',
668
+ '+=',
669
+ '-=',
670
+ '*=',
671
+ '/=',
672
+ '%=',
673
+ '&=',
674
+ '|=',
675
+ '^=',
676
+ '<<=',
677
+ '>>=',
678
+ '>>>=',
679
+ '==',
680
+ '!=',
681
+ '<',
682
+ '>',
683
+ '<=',
684
+ '>=',
685
+ '&&',
686
+ '||',
687
+ '!',
688
+ '&',
689
+ '|',
690
+ '^',
691
+ '~',
692
+ '<<',
693
+ '>>',
694
+ '>>>',
695
+ '++',
696
+ '--',
697
+ 'instanceof',
698
+ 'new',
699
+ 'if',
700
+ 'else',
701
+ 'for',
702
+ 'while',
703
+ 'do',
704
+ 'switch',
705
+ 'case',
706
+ 'return',
707
+ 'throw',
708
+ 'break',
709
+ 'continue',
710
+ 'try',
711
+ 'catch',
712
+ 'finally',
713
+ '.',
714
+ ',',
715
+ ';',
716
+ ':',
717
+ '?',
718
+ '->',
719
+ ]),
720
+ operandLeafTypes: new Set([
721
+ 'identifier',
722
+ 'type_identifier',
723
+ 'decimal_integer_literal',
724
+ 'hex_integer_literal',
725
+ 'octal_integer_literal',
726
+ 'binary_integer_literal',
727
+ 'decimal_floating_point_literal',
728
+ 'hex_floating_point_literal',
729
+ 'string_literal',
730
+ 'character_literal',
731
+ 'true',
732
+ 'false',
733
+ 'null',
734
+ 'this',
735
+ 'super',
736
+ ]),
737
+ compoundOperators: new Set(['method_invocation', 'array_access', 'object_creation_expression']),
738
+ skipTypes: new Set(['type_arguments', 'type_parameters']),
739
+ };
740
+
741
+ const CSHARP_HALSTEAD = {
742
+ operatorLeafTypes: new Set([
743
+ '+',
744
+ '-',
745
+ '*',
746
+ '/',
747
+ '%',
748
+ '=',
749
+ '+=',
750
+ '-=',
751
+ '*=',
752
+ '/=',
753
+ '%=',
754
+ '&=',
755
+ '|=',
756
+ '^=',
757
+ '<<=',
758
+ '>>=',
759
+ '==',
760
+ '!=',
761
+ '<',
762
+ '>',
763
+ '<=',
764
+ '>=',
765
+ '&&',
766
+ '||',
767
+ '!',
768
+ '??',
769
+ '??=',
770
+ '&',
771
+ '|',
772
+ '^',
773
+ '~',
774
+ '<<',
775
+ '>>',
776
+ '++',
777
+ '--',
778
+ 'is',
779
+ 'as',
780
+ 'new',
781
+ 'typeof',
782
+ 'sizeof',
783
+ 'nameof',
784
+ 'if',
785
+ 'else',
786
+ 'for',
787
+ 'foreach',
788
+ 'while',
789
+ 'do',
790
+ 'switch',
791
+ 'case',
792
+ 'return',
793
+ 'throw',
794
+ 'break',
795
+ 'continue',
796
+ 'try',
797
+ 'catch',
798
+ 'finally',
799
+ 'await',
800
+ 'yield',
801
+ '.',
802
+ '?.',
803
+ ',',
804
+ ';',
805
+ ':',
806
+ '=>',
807
+ '->',
808
+ ]),
809
+ operandLeafTypes: new Set([
810
+ 'identifier',
811
+ 'integer_literal',
812
+ 'real_literal',
813
+ 'string_literal',
814
+ 'character_literal',
815
+ 'verbatim_string_literal',
816
+ 'interpolated_string_text',
817
+ 'true',
818
+ 'false',
819
+ 'null',
820
+ 'this',
821
+ 'base',
822
+ ]),
823
+ compoundOperators: new Set([
824
+ 'invocation_expression',
825
+ 'element_access_expression',
826
+ 'object_creation_expression',
827
+ ]),
828
+ skipTypes: new Set(['type_argument_list', 'type_parameter_list']),
829
+ };
830
+
831
+ const RUBY_HALSTEAD = {
832
+ operatorLeafTypes: new Set([
833
+ '+',
834
+ '-',
835
+ '*',
836
+ '/',
837
+ '%',
838
+ '**',
839
+ '=',
840
+ '+=',
841
+ '-=',
842
+ '*=',
843
+ '/=',
844
+ '%=',
845
+ '**=',
846
+ '&=',
847
+ '|=',
848
+ '^=',
849
+ '<<=',
850
+ '>>=',
851
+ '==',
852
+ '!=',
853
+ '<',
854
+ '>',
855
+ '<=',
856
+ '>=',
857
+ '<=>',
858
+ '===',
859
+ '=~',
860
+ '!~',
861
+ '&&',
862
+ '||',
863
+ '!',
864
+ 'and',
865
+ 'or',
866
+ 'not',
867
+ '&',
868
+ '|',
869
+ '^',
870
+ '~',
871
+ '<<',
872
+ '>>',
873
+ 'if',
874
+ 'else',
875
+ 'elsif',
876
+ 'unless',
877
+ 'case',
878
+ 'when',
879
+ 'for',
880
+ 'while',
881
+ 'until',
882
+ 'do',
883
+ 'begin',
884
+ 'end',
885
+ 'return',
886
+ 'raise',
887
+ 'break',
888
+ 'next',
889
+ 'redo',
890
+ 'retry',
891
+ 'rescue',
892
+ 'ensure',
893
+ 'yield',
894
+ 'def',
895
+ 'class',
896
+ 'module',
897
+ '.',
898
+ ',',
899
+ ':',
900
+ '::',
901
+ '=>',
902
+ '->',
903
+ ]),
904
+ operandLeafTypes: new Set([
905
+ 'identifier',
906
+ 'constant',
907
+ 'instance_variable',
908
+ 'class_variable',
909
+ 'global_variable',
910
+ 'integer',
911
+ 'float',
912
+ 'string_content',
913
+ 'symbol',
914
+ 'true',
915
+ 'false',
916
+ 'nil',
917
+ 'self',
918
+ ]),
919
+ compoundOperators: new Set(['call', 'element_reference']),
920
+ skipTypes: new Set([]),
921
+ };
922
+
923
+ const PHP_HALSTEAD = {
924
+ operatorLeafTypes: new Set([
925
+ '+',
926
+ '-',
927
+ '*',
928
+ '/',
929
+ '%',
930
+ '**',
931
+ '=',
932
+ '+=',
933
+ '-=',
934
+ '*=',
935
+ '/=',
936
+ '%=',
937
+ '**=',
938
+ '.=',
939
+ '&=',
940
+ '|=',
941
+ '^=',
942
+ '<<=',
943
+ '>>=',
944
+ '==',
945
+ '===',
946
+ '!=',
947
+ '!==',
948
+ '<',
949
+ '>',
950
+ '<=',
951
+ '>=',
952
+ '<=>',
953
+ '&&',
954
+ '||',
955
+ '!',
956
+ 'and',
957
+ 'or',
958
+ 'xor',
959
+ '??',
960
+ '&',
961
+ '|',
962
+ '^',
963
+ '~',
964
+ '<<',
965
+ '>>',
966
+ '++',
967
+ '--',
968
+ 'instanceof',
969
+ 'new',
970
+ 'clone',
971
+ 'if',
972
+ 'else',
973
+ 'elseif',
974
+ 'for',
975
+ 'foreach',
976
+ 'while',
977
+ 'do',
978
+ 'switch',
979
+ 'case',
980
+ 'return',
981
+ 'throw',
982
+ 'break',
983
+ 'continue',
984
+ 'try',
985
+ 'catch',
986
+ 'finally',
987
+ 'echo',
988
+ 'print',
989
+ 'yield',
990
+ '.',
991
+ '->',
992
+ '?->',
993
+ '::',
994
+ ',',
995
+ ';',
996
+ ':',
997
+ '?',
998
+ '=>',
999
+ ]),
1000
+ operandLeafTypes: new Set([
1001
+ 'name',
1002
+ 'variable_name',
1003
+ 'integer',
1004
+ 'float',
1005
+ 'string_content',
1006
+ 'true',
1007
+ 'false',
1008
+ 'null',
1009
+ ]),
1010
+ compoundOperators: new Set([
1011
+ 'function_call_expression',
1012
+ 'member_call_expression',
1013
+ 'scoped_call_expression',
1014
+ 'subscript_expression',
1015
+ 'object_creation_expression',
1016
+ ]),
1017
+ skipTypes: new Set([]),
1018
+ };
1019
+
1020
+ export const HALSTEAD_RULES = new Map([
1021
+ ['javascript', JS_TS_HALSTEAD],
1022
+ ['typescript', JS_TS_HALSTEAD],
1023
+ ['tsx', JS_TS_HALSTEAD],
1024
+ ['python', PYTHON_HALSTEAD],
1025
+ ['go', GO_HALSTEAD],
1026
+ ['rust', RUST_HALSTEAD],
1027
+ ['java', JAVA_HALSTEAD],
1028
+ ['c_sharp', CSHARP_HALSTEAD],
1029
+ ['ruby', RUBY_HALSTEAD],
1030
+ ['php', PHP_HALSTEAD],
1031
+ ]);
1032
+
1033
+ // ─── Halstead Metrics Computation ─────────────────────────────────────────
1034
+
1035
+ /**
1036
+ * Compute Halstead metrics for a function's AST subtree.
1037
+ *
1038
+ * @param {object} functionNode - tree-sitter node for the function
1039
+ * @param {string} language - Language ID
1040
+ * @returns {{ n1: number, n2: number, bigN1: number, bigN2: number, vocabulary: number, length: number, volume: number, difficulty: number, effort: number, bugs: number } | null}
1041
+ */
1042
+ export function computeHalsteadMetrics(functionNode, language) {
1043
+ const rules = HALSTEAD_RULES.get(language);
1044
+ if (!rules) return null;
1045
+
1046
+ const operators = new Map(); // type -> count
1047
+ const operands = new Map(); // text -> count
1048
+
1049
+ function walk(node) {
1050
+ if (!node) return;
1051
+
1052
+ // Skip type annotation subtrees
1053
+ if (rules.skipTypes.has(node.type)) return;
1054
+
1055
+ // Compound operators (non-leaf): count the node type as an operator
1056
+ if (rules.compoundOperators.has(node.type)) {
1057
+ operators.set(node.type, (operators.get(node.type) || 0) + 1);
1058
+ }
1059
+
1060
+ // Leaf nodes: classify as operator or operand
1061
+ if (node.childCount === 0) {
1062
+ if (rules.operatorLeafTypes.has(node.type)) {
1063
+ operators.set(node.type, (operators.get(node.type) || 0) + 1);
1064
+ } else if (rules.operandLeafTypes.has(node.type)) {
1065
+ const text = node.text;
1066
+ operands.set(text, (operands.get(text) || 0) + 1);
1067
+ }
1068
+ }
1069
+
1070
+ for (let i = 0; i < node.childCount; i++) {
1071
+ walk(node.child(i));
1072
+ }
1073
+ }
1074
+
1075
+ walk(functionNode);
1076
+
1077
+ const n1 = operators.size; // distinct operators
1078
+ const n2 = operands.size; // distinct operands
1079
+ let bigN1 = 0; // total operators
1080
+ for (const c of operators.values()) bigN1 += c;
1081
+ let bigN2 = 0; // total operands
1082
+ for (const c of operands.values()) bigN2 += c;
1083
+
1084
+ const vocabulary = n1 + n2;
1085
+ const length = bigN1 + bigN2;
1086
+
1087
+ // Guard against zero
1088
+ const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
1089
+ const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
1090
+ const effort = difficulty * volume;
1091
+ const bugs = volume / 3000;
1092
+
1093
+ return {
1094
+ n1,
1095
+ n2,
1096
+ bigN1,
1097
+ bigN2,
1098
+ vocabulary,
1099
+ length,
1100
+ volume: +volume.toFixed(2),
1101
+ difficulty: +difficulty.toFixed(2),
1102
+ effort: +effort.toFixed(2),
1103
+ bugs: +bugs.toFixed(4),
1104
+ };
1105
+ }
1106
+
1107
+ // ─── LOC Metrics Computation ──────────────────────────────────────────────
1108
+
1109
+ const C_STYLE_PREFIXES = ['//', '/*', '*', '*/'];
1110
+
1111
+ const COMMENT_PREFIXES = new Map([
1112
+ ['javascript', C_STYLE_PREFIXES],
1113
+ ['typescript', C_STYLE_PREFIXES],
1114
+ ['tsx', C_STYLE_PREFIXES],
1115
+ ['go', C_STYLE_PREFIXES],
1116
+ ['rust', C_STYLE_PREFIXES],
1117
+ ['java', C_STYLE_PREFIXES],
1118
+ ['c_sharp', C_STYLE_PREFIXES],
1119
+ ['python', ['#']],
1120
+ ['ruby', ['#']],
1121
+ ['php', ['//', '#', '/*', '*', '*/']],
1122
+ ]);
1123
+
1124
+ /**
1125
+ * Compute LOC metrics from a function node's source text.
1126
+ *
1127
+ * @param {object} functionNode - tree-sitter node
1128
+ * @param {string} [language] - Language ID (falls back to C-style prefixes)
1129
+ * @returns {{ loc: number, sloc: number, commentLines: number }}
1130
+ */
1131
+ export function computeLOCMetrics(functionNode, language) {
1132
+ const text = functionNode.text;
1133
+ const lines = text.split('\n');
1134
+ const loc = lines.length;
1135
+ const prefixes = (language && COMMENT_PREFIXES.get(language)) || C_STYLE_PREFIXES;
1136
+
1137
+ let commentLines = 0;
1138
+ let blankLines = 0;
1139
+
1140
+ for (const line of lines) {
1141
+ const trimmed = line.trim();
1142
+ if (trimmed === '') {
1143
+ blankLines++;
1144
+ } else if (prefixes.some((p) => trimmed.startsWith(p))) {
1145
+ commentLines++;
1146
+ }
1147
+ }
1148
+
1149
+ const sloc = Math.max(1, loc - blankLines - commentLines);
1150
+ return { loc, sloc, commentLines };
1151
+ }
1152
+
1153
+ // ─── Maintainability Index ────────────────────────────────────────────────
1154
+
1155
+ /**
1156
+ * Compute normalized Maintainability Index (0-100 scale).
1157
+ *
1158
+ * Original SEI formula: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
1159
+ * Microsoft normalization: max(0, min(100, MI * 100/171))
1160
+ *
1161
+ * @param {number} volume - Halstead volume
1162
+ * @param {number} cyclomatic - Cyclomatic complexity
1163
+ * @param {number} sloc - Source lines of code
1164
+ * @param {number} [commentRatio] - Comment ratio (0-1), optional
1165
+ * @returns {number} Normalized MI (0-100)
1166
+ */
1167
+ export function computeMaintainabilityIndex(volume, cyclomatic, sloc, commentRatio) {
1168
+ // Guard against zero/negative values in logarithms
1169
+ const safeVolume = Math.max(volume, 1);
1170
+ const safeSLOC = Math.max(sloc, 1);
1171
+
1172
+ let mi = 171 - 5.2 * Math.log(safeVolume) - 0.23 * cyclomatic - 16.2 * Math.log(safeSLOC);
1173
+
1174
+ if (commentRatio != null && commentRatio > 0) {
1175
+ mi += 50 * Math.sin(Math.sqrt(2.4 * commentRatio));
1176
+ }
1177
+
1178
+ // Microsoft normalization: 0-100 scale
1179
+ const normalized = Math.max(0, Math.min(100, (mi * 100) / 171));
1180
+ return +normalized.toFixed(1);
1181
+ }
1182
+
1183
+ // ─── Algorithm: Single-Traversal DFS ──────────────────────────────────────
1184
+
1185
+ /**
1186
+ * Compute cognitive complexity, cyclomatic complexity, and max nesting depth
1187
+ * for a function's AST subtree in a single DFS walk.
1188
+ *
1189
+ * @param {object} functionNode - tree-sitter node for the function body
1190
+ * @param {string} language - Language ID (e.g. 'javascript', 'typescript')
1191
+ * @returns {{ cognitive: number, cyclomatic: number, maxNesting: number } | null}
1192
+ */
1193
+ export function computeFunctionComplexity(functionNode, language) {
1194
+ const rules = COMPLEXITY_RULES.get(language);
1195
+ if (!rules) return null;
1196
+
1197
+ let cognitive = 0;
1198
+ let cyclomatic = 1; // McCabe starts at 1
1199
+ let maxNesting = 0;
1200
+
1201
+ function walk(node, nestingLevel, isTopFunction) {
1202
+ if (!node) return;
1203
+
1204
+ const type = node.type;
1205
+
1206
+ // Track nesting depth
1207
+ if (nestingLevel > maxNesting) maxNesting = nestingLevel;
1208
+
1209
+ // Handle logical operators in binary expressions
1210
+ if (type === rules.logicalNodeType) {
1211
+ const op = node.child(1)?.type;
1212
+ if (op && rules.logicalOperators.has(op)) {
1213
+ // Cyclomatic: +1 for every logical operator
1214
+ cyclomatic++;
1215
+
1216
+ // Cognitive: +1 only when operator changes from the previous sibling sequence
1217
+ // Walk up to check if parent is same type with same operator
1218
+ const parent = node.parent;
1219
+ let sameSequence = false;
1220
+ if (parent && parent.type === rules.logicalNodeType) {
1221
+ const parentOp = parent.child(1)?.type;
1222
+ if (parentOp === op) {
1223
+ sameSequence = true;
1224
+ }
1225
+ }
1226
+ if (!sameSequence) {
1227
+ cognitive++;
1228
+ }
1229
+
1230
+ // Walk children manually to avoid double-counting
1231
+ for (let i = 0; i < node.childCount; i++) {
1232
+ walk(node.child(i), nestingLevel, false);
1233
+ }
1234
+ return;
1235
+ }
1236
+ }
1237
+
1238
+ // Handle optional chaining (cyclomatic only)
1239
+ if (type === rules.optionalChainType) {
1240
+ cyclomatic++;
1241
+ }
1242
+
1243
+ // Handle branch/control flow nodes (skip keyword leaf tokens like Ruby's `if`)
1244
+ if (rules.branchNodes.has(type) && node.childCount > 0) {
1245
+ // Pattern A: else clause wraps if (JS/C#/Rust)
1246
+ if (rules.elseNodeType && type === rules.elseNodeType) {
1247
+ const firstChild = node.namedChild(0);
1248
+ if (firstChild && firstChild.type === rules.ifNodeType) {
1249
+ // else-if: the if_statement child handles its own increment
1250
+ for (let i = 0; i < node.childCount; i++) {
1251
+ walk(node.child(i), nestingLevel, false);
1252
+ }
1253
+ return;
1254
+ }
1255
+ // Plain else
1256
+ cognitive++;
1257
+ for (let i = 0; i < node.childCount; i++) {
1258
+ walk(node.child(i), nestingLevel, false);
1259
+ }
1260
+ return;
1261
+ }
1262
+
1263
+ // Pattern B: explicit elif node (Python/Ruby/PHP)
1264
+ if (rules.elifNodeType && type === rules.elifNodeType) {
1265
+ cognitive++;
1266
+ cyclomatic++;
1267
+ for (let i = 0; i < node.childCount; i++) {
1268
+ walk(node.child(i), nestingLevel, false);
1269
+ }
1270
+ return;
1271
+ }
1272
+
1273
+ // Detect else-if via Pattern A or C
1274
+ let isElseIf = false;
1275
+ if (type === rules.ifNodeType) {
1276
+ if (rules.elseViaAlternative) {
1277
+ // Pattern C (Go/Java): if_statement is the alternative of parent if_statement
1278
+ isElseIf =
1279
+ node.parent?.type === rules.ifNodeType &&
1280
+ node.parent.childForFieldName('alternative')?.id === node.id;
1281
+ } else if (rules.elseNodeType) {
1282
+ // Pattern A (JS/C#/Rust): if_statement inside else_clause
1283
+ isElseIf = node.parent?.type === rules.elseNodeType;
1284
+ }
1285
+ }
1286
+
1287
+ if (isElseIf) {
1288
+ cognitive++;
1289
+ cyclomatic++;
1290
+ for (let i = 0; i < node.childCount; i++) {
1291
+ walk(node.child(i), nestingLevel, false);
1292
+ }
1293
+ return;
1294
+ }
1295
+
1296
+ // Regular branch node
1297
+ cognitive += 1 + nestingLevel; // structural + nesting
1298
+ cyclomatic++;
1299
+
1300
+ // Switch-like nodes don't add cyclomatic themselves (cases do)
1301
+ if (rules.switchLikeNodes?.has(type)) {
1302
+ cyclomatic--; // Undo the ++ above; cases handle cyclomatic
1303
+ }
1304
+
1305
+ if (rules.nestingNodes.has(type)) {
1306
+ for (let i = 0; i < node.childCount; i++) {
1307
+ walk(node.child(i), nestingLevel + 1, false);
1308
+ }
1309
+ return;
1310
+ }
1311
+ }
1312
+
1313
+ // Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
1314
+ if (
1315
+ rules.elseViaAlternative &&
1316
+ type !== rules.ifNodeType &&
1317
+ node.parent?.type === rules.ifNodeType &&
1318
+ node.parent.childForFieldName('alternative')?.id === node.id
1319
+ ) {
1320
+ cognitive++;
1321
+ for (let i = 0; i < node.childCount; i++) {
1322
+ walk(node.child(i), nestingLevel, false);
1323
+ }
1324
+ return;
1325
+ }
1326
+
1327
+ // Handle case nodes (cyclomatic only, skip keyword leaves)
1328
+ if (rules.caseNodes.has(type) && node.childCount > 0) {
1329
+ cyclomatic++;
1330
+ }
1331
+
1332
+ // Handle nested function definitions (increase nesting)
1333
+ if (!isTopFunction && rules.functionNodes.has(type)) {
1334
+ for (let i = 0; i < node.childCount; i++) {
1335
+ walk(node.child(i), nestingLevel + 1, false);
1336
+ }
1337
+ return;
1338
+ }
1339
+
1340
+ // Walk children
1341
+ for (let i = 0; i < node.childCount; i++) {
1342
+ walk(node.child(i), nestingLevel, false);
1343
+ }
1344
+ }
1345
+
1346
+ walk(functionNode, 0, true);
1347
+
1348
+ return { cognitive, cyclomatic, maxNesting };
1349
+ }
1350
+
1351
+ // ─── Merged Single-Pass Computation ───────────────────────────────────────
1352
+
1353
+ /**
1354
+ * Compute all metrics (complexity + Halstead + LOC + MI) in a single DFS walk.
1355
+ * Merges computeFunctionComplexity and computeHalsteadMetrics into one tree
1356
+ * traversal, avoiding two separate DFS walks per function node at build time.
1357
+ * LOC is text-based (not tree-based) and computed separately (very cheap).
1358
+ *
1359
+ * @param {object} functionNode - tree-sitter node for the function
1360
+ * @param {string} langId - Language ID (e.g. 'javascript', 'python')
1361
+ * @returns {{ cognitive: number, cyclomatic: number, maxNesting: number, halstead: object|null, loc: object, mi: number } | null}
1362
+ */
1363
+ export function computeAllMetrics(functionNode, langId) {
1364
+ const cRules = COMPLEXITY_RULES.get(langId);
1365
+ if (!cRules) return null;
1366
+ const hRules = HALSTEAD_RULES.get(langId);
1367
+
1368
+ // ── Complexity state ──
1369
+ let cognitive = 0;
1370
+ let cyclomatic = 1; // McCabe starts at 1
1371
+ let maxNesting = 0;
1372
+
1373
+ // ── Halstead state ──
1374
+ const operators = hRules ? new Map() : null;
1375
+ const operands = hRules ? new Map() : null;
1376
+
1377
+ function walk(node, nestingLevel, isTopFunction, halsteadSkip) {
1378
+ if (!node) return;
1379
+
1380
+ const type = node.type;
1381
+
1382
+ // ── Halstead classification ──
1383
+ // Propagate skip through type-annotation subtrees (e.g. TS generics, Java type params)
1384
+ const skipH = halsteadSkip || (hRules ? hRules.skipTypes.has(type) : false);
1385
+ if (hRules && !skipH) {
1386
+ // Compound operators (non-leaf): count node type as operator
1387
+ if (hRules.compoundOperators.has(type)) {
1388
+ operators.set(type, (operators.get(type) || 0) + 1);
1389
+ }
1390
+ // Leaf nodes: classify as operator or operand
1391
+ if (node.childCount === 0) {
1392
+ if (hRules.operatorLeafTypes.has(type)) {
1393
+ operators.set(type, (operators.get(type) || 0) + 1);
1394
+ } else if (hRules.operandLeafTypes.has(type)) {
1395
+ const text = node.text;
1396
+ operands.set(text, (operands.get(text) || 0) + 1);
1397
+ }
1398
+ }
1399
+ }
1400
+
1401
+ // ── Complexity: track nesting depth ──
1402
+ if (nestingLevel > maxNesting) maxNesting = nestingLevel;
1403
+
1404
+ // Handle logical operators in binary expressions
1405
+ if (type === cRules.logicalNodeType) {
1406
+ const op = node.child(1)?.type;
1407
+ if (op && cRules.logicalOperators.has(op)) {
1408
+ cyclomatic++;
1409
+ const parent = node.parent;
1410
+ let sameSequence = false;
1411
+ if (parent && parent.type === cRules.logicalNodeType) {
1412
+ const parentOp = parent.child(1)?.type;
1413
+ if (parentOp === op) sameSequence = true;
1414
+ }
1415
+ if (!sameSequence) cognitive++;
1416
+ for (let i = 0; i < node.childCount; i++) {
1417
+ walk(node.child(i), nestingLevel, false, skipH);
1418
+ }
1419
+ return;
1420
+ }
1421
+ }
1422
+
1423
+ // Handle optional chaining (cyclomatic only)
1424
+ if (type === cRules.optionalChainType) {
1425
+ cyclomatic++;
1426
+ }
1427
+
1428
+ // Handle branch/control flow nodes (skip keyword leaf tokens like Ruby's `if`)
1429
+ if (cRules.branchNodes.has(type) && node.childCount > 0) {
1430
+ // Pattern A: else clause wraps if (JS/C#/Rust)
1431
+ if (cRules.elseNodeType && type === cRules.elseNodeType) {
1432
+ const firstChild = node.namedChild(0);
1433
+ if (firstChild && firstChild.type === cRules.ifNodeType) {
1434
+ for (let i = 0; i < node.childCount; i++) {
1435
+ walk(node.child(i), nestingLevel, false, skipH);
1436
+ }
1437
+ return;
1438
+ }
1439
+ cognitive++;
1440
+ for (let i = 0; i < node.childCount; i++) {
1441
+ walk(node.child(i), nestingLevel, false, skipH);
1442
+ }
1443
+ return;
1444
+ }
1445
+
1446
+ // Pattern B: explicit elif node (Python/Ruby/PHP)
1447
+ if (cRules.elifNodeType && type === cRules.elifNodeType) {
1448
+ cognitive++;
1449
+ cyclomatic++;
1450
+ for (let i = 0; i < node.childCount; i++) {
1451
+ walk(node.child(i), nestingLevel, false, skipH);
1452
+ }
1453
+ return;
1454
+ }
1455
+
1456
+ // Detect else-if via Pattern A or C
1457
+ let isElseIf = false;
1458
+ if (type === cRules.ifNodeType) {
1459
+ if (cRules.elseViaAlternative) {
1460
+ isElseIf =
1461
+ node.parent?.type === cRules.ifNodeType &&
1462
+ node.parent.childForFieldName('alternative')?.id === node.id;
1463
+ } else if (cRules.elseNodeType) {
1464
+ isElseIf = node.parent?.type === cRules.elseNodeType;
1465
+ }
1466
+ }
1467
+
1468
+ if (isElseIf) {
1469
+ cognitive++;
1470
+ cyclomatic++;
1471
+ for (let i = 0; i < node.childCount; i++) {
1472
+ walk(node.child(i), nestingLevel, false, skipH);
1473
+ }
1474
+ return;
1475
+ }
1476
+
1477
+ // Regular branch node
1478
+ cognitive += 1 + nestingLevel;
1479
+ cyclomatic++;
1480
+
1481
+ // Switch-like nodes don't add cyclomatic themselves (cases do)
1482
+ if (cRules.switchLikeNodes?.has(type)) {
1483
+ cyclomatic--;
1484
+ }
1485
+
1486
+ if (cRules.nestingNodes.has(type)) {
1487
+ for (let i = 0; i < node.childCount; i++) {
1488
+ walk(node.child(i), nestingLevel + 1, false, skipH);
1489
+ }
1490
+ return;
1491
+ }
1492
+ }
1493
+
1494
+ // Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
1495
+ if (
1496
+ cRules.elseViaAlternative &&
1497
+ type !== cRules.ifNodeType &&
1498
+ node.parent?.type === cRules.ifNodeType &&
1499
+ node.parent.childForFieldName('alternative')?.id === node.id
1500
+ ) {
1501
+ cognitive++;
1502
+ for (let i = 0; i < node.childCount; i++) {
1503
+ walk(node.child(i), nestingLevel, false, skipH);
1504
+ }
1505
+ return;
1506
+ }
1507
+
1508
+ // Handle case nodes (cyclomatic only, skip keyword leaves)
1509
+ if (cRules.caseNodes.has(type) && node.childCount > 0) {
1510
+ cyclomatic++;
1511
+ }
1512
+
1513
+ // Handle nested function definitions (increase nesting)
1514
+ if (!isTopFunction && cRules.functionNodes.has(type)) {
1515
+ for (let i = 0; i < node.childCount; i++) {
1516
+ walk(node.child(i), nestingLevel + 1, false, skipH);
1517
+ }
1518
+ return;
1519
+ }
1520
+
1521
+ // Walk children
1522
+ for (let i = 0; i < node.childCount; i++) {
1523
+ walk(node.child(i), nestingLevel, false, skipH);
1524
+ }
1525
+ }
1526
+
1527
+ walk(functionNode, 0, true, false);
1528
+
1529
+ // ── Compute Halstead derived metrics ──
1530
+ let halstead = null;
1531
+ if (hRules && operators && operands) {
1532
+ const n1 = operators.size;
1533
+ const n2 = operands.size;
1534
+ let bigN1 = 0;
1535
+ for (const c of operators.values()) bigN1 += c;
1536
+ let bigN2 = 0;
1537
+ for (const c of operands.values()) bigN2 += c;
1538
+
1539
+ const vocabulary = n1 + n2;
1540
+ const length = bigN1 + bigN2;
1541
+ const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
1542
+ const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
1543
+ const effort = difficulty * volume;
1544
+ const bugs = volume / 3000;
1545
+
1546
+ halstead = {
1547
+ n1,
1548
+ n2,
1549
+ bigN1,
1550
+ bigN2,
1551
+ vocabulary,
1552
+ length,
1553
+ volume: +volume.toFixed(2),
1554
+ difficulty: +difficulty.toFixed(2),
1555
+ effort: +effort.toFixed(2),
1556
+ bugs: +bugs.toFixed(4),
1557
+ };
1558
+ }
1559
+
1560
+ // ── LOC metrics (text-based, cheap) ──
1561
+ const loc = computeLOCMetrics(functionNode, langId);
1562
+
1563
+ // ── Maintainability Index ──
1564
+ const volume = halstead ? halstead.volume : 0;
1565
+ const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
1566
+ const mi = computeMaintainabilityIndex(volume, cyclomatic, loc.sloc, commentRatio);
1567
+
1568
+ return { cognitive, cyclomatic, maxNesting, halstead, loc, mi };
1569
+ }
1570
+
1571
+ // ─── Build-Time: Compute Metrics for Changed Files ────────────────────────
1572
+
1573
+ /**
1574
+ * Find the function body node in a parse tree that matches a given line range.
1575
+ */
1576
+ function findFunctionNode(rootNode, startLine, _endLine, rules) {
1577
+ // tree-sitter lines are 0-indexed
1578
+ const targetStart = startLine - 1;
1579
+
1580
+ let best = null;
1581
+
1582
+ function search(node) {
1583
+ const nodeStart = node.startPosition.row;
1584
+ const nodeEnd = node.endPosition.row;
1585
+
1586
+ // Prune branches outside range
1587
+ if (nodeEnd < targetStart || nodeStart > targetStart + 1) return;
1588
+
1589
+ if (rules.functionNodes.has(node.type) && nodeStart === targetStart) {
1590
+ // Found a function node at the right position — pick it
1591
+ if (!best || nodeEnd - nodeStart < best.endPosition.row - best.startPosition.row) {
1592
+ best = node;
1593
+ }
1594
+ }
1595
+
1596
+ for (let i = 0; i < node.childCount; i++) {
1597
+ search(node.child(i));
1598
+ }
1599
+ }
1600
+
1601
+ search(rootNode);
1602
+ return best;
1603
+ }
1604
+
1605
+ /**
1606
+ * Re-parse changed files with WASM tree-sitter, find function AST subtrees,
1607
+ * compute complexity, and upsert into function_complexity table.
1608
+ *
1609
+ * @param {object} db - open better-sqlite3 database (read-write)
1610
+ * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, ... }>
1611
+ * @param {string} rootDir - absolute project root path
1612
+ * @param {object} [engineOpts] - engine options (unused; always uses WASM for AST)
1613
+ */
1614
+ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOpts) {
1615
+ // Only initialize WASM parsers if some files lack both a cached tree AND pre-computed complexity
1616
+ let parsers = null;
1617
+ let extToLang = null;
1618
+ let needsFallback = false;
1619
+ for (const [relPath, symbols] of fileSymbols) {
1620
+ if (!symbols._tree) {
1621
+ // Only consider files whose language actually has complexity rules
1622
+ const ext = path.extname(relPath).toLowerCase();
1623
+ if (!COMPLEXITY_EXTENSIONS.has(ext)) continue;
1624
+ // Check if all function/method defs have pre-computed complexity (native engine)
1625
+ const hasPrecomputed = symbols.definitions.every(
1626
+ (d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
1627
+ );
1628
+ if (!hasPrecomputed) {
1629
+ needsFallback = true;
1630
+ break;
1631
+ }
1632
+ }
1633
+ }
1634
+ if (needsFallback) {
1635
+ const { createParsers } = await import('./parser.js');
1636
+ parsers = await createParsers();
1637
+ extToLang = new Map();
1638
+ for (const entry of LANGUAGE_REGISTRY) {
1639
+ for (const ext of entry.extensions) {
1640
+ extToLang.set(ext, entry.id);
1641
+ }
1642
+ }
1643
+ }
1644
+
1645
+ const { getParser } = await import('./parser.js');
1646
+
1647
+ const upsert = db.prepare(
1648
+ `INSERT OR REPLACE INTO function_complexity
1649
+ (node_id, cognitive, cyclomatic, max_nesting,
1650
+ loc, sloc, comment_lines,
1651
+ halstead_n1, halstead_n2, halstead_big_n1, halstead_big_n2,
1652
+ halstead_vocabulary, halstead_length, halstead_volume,
1653
+ halstead_difficulty, halstead_effort, halstead_bugs,
1654
+ maintainability_index)
1655
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1656
+ );
1657
+ const getNodeId = db.prepare(
1658
+ "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
1659
+ );
1660
+
1661
+ let analyzed = 0;
1662
+
1663
+ const tx = db.transaction(() => {
1664
+ for (const [relPath, symbols] of fileSymbols) {
1665
+ // Check if all function/method defs have pre-computed complexity
1666
+ const allPrecomputed = symbols.definitions.every(
1667
+ (d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
1668
+ );
1669
+
1670
+ let tree = symbols._tree;
1671
+ let langId = symbols._langId;
1672
+
1673
+ // Only attempt WASM fallback if we actually need AST-based computation
1674
+ if (!allPrecomputed && !tree) {
1675
+ const ext = path.extname(relPath).toLowerCase();
1676
+ if (!COMPLEXITY_EXTENSIONS.has(ext)) continue; // Language has no complexity rules
1677
+ if (!extToLang) continue; // No WASM parsers available
1678
+ langId = extToLang.get(ext);
1679
+ if (!langId) continue;
1680
+
1681
+ const absPath = path.join(rootDir, relPath);
1682
+ let code;
1683
+ try {
1684
+ code = fs.readFileSync(absPath, 'utf-8');
1685
+ } catch {
1686
+ continue;
1687
+ }
1688
+
1689
+ const parser = getParser(parsers, absPath);
1690
+ if (!parser) continue;
1691
+
1692
+ try {
1693
+ tree = parser.parse(code);
1694
+ } catch {
1695
+ continue;
1696
+ }
1697
+ }
1698
+
1699
+ const rules = langId ? COMPLEXITY_RULES.get(langId) : null;
1700
+
1701
+ for (const def of symbols.definitions) {
1702
+ if (def.kind !== 'function' && def.kind !== 'method') continue;
1703
+ if (!def.line) continue;
1704
+
1705
+ // Use pre-computed complexity from native engine if available
1706
+ if (def.complexity) {
1707
+ const row = getNodeId.get(def.name, relPath, def.line);
1708
+ if (!row) continue;
1709
+ const ch = def.complexity.halstead;
1710
+ const cl = def.complexity.loc;
1711
+ upsert.run(
1712
+ row.id,
1713
+ def.complexity.cognitive,
1714
+ def.complexity.cyclomatic,
1715
+ def.complexity.maxNesting ?? 0,
1716
+ cl ? cl.loc : 0,
1717
+ cl ? cl.sloc : 0,
1718
+ cl ? cl.commentLines : 0,
1719
+ ch ? ch.n1 : 0,
1720
+ ch ? ch.n2 : 0,
1721
+ ch ? ch.bigN1 : 0,
1722
+ ch ? ch.bigN2 : 0,
1723
+ ch ? ch.vocabulary : 0,
1724
+ ch ? ch.length : 0,
1725
+ ch ? ch.volume : 0,
1726
+ ch ? ch.difficulty : 0,
1727
+ ch ? ch.effort : 0,
1728
+ ch ? ch.bugs : 0,
1729
+ def.complexity.maintainabilityIndex ?? 0,
1730
+ );
1731
+ analyzed++;
1732
+ continue;
1733
+ }
1734
+
1735
+ // Fallback: compute from AST tree
1736
+ if (!tree || !rules) continue;
1737
+
1738
+ const funcNode = findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
1739
+ if (!funcNode) continue;
1740
+
1741
+ // Single-pass: complexity + Halstead + LOC + MI in one DFS walk
1742
+ const metrics = computeAllMetrics(funcNode, langId);
1743
+ if (!metrics) continue;
1744
+
1745
+ const row = getNodeId.get(def.name, relPath, def.line);
1746
+ if (!row) continue;
1747
+
1748
+ const h = metrics.halstead;
1749
+ upsert.run(
1750
+ row.id,
1751
+ metrics.cognitive,
1752
+ metrics.cyclomatic,
1753
+ metrics.maxNesting,
1754
+ metrics.loc.loc,
1755
+ metrics.loc.sloc,
1756
+ metrics.loc.commentLines,
1757
+ h ? h.n1 : 0,
1758
+ h ? h.n2 : 0,
1759
+ h ? h.bigN1 : 0,
1760
+ h ? h.bigN2 : 0,
1761
+ h ? h.vocabulary : 0,
1762
+ h ? h.length : 0,
1763
+ h ? h.volume : 0,
1764
+ h ? h.difficulty : 0,
1765
+ h ? h.effort : 0,
1766
+ h ? h.bugs : 0,
1767
+ metrics.mi,
1768
+ );
1769
+ analyzed++;
1770
+ }
1771
+
1772
+ // Release cached tree for GC
1773
+ symbols._tree = null;
1774
+ }
1775
+ });
1776
+
1777
+ tx();
1778
+
1779
+ if (analyzed > 0) {
1780
+ info(`Complexity: ${analyzed} functions analyzed`);
1781
+ }
1782
+ }
1783
+
1784
+ // ─── Query-Time Functions ─────────────────────────────────────────────────
1785
+
1786
+ /**
1787
+ * Return structured complexity data for querying.
1788
+ *
1789
+ * @param {string} [customDbPath] - Path to graph.db
1790
+ * @param {object} [opts] - Options
1791
+ * @param {string} [opts.target] - Function name filter (partial match)
1792
+ * @param {number} [opts.limit] - Max results (default: 20)
1793
+ * @param {string} [opts.sort] - Sort by: cognitive | cyclomatic | nesting (default: cognitive)
1794
+ * @param {boolean} [opts.aboveThreshold] - Only functions above warn thresholds
1795
+ * @param {string} [opts.file] - Filter by file (partial match)
1796
+ * @param {string} [opts.kind] - Filter by symbol kind
1797
+ * @param {boolean} [opts.noTests] - Exclude test files
1798
+ * @returns {{ functions: object[], summary: object, thresholds: object }}
1799
+ */
1800
+ export function complexityData(customDbPath, opts = {}) {
1801
+ const db = openReadonlyOrFail(customDbPath);
1802
+ const limit = opts.limit || 20;
1803
+ const sort = opts.sort || 'cognitive';
1804
+ const noTests = opts.noTests || false;
1805
+ const aboveThreshold = opts.aboveThreshold || false;
1806
+ const target = opts.target || null;
1807
+ const fileFilter = opts.file || null;
1808
+ const kindFilter = opts.kind || null;
1809
+
1810
+ // Load thresholds from config
1811
+ const config = loadConfig(process.cwd());
1812
+ const thresholds = config.manifesto?.rules || {
1813
+ cognitive: { warn: 15, fail: null },
1814
+ cyclomatic: { warn: 10, fail: null },
1815
+ maxNesting: { warn: 4, fail: null },
1816
+ maintainabilityIndex: { warn: 20, fail: null },
1817
+ };
1818
+
1819
+ // Build query
1820
+ let where = "WHERE n.kind IN ('function','method')";
1821
+ const params = [];
1822
+
1823
+ if (noTests) {
1824
+ where += ` AND n.file NOT LIKE '%.test.%'
1825
+ AND n.file NOT LIKE '%.spec.%'
1826
+ AND n.file NOT LIKE '%__test__%'
1827
+ AND n.file NOT LIKE '%__tests__%'
1828
+ AND n.file NOT LIKE '%.stories.%'`;
1829
+ }
1830
+ if (target) {
1831
+ where += ' AND n.name LIKE ?';
1832
+ params.push(`%${target}%`);
1833
+ }
1834
+ if (fileFilter) {
1835
+ where += ' AND n.file LIKE ?';
1836
+ params.push(`%${fileFilter}%`);
1837
+ }
1838
+ if (kindFilter) {
1839
+ where += ' AND n.kind = ?';
1840
+ params.push(kindFilter);
1841
+ }
1842
+
1843
+ const isValidThreshold = (v) => typeof v === 'number' && Number.isFinite(v);
1844
+
1845
+ let having = '';
1846
+ if (aboveThreshold) {
1847
+ const conditions = [];
1848
+ if (isValidThreshold(thresholds.cognitive?.warn)) {
1849
+ conditions.push(`fc.cognitive >= ${thresholds.cognitive.warn}`);
1850
+ }
1851
+ if (isValidThreshold(thresholds.cyclomatic?.warn)) {
1852
+ conditions.push(`fc.cyclomatic >= ${thresholds.cyclomatic.warn}`);
1853
+ }
1854
+ if (isValidThreshold(thresholds.maxNesting?.warn)) {
1855
+ conditions.push(`fc.max_nesting >= ${thresholds.maxNesting.warn}`);
1856
+ }
1857
+ if (isValidThreshold(thresholds.maintainabilityIndex?.warn)) {
1858
+ conditions.push(
1859
+ `fc.maintainability_index > 0 AND fc.maintainability_index <= ${thresholds.maintainabilityIndex.warn}`,
1860
+ );
1861
+ }
1862
+ if (conditions.length > 0) {
1863
+ having = `AND (${conditions.join(' OR ')})`;
1864
+ }
1865
+ }
1866
+
1867
+ const orderMap = {
1868
+ cognitive: 'fc.cognitive DESC',
1869
+ cyclomatic: 'fc.cyclomatic DESC',
1870
+ nesting: 'fc.max_nesting DESC',
1871
+ mi: 'fc.maintainability_index ASC',
1872
+ volume: 'fc.halstead_volume DESC',
1873
+ effort: 'fc.halstead_effort DESC',
1874
+ bugs: 'fc.halstead_bugs DESC',
1875
+ loc: 'fc.loc DESC',
1876
+ };
1877
+ const orderBy = orderMap[sort] || 'fc.cognitive DESC';
1878
+
1879
+ let rows;
1880
+ try {
1881
+ rows = db
1882
+ .prepare(
1883
+ `SELECT n.name, n.kind, n.file, n.line, n.end_line,
1884
+ fc.cognitive, fc.cyclomatic, fc.max_nesting,
1885
+ fc.loc, fc.sloc, fc.maintainability_index,
1886
+ fc.halstead_volume, fc.halstead_difficulty, fc.halstead_effort, fc.halstead_bugs
1887
+ FROM function_complexity fc
1888
+ JOIN nodes n ON fc.node_id = n.id
1889
+ ${where} ${having}
1890
+ ORDER BY ${orderBy}
1891
+ LIMIT ?`,
1892
+ )
1893
+ .all(...params, limit);
1894
+ } catch {
1895
+ db.close();
1896
+ return { functions: [], summary: null, thresholds };
1897
+ }
1898
+
1899
+ // Post-filter test files if needed (belt-and-suspenders for isTestFile)
1900
+ const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
1901
+
1902
+ const functions = filtered.map((r) => {
1903
+ const exceeds = [];
1904
+ if (isValidThreshold(thresholds.cognitive?.warn) && r.cognitive >= thresholds.cognitive.warn)
1905
+ exceeds.push('cognitive');
1906
+ if (isValidThreshold(thresholds.cyclomatic?.warn) && r.cyclomatic >= thresholds.cyclomatic.warn)
1907
+ exceeds.push('cyclomatic');
1908
+ if (
1909
+ isValidThreshold(thresholds.maxNesting?.warn) &&
1910
+ r.max_nesting >= thresholds.maxNesting.warn
1911
+ )
1912
+ exceeds.push('maxNesting');
1913
+ if (
1914
+ isValidThreshold(thresholds.maintainabilityIndex?.warn) &&
1915
+ r.maintainability_index > 0 &&
1916
+ r.maintainability_index <= thresholds.maintainabilityIndex.warn
1917
+ )
1918
+ exceeds.push('maintainabilityIndex');
1919
+
1920
+ return {
1921
+ name: r.name,
1922
+ kind: r.kind,
1923
+ file: r.file,
1924
+ line: r.line,
1925
+ endLine: r.end_line || null,
1926
+ cognitive: r.cognitive,
1927
+ cyclomatic: r.cyclomatic,
1928
+ maxNesting: r.max_nesting,
1929
+ loc: r.loc || 0,
1930
+ sloc: r.sloc || 0,
1931
+ maintainabilityIndex: r.maintainability_index || 0,
1932
+ halstead: {
1933
+ volume: r.halstead_volume || 0,
1934
+ difficulty: r.halstead_difficulty || 0,
1935
+ effort: r.halstead_effort || 0,
1936
+ bugs: r.halstead_bugs || 0,
1937
+ },
1938
+ exceeds: exceeds.length > 0 ? exceeds : undefined,
1939
+ };
1940
+ });
1941
+
1942
+ // Summary stats
1943
+ let summary = null;
1944
+ try {
1945
+ const allRows = db
1946
+ .prepare(
1947
+ `SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
1948
+ FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
1949
+ WHERE n.kind IN ('function','method')
1950
+ ${noTests ? `AND n.file NOT LIKE '%.test.%' AND n.file NOT LIKE '%.spec.%' AND n.file NOT LIKE '%__test__%' AND n.file NOT LIKE '%__tests__%' AND n.file NOT LIKE '%.stories.%'` : ''}`,
1951
+ )
1952
+ .all();
1953
+
1954
+ if (allRows.length > 0) {
1955
+ const miValues = allRows.map((r) => r.maintainability_index || 0);
1956
+ summary = {
1957
+ analyzed: allRows.length,
1958
+ avgCognitive: +(allRows.reduce((s, r) => s + r.cognitive, 0) / allRows.length).toFixed(1),
1959
+ avgCyclomatic: +(allRows.reduce((s, r) => s + r.cyclomatic, 0) / allRows.length).toFixed(1),
1960
+ maxCognitive: Math.max(...allRows.map((r) => r.cognitive)),
1961
+ maxCyclomatic: Math.max(...allRows.map((r) => r.cyclomatic)),
1962
+ avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
1963
+ minMI: +Math.min(...miValues).toFixed(1),
1964
+ aboveWarn: allRows.filter(
1965
+ (r) =>
1966
+ (isValidThreshold(thresholds.cognitive?.warn) &&
1967
+ r.cognitive >= thresholds.cognitive.warn) ||
1968
+ (isValidThreshold(thresholds.cyclomatic?.warn) &&
1969
+ r.cyclomatic >= thresholds.cyclomatic.warn) ||
1970
+ (isValidThreshold(thresholds.maxNesting?.warn) &&
1971
+ r.max_nesting >= thresholds.maxNesting.warn) ||
1972
+ (isValidThreshold(thresholds.maintainabilityIndex?.warn) &&
1973
+ r.maintainability_index > 0 &&
1974
+ r.maintainability_index <= thresholds.maintainabilityIndex.warn),
1975
+ ).length,
1976
+ };
1977
+ }
1978
+ } catch {
1979
+ /* ignore */
1980
+ }
1981
+
1982
+ db.close();
1983
+ return { functions, summary, thresholds };
1984
+ }
1985
+
1986
+ /**
1987
+ * Format complexity output for CLI display.
1988
+ */
1989
+ export function complexity(customDbPath, opts = {}) {
1990
+ const data = complexityData(customDbPath, opts);
1991
+
1992
+ if (opts.json) {
1993
+ console.log(JSON.stringify(data, null, 2));
1994
+ return;
1995
+ }
1996
+
1997
+ if (data.functions.length === 0) {
1998
+ if (data.summary === null) {
1999
+ console.log(
2000
+ '\nNo complexity data found. Run "codegraph build" first to analyze your codebase.\n',
2001
+ );
2002
+ } else {
2003
+ console.log('\nNo functions match the given filters.\n');
2004
+ }
2005
+ return;
2006
+ }
2007
+
2008
+ const header = opts.aboveThreshold ? 'Functions Above Threshold' : 'Function Complexity';
2009
+ console.log(`\n# ${header}\n`);
2010
+
2011
+ if (opts.health) {
2012
+ // Health-focused view with Halstead + MI columns
2013
+ console.log(
2014
+ ` ${'Function'.padEnd(35)} ${'File'.padEnd(25)} ${'MI'.padStart(5)} ${'Vol'.padStart(7)} ${'Diff'.padStart(6)} ${'Effort'.padStart(9)} ${'Bugs'.padStart(6)} ${'LOC'.padStart(5)} ${'SLOC'.padStart(5)}`,
2015
+ );
2016
+ console.log(
2017
+ ` ${'─'.repeat(35)} ${'─'.repeat(25)} ${'─'.repeat(5)} ${'─'.repeat(7)} ${'─'.repeat(6)} ${'─'.repeat(9)} ${'─'.repeat(6)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
2018
+ );
2019
+
2020
+ for (const fn of data.functions) {
2021
+ const name = fn.name.length > 33 ? `${fn.name.slice(0, 32)}…` : fn.name;
2022
+ const file = fn.file.length > 23 ? `…${fn.file.slice(-22)}` : fn.file;
2023
+ const miWarn = fn.exceeds?.includes('maintainabilityIndex') ? '!' : ' ';
2024
+ console.log(
2025
+ ` ${name.padEnd(35)} ${file.padEnd(25)} ${String(fn.maintainabilityIndex).padStart(5)}${miWarn}${String(fn.halstead.volume).padStart(7)} ${String(fn.halstead.difficulty).padStart(6)} ${String(fn.halstead.effort).padStart(9)} ${String(fn.halstead.bugs).padStart(6)} ${String(fn.loc).padStart(5)} ${String(fn.sloc).padStart(5)}`,
2026
+ );
2027
+ }
2028
+ } else {
2029
+ // Default view with MI column appended
2030
+ console.log(
2031
+ ` ${'Function'.padEnd(40)} ${'File'.padEnd(30)} ${'Cog'.padStart(4)} ${'Cyc'.padStart(4)} ${'Nest'.padStart(5)} ${'MI'.padStart(5)}`,
2032
+ );
2033
+ console.log(
2034
+ ` ${'─'.repeat(40)} ${'─'.repeat(30)} ${'─'.repeat(4)} ${'─'.repeat(4)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
2035
+ );
2036
+
2037
+ for (const fn of data.functions) {
2038
+ const name = fn.name.length > 38 ? `${fn.name.slice(0, 37)}…` : fn.name;
2039
+ const file = fn.file.length > 28 ? `…${fn.file.slice(-27)}` : fn.file;
2040
+ const warn = fn.exceeds ? ' !' : '';
2041
+ const mi = fn.maintainabilityIndex > 0 ? String(fn.maintainabilityIndex) : '-';
2042
+ console.log(
2043
+ ` ${name.padEnd(40)} ${file.padEnd(30)} ${String(fn.cognitive).padStart(4)} ${String(fn.cyclomatic).padStart(4)} ${String(fn.maxNesting).padStart(5)} ${mi.padStart(5)}${warn}`,
2044
+ );
2045
+ }
2046
+ }
2047
+
2048
+ if (data.summary) {
2049
+ const s = data.summary;
2050
+ const miPart = s.avgMI != null ? ` | avg MI: ${s.avgMI}` : '';
2051
+ console.log(
2052
+ `\n ${s.analyzed} functions analyzed | avg cognitive: ${s.avgCognitive} | avg cyclomatic: ${s.avgCyclomatic}${miPart} | ${s.aboveWarn} above threshold`,
2053
+ );
2054
+ }
2055
+ console.log();
2056
+ }