@optave/codegraph 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +5 -5
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +8 -9
  4. package/src/ast-analysis/rules/csharp.js +201 -0
  5. package/src/ast-analysis/rules/go.js +182 -0
  6. package/src/ast-analysis/rules/index.js +82 -0
  7. package/src/ast-analysis/rules/java.js +175 -0
  8. package/src/ast-analysis/rules/javascript.js +246 -0
  9. package/src/ast-analysis/rules/php.js +219 -0
  10. package/src/ast-analysis/rules/python.js +196 -0
  11. package/src/ast-analysis/rules/ruby.js +204 -0
  12. package/src/ast-analysis/rules/rust.js +173 -0
  13. package/src/ast-analysis/shared.js +223 -0
  14. package/src/ast.js +15 -28
  15. package/src/audit.js +4 -5
  16. package/src/boundaries.js +1 -1
  17. package/src/branch-compare.js +84 -79
  18. package/src/builder.js +0 -5
  19. package/src/cfg.js +106 -338
  20. package/src/check.js +3 -3
  21. package/src/cli.js +99 -179
  22. package/src/cochange.js +1 -1
  23. package/src/communities.js +13 -16
  24. package/src/complexity.js +196 -1239
  25. package/src/cycles.js +1 -1
  26. package/src/dataflow.js +269 -694
  27. package/src/db/connection.js +88 -0
  28. package/src/db/migrations.js +312 -0
  29. package/src/db/query-builder.js +280 -0
  30. package/src/db/repository.js +134 -0
  31. package/src/db.js +19 -399
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +161 -162
  35. package/src/index.js +34 -1
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +37 -20
  39. package/src/owners.js +132 -132
  40. package/src/queries-cli.js +866 -0
  41. package/src/queries.js +1323 -2267
  42. package/src/result-formatter.js +21 -0
  43. package/src/sequence.js +177 -182
  44. package/src/structure.js +200 -199
  45. package/src/test-filter.js +7 -0
  46. package/src/triage.js +120 -162
  47. package/src/viewer.js +1 -1
package/src/complexity.js CHANGED
@@ -1,1035 +1,24 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { COMPLEXITY_RULES, HALSTEAD_RULES } from './ast-analysis/rules/index.js';
4
+ import {
5
+ findFunctionNode as _findFunctionNode,
6
+ buildExtensionSet,
7
+ buildExtToLangMap,
8
+ } from './ast-analysis/shared.js';
3
9
  import { loadConfig } from './config.js';
4
10
  import { openReadonlyOrFail } from './db.js';
5
11
  import { info } from './logger.js';
6
- import { paginateResult, printNdjson } from './paginate.js';
7
- import { LANGUAGE_REGISTRY } from './parser.js';
8
- import { isTestFile } from './queries.js';
9
-
10
- // ─── Language-Specific Node Type Registry ─────────────────────────────────
11
-
12
- const JS_TS_RULES = {
13
- // Structural increments (cognitive +1, cyclomatic varies)
14
- branchNodes: new Set([
15
- 'if_statement',
16
- 'else_clause',
17
- 'switch_statement',
18
- 'for_statement',
19
- 'for_in_statement',
20
- 'while_statement',
21
- 'do_statement',
22
- 'catch_clause',
23
- 'ternary_expression',
24
- ]),
25
- // Cyclomatic-only: each case adds a path
26
- caseNodes: new Set(['switch_case']),
27
- // Logical operators: cognitive +1 per sequence change, cyclomatic +1 each
28
- logicalOperators: new Set(['&&', '||', '??']),
29
- logicalNodeType: 'binary_expression',
30
- // Optional chaining: cyclomatic only
31
- optionalChainType: 'optional_chain_expression',
32
- // Nesting-sensitive: these increment nesting depth
33
- nestingNodes: new Set([
34
- 'if_statement',
35
- 'switch_statement',
36
- 'for_statement',
37
- 'for_in_statement',
38
- 'while_statement',
39
- 'do_statement',
40
- 'catch_clause',
41
- 'ternary_expression',
42
- ]),
43
- // Function-like nodes (increase nesting when nested)
44
- functionNodes: new Set([
45
- 'function_declaration',
46
- 'function_expression',
47
- 'arrow_function',
48
- 'method_definition',
49
- 'generator_function',
50
- 'generator_function_declaration',
51
- ]),
52
- // If/else pattern detection
53
- ifNodeType: 'if_statement',
54
- elseNodeType: 'else_clause',
55
- elifNodeType: null,
56
- elseViaAlternative: false,
57
- switchLikeNodes: new Set(['switch_statement']),
58
- };
59
-
60
- const PYTHON_RULES = {
61
- branchNodes: new Set([
62
- 'if_statement',
63
- 'elif_clause',
64
- 'else_clause',
65
- 'for_statement',
66
- 'while_statement',
67
- 'except_clause',
68
- 'conditional_expression',
69
- 'match_statement',
70
- ]),
71
- caseNodes: new Set(['case_clause']),
72
- logicalOperators: new Set(['and', 'or']),
73
- logicalNodeType: 'boolean_operator',
74
- optionalChainType: null,
75
- nestingNodes: new Set([
76
- 'if_statement',
77
- 'for_statement',
78
- 'while_statement',
79
- 'except_clause',
80
- 'conditional_expression',
81
- ]),
82
- functionNodes: new Set(['function_definition', 'lambda']),
83
- ifNodeType: 'if_statement',
84
- elseNodeType: 'else_clause',
85
- elifNodeType: 'elif_clause',
86
- elseViaAlternative: false,
87
- switchLikeNodes: new Set(['match_statement']),
88
- };
89
-
90
- const GO_RULES = {
91
- branchNodes: new Set([
92
- 'if_statement',
93
- 'for_statement',
94
- 'expression_switch_statement',
95
- 'type_switch_statement',
96
- 'select_statement',
97
- ]),
98
- caseNodes: new Set(['expression_case', 'type_case', 'default_case', 'communication_case']),
99
- logicalOperators: new Set(['&&', '||']),
100
- logicalNodeType: 'binary_expression',
101
- optionalChainType: null,
102
- nestingNodes: new Set([
103
- 'if_statement',
104
- 'for_statement',
105
- 'expression_switch_statement',
106
- 'type_switch_statement',
107
- 'select_statement',
108
- ]),
109
- functionNodes: new Set(['function_declaration', 'method_declaration', 'func_literal']),
110
- ifNodeType: 'if_statement',
111
- elseNodeType: null,
112
- elifNodeType: null,
113
- elseViaAlternative: true,
114
- switchLikeNodes: new Set(['expression_switch_statement', 'type_switch_statement']),
115
- };
116
-
117
- const RUST_RULES = {
118
- branchNodes: new Set([
119
- 'if_expression',
120
- 'else_clause',
121
- 'for_expression',
122
- 'while_expression',
123
- 'loop_expression',
124
- 'if_let_expression',
125
- 'while_let_expression',
126
- 'match_expression',
127
- ]),
128
- caseNodes: new Set(['match_arm']),
129
- logicalOperators: new Set(['&&', '||']),
130
- logicalNodeType: 'binary_expression',
131
- optionalChainType: null,
132
- nestingNodes: new Set([
133
- 'if_expression',
134
- 'for_expression',
135
- 'while_expression',
136
- 'loop_expression',
137
- 'if_let_expression',
138
- 'while_let_expression',
139
- 'match_expression',
140
- ]),
141
- functionNodes: new Set(['function_item', 'closure_expression']),
142
- ifNodeType: 'if_expression',
143
- elseNodeType: 'else_clause',
144
- elifNodeType: null,
145
- elseViaAlternative: false,
146
- switchLikeNodes: new Set(['match_expression']),
147
- };
148
-
149
- const JAVA_RULES = {
150
- branchNodes: new Set([
151
- 'if_statement',
152
- 'for_statement',
153
- 'enhanced_for_statement',
154
- 'while_statement',
155
- 'do_statement',
156
- 'catch_clause',
157
- 'ternary_expression',
158
- 'switch_expression',
159
- ]),
160
- caseNodes: new Set(['switch_label']),
161
- logicalOperators: new Set(['&&', '||']),
162
- logicalNodeType: 'binary_expression',
163
- optionalChainType: null,
164
- nestingNodes: new Set([
165
- 'if_statement',
166
- 'for_statement',
167
- 'enhanced_for_statement',
168
- 'while_statement',
169
- 'do_statement',
170
- 'catch_clause',
171
- 'ternary_expression',
172
- ]),
173
- functionNodes: new Set(['method_declaration', 'constructor_declaration', 'lambda_expression']),
174
- ifNodeType: 'if_statement',
175
- elseNodeType: null,
176
- elifNodeType: null,
177
- elseViaAlternative: true,
178
- switchLikeNodes: new Set(['switch_expression']),
179
- };
180
-
181
- const CSHARP_RULES = {
182
- branchNodes: new Set([
183
- 'if_statement',
184
- 'else_clause',
185
- 'for_statement',
186
- 'foreach_statement',
187
- 'while_statement',
188
- 'do_statement',
189
- 'catch_clause',
190
- 'conditional_expression',
191
- 'switch_statement',
192
- ]),
193
- caseNodes: new Set(['switch_section']),
194
- logicalOperators: new Set(['&&', '||', '??']),
195
- logicalNodeType: 'binary_expression',
196
- optionalChainType: 'conditional_access_expression',
197
- nestingNodes: new Set([
198
- 'if_statement',
199
- 'for_statement',
200
- 'foreach_statement',
201
- 'while_statement',
202
- 'do_statement',
203
- 'catch_clause',
204
- 'conditional_expression',
205
- 'switch_statement',
206
- ]),
207
- functionNodes: new Set([
208
- 'method_declaration',
209
- 'constructor_declaration',
210
- 'lambda_expression',
211
- 'local_function_statement',
212
- ]),
213
- ifNodeType: 'if_statement',
214
- elseNodeType: null,
215
- elifNodeType: null,
216
- elseViaAlternative: true,
217
- switchLikeNodes: new Set(['switch_statement']),
218
- };
219
-
220
- const RUBY_RULES = {
221
- branchNodes: new Set([
222
- 'if',
223
- 'elsif',
224
- 'else',
225
- 'unless',
226
- 'case',
227
- 'for',
228
- 'while',
229
- 'until',
230
- 'rescue',
231
- 'conditional',
232
- ]),
233
- caseNodes: new Set(['when']),
234
- logicalOperators: new Set(['and', 'or', '&&', '||']),
235
- logicalNodeType: 'binary',
236
- optionalChainType: null,
237
- nestingNodes: new Set(['if', 'unless', 'case', 'for', 'while', 'until', 'rescue', 'conditional']),
238
- functionNodes: new Set(['method', 'singleton_method', 'lambda', 'do_block']),
239
- ifNodeType: 'if',
240
- elseNodeType: 'else',
241
- elifNodeType: 'elsif',
242
- elseViaAlternative: false,
243
- switchLikeNodes: new Set(['case']),
244
- };
245
-
246
- const PHP_RULES = {
247
- branchNodes: new Set([
248
- 'if_statement',
249
- 'else_if_clause',
250
- 'else_clause',
251
- 'for_statement',
252
- 'foreach_statement',
253
- 'while_statement',
254
- 'do_statement',
255
- 'catch_clause',
256
- 'conditional_expression',
257
- 'switch_statement',
258
- ]),
259
- caseNodes: new Set(['case_statement', 'default_statement']),
260
- logicalOperators: new Set(['&&', '||', 'and', 'or', '??']),
261
- logicalNodeType: 'binary_expression',
262
- optionalChainType: 'nullsafe_member_access_expression',
263
- nestingNodes: new Set([
264
- 'if_statement',
265
- 'for_statement',
266
- 'foreach_statement',
267
- 'while_statement',
268
- 'do_statement',
269
- 'catch_clause',
270
- 'conditional_expression',
271
- 'switch_statement',
272
- ]),
273
- functionNodes: new Set([
274
- 'function_definition',
275
- 'method_declaration',
276
- 'anonymous_function_creation_expression',
277
- 'arrow_function',
278
- ]),
279
- ifNodeType: 'if_statement',
280
- elseNodeType: 'else_clause',
281
- elifNodeType: 'else_if_clause',
282
- elseViaAlternative: false,
283
- switchLikeNodes: new Set(['switch_statement']),
284
- };
285
-
286
- export const COMPLEXITY_RULES = new Map([
287
- ['javascript', JS_TS_RULES],
288
- ['typescript', JS_TS_RULES],
289
- ['tsx', JS_TS_RULES],
290
- ['python', PYTHON_RULES],
291
- ['go', GO_RULES],
292
- ['rust', RUST_RULES],
293
- ['java', JAVA_RULES],
294
- ['csharp', CSHARP_RULES],
295
- ['ruby', RUBY_RULES],
296
- ['php', PHP_RULES],
297
- ]);
12
+ import { paginateResult } from './paginate.js';
298
13
 
299
- // Extensions whose language has complexity rules — used to skip needless WASM init
300
- const COMPLEXITY_EXTENSIONS = new Set();
301
- for (const entry of LANGUAGE_REGISTRY) {
302
- if (COMPLEXITY_RULES.has(entry.id)) {
303
- for (const ext of entry.extensions) COMPLEXITY_EXTENSIONS.add(ext);
304
- }
305
- }
14
+ import { outputResult } from './result-formatter.js';
15
+ import { isTestFile } from './test-filter.js';
306
16
 
307
- // ─── Halstead Operator/Operand Classification ────────────────────────────
308
-
309
- const JS_TS_HALSTEAD = {
310
- operatorLeafTypes: new Set([
311
- // Arithmetic
312
- '+',
313
- '-',
314
- '*',
315
- '/',
316
- '%',
317
- '**',
318
- // Assignment
319
- '=',
320
- '+=',
321
- '-=',
322
- '*=',
323
- '/=',
324
- '%=',
325
- '**=',
326
- '<<=',
327
- '>>=',
328
- '>>>=',
329
- '&=',
330
- '|=',
331
- '^=',
332
- '&&=',
333
- '||=',
334
- '??=',
335
- // Comparison
336
- '==',
337
- '===',
338
- '!=',
339
- '!==',
340
- '<',
341
- '>',
342
- '<=',
343
- '>=',
344
- // Logical
345
- '&&',
346
- '||',
347
- '!',
348
- '??',
349
- // Bitwise
350
- '&',
351
- '|',
352
- '^',
353
- '~',
354
- '<<',
355
- '>>',
356
- '>>>',
357
- // Unary
358
- '++',
359
- '--',
360
- // Keywords as operators
361
- 'typeof',
362
- 'instanceof',
363
- 'new',
364
- 'return',
365
- 'throw',
366
- 'yield',
367
- 'await',
368
- 'if',
369
- 'else',
370
- 'for',
371
- 'while',
372
- 'do',
373
- 'switch',
374
- 'case',
375
- 'break',
376
- 'continue',
377
- 'try',
378
- 'catch',
379
- 'finally',
380
- // Arrow, spread, ternary, access
381
- '=>',
382
- '...',
383
- '?',
384
- ':',
385
- '.',
386
- '?.',
387
- // Delimiters counted as operators
388
- ',',
389
- ';',
390
- ]),
391
- operandLeafTypes: new Set([
392
- 'identifier',
393
- 'property_identifier',
394
- 'shorthand_property_identifier',
395
- 'shorthand_property_identifier_pattern',
396
- 'number',
397
- 'string_fragment',
398
- 'regex_pattern',
399
- 'true',
400
- 'false',
401
- 'null',
402
- 'undefined',
403
- 'this',
404
- 'super',
405
- 'private_property_identifier',
406
- ]),
407
- compoundOperators: new Set([
408
- 'call_expression',
409
- 'subscript_expression',
410
- 'new_expression',
411
- 'template_substitution',
412
- ]),
413
- skipTypes: new Set(['type_annotation', 'type_parameters', 'return_type', 'implements_clause']),
414
- };
415
-
416
- const PYTHON_HALSTEAD = {
417
- operatorLeafTypes: new Set([
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
- '>=',
444
- 'and',
445
- 'or',
446
- 'not',
447
- '&',
448
- '|',
449
- '^',
450
- '~',
451
- '<<',
452
- '>>',
453
- 'if',
454
- 'else',
455
- 'elif',
456
- 'for',
457
- 'while',
458
- 'with',
459
- 'try',
460
- 'except',
461
- 'finally',
462
- 'raise',
463
- 'return',
464
- 'yield',
465
- 'await',
466
- 'pass',
467
- 'break',
468
- 'continue',
469
- 'import',
470
- 'from',
471
- 'as',
472
- 'in',
473
- 'is',
474
- 'lambda',
475
- 'del',
476
- '.',
477
- ',',
478
- ':',
479
- '@',
480
- '->',
481
- ]),
482
- operandLeafTypes: new Set([
483
- 'identifier',
484
- 'integer',
485
- 'float',
486
- 'string_content',
487
- 'true',
488
- 'false',
489
- 'none',
490
- ]),
491
- compoundOperators: new Set(['call', 'subscript', 'attribute']),
492
- skipTypes: new Set([]),
493
- };
494
-
495
- const GO_HALSTEAD = {
496
- operatorLeafTypes: new Set([
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
- '--',
532
- 'if',
533
- 'else',
534
- 'for',
535
- 'switch',
536
- 'select',
537
- 'case',
538
- 'default',
539
- 'return',
540
- 'break',
541
- 'continue',
542
- 'goto',
543
- 'fallthrough',
544
- 'go',
545
- 'defer',
546
- 'range',
547
- 'chan',
548
- 'func',
549
- 'var',
550
- 'const',
551
- 'type',
552
- 'struct',
553
- 'interface',
554
- '.',
555
- ',',
556
- ';',
557
- ':',
558
- '<-',
559
- ]),
560
- operandLeafTypes: new Set([
561
- 'identifier',
562
- 'field_identifier',
563
- 'package_identifier',
564
- 'type_identifier',
565
- 'int_literal',
566
- 'float_literal',
567
- 'imaginary_literal',
568
- 'rune_literal',
569
- 'interpreted_string_literal',
570
- 'raw_string_literal',
571
- 'true',
572
- 'false',
573
- 'nil',
574
- 'iota',
575
- ]),
576
- compoundOperators: new Set(['call_expression', 'index_expression', 'selector_expression']),
577
- skipTypes: new Set([]),
578
- };
579
-
580
- const RUST_HALSTEAD = {
581
- operatorLeafTypes: new Set([
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
- '>>',
612
- 'if',
613
- 'else',
614
- 'for',
615
- 'while',
616
- 'loop',
617
- 'match',
618
- 'return',
619
- 'break',
620
- 'continue',
621
- 'let',
622
- 'mut',
623
- 'ref',
624
- 'as',
625
- 'in',
626
- 'move',
627
- 'fn',
628
- 'struct',
629
- 'enum',
630
- 'trait',
631
- 'impl',
632
- 'pub',
633
- 'mod',
634
- 'use',
635
- '.',
636
- ',',
637
- ';',
638
- ':',
639
- '::',
640
- '=>',
641
- '->',
642
- '?',
643
- ]),
644
- operandLeafTypes: new Set([
645
- 'identifier',
646
- 'field_identifier',
647
- 'type_identifier',
648
- 'integer_literal',
649
- 'float_literal',
650
- 'string_content',
651
- 'char_literal',
652
- 'true',
653
- 'false',
654
- 'self',
655
- 'Self',
656
- ]),
657
- compoundOperators: new Set(['call_expression', 'index_expression', 'field_expression']),
658
- skipTypes: new Set([]),
659
- };
660
-
661
- const JAVA_HALSTEAD = {
662
- operatorLeafTypes: new Set([
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
- '--',
698
- 'instanceof',
699
- 'new',
700
- 'if',
701
- 'else',
702
- 'for',
703
- 'while',
704
- 'do',
705
- 'switch',
706
- 'case',
707
- 'return',
708
- 'throw',
709
- 'break',
710
- 'continue',
711
- 'try',
712
- 'catch',
713
- 'finally',
714
- '.',
715
- ',',
716
- ';',
717
- ':',
718
- '?',
719
- '->',
720
- ]),
721
- operandLeafTypes: new Set([
722
- 'identifier',
723
- 'type_identifier',
724
- 'decimal_integer_literal',
725
- 'hex_integer_literal',
726
- 'octal_integer_literal',
727
- 'binary_integer_literal',
728
- 'decimal_floating_point_literal',
729
- 'hex_floating_point_literal',
730
- 'string_literal',
731
- 'character_literal',
732
- 'true',
733
- 'false',
734
- 'null',
735
- 'this',
736
- 'super',
737
- ]),
738
- compoundOperators: new Set(['method_invocation', 'array_access', 'object_creation_expression']),
739
- skipTypes: new Set(['type_arguments', 'type_parameters']),
740
- };
741
-
742
- const CSHARP_HALSTEAD = {
743
- operatorLeafTypes: new Set([
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
- '--',
779
- 'is',
780
- 'as',
781
- 'new',
782
- 'typeof',
783
- 'sizeof',
784
- 'nameof',
785
- 'if',
786
- 'else',
787
- 'for',
788
- 'foreach',
789
- 'while',
790
- 'do',
791
- 'switch',
792
- 'case',
793
- 'return',
794
- 'throw',
795
- 'break',
796
- 'continue',
797
- 'try',
798
- 'catch',
799
- 'finally',
800
- 'await',
801
- 'yield',
802
- '.',
803
- '?.',
804
- ',',
805
- ';',
806
- ':',
807
- '=>',
808
- '->',
809
- ]),
810
- operandLeafTypes: new Set([
811
- 'identifier',
812
- 'integer_literal',
813
- 'real_literal',
814
- 'string_literal',
815
- 'character_literal',
816
- 'verbatim_string_literal',
817
- 'interpolated_string_text',
818
- 'true',
819
- 'false',
820
- 'null',
821
- 'this',
822
- 'base',
823
- ]),
824
- compoundOperators: new Set([
825
- 'invocation_expression',
826
- 'element_access_expression',
827
- 'object_creation_expression',
828
- ]),
829
- skipTypes: new Set(['type_argument_list', 'type_parameter_list']),
830
- };
831
-
832
- const RUBY_HALSTEAD = {
833
- operatorLeafTypes: new Set([
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
- '!',
865
- 'and',
866
- 'or',
867
- 'not',
868
- '&',
869
- '|',
870
- '^',
871
- '~',
872
- '<<',
873
- '>>',
874
- 'if',
875
- 'else',
876
- 'elsif',
877
- 'unless',
878
- 'case',
879
- 'when',
880
- 'for',
881
- 'while',
882
- 'until',
883
- 'do',
884
- 'begin',
885
- 'end',
886
- 'return',
887
- 'raise',
888
- 'break',
889
- 'next',
890
- 'redo',
891
- 'retry',
892
- 'rescue',
893
- 'ensure',
894
- 'yield',
895
- 'def',
896
- 'class',
897
- 'module',
898
- '.',
899
- ',',
900
- ':',
901
- '::',
902
- '=>',
903
- '->',
904
- ]),
905
- operandLeafTypes: new Set([
906
- 'identifier',
907
- 'constant',
908
- 'instance_variable',
909
- 'class_variable',
910
- 'global_variable',
911
- 'integer',
912
- 'float',
913
- 'string_content',
914
- 'symbol',
915
- 'true',
916
- 'false',
917
- 'nil',
918
- 'self',
919
- ]),
920
- compoundOperators: new Set(['call', 'element_reference']),
921
- skipTypes: new Set([]),
922
- };
923
-
924
- const PHP_HALSTEAD = {
925
- operatorLeafTypes: new Set([
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
- '!',
957
- 'and',
958
- 'or',
959
- 'xor',
960
- '??',
961
- '&',
962
- '|',
963
- '^',
964
- '~',
965
- '<<',
966
- '>>',
967
- '++',
968
- '--',
969
- 'instanceof',
970
- 'new',
971
- 'clone',
972
- 'if',
973
- 'else',
974
- 'elseif',
975
- 'for',
976
- 'foreach',
977
- 'while',
978
- 'do',
979
- 'switch',
980
- 'case',
981
- 'return',
982
- 'throw',
983
- 'break',
984
- 'continue',
985
- 'try',
986
- 'catch',
987
- 'finally',
988
- 'echo',
989
- 'print',
990
- 'yield',
991
- '.',
992
- '->',
993
- '?->',
994
- '::',
995
- ',',
996
- ';',
997
- ':',
998
- '?',
999
- '=>',
1000
- ]),
1001
- operandLeafTypes: new Set([
1002
- 'name',
1003
- 'variable_name',
1004
- 'integer',
1005
- 'float',
1006
- 'string_content',
1007
- 'true',
1008
- 'false',
1009
- 'null',
1010
- ]),
1011
- compoundOperators: new Set([
1012
- 'function_call_expression',
1013
- 'member_call_expression',
1014
- 'scoped_call_expression',
1015
- 'subscript_expression',
1016
- 'object_creation_expression',
1017
- ]),
1018
- skipTypes: new Set([]),
1019
- };
1020
-
1021
- export const HALSTEAD_RULES = new Map([
1022
- ['javascript', JS_TS_HALSTEAD],
1023
- ['typescript', JS_TS_HALSTEAD],
1024
- ['tsx', JS_TS_HALSTEAD],
1025
- ['python', PYTHON_HALSTEAD],
1026
- ['go', GO_HALSTEAD],
1027
- ['rust', RUST_HALSTEAD],
1028
- ['java', JAVA_HALSTEAD],
1029
- ['csharp', CSHARP_HALSTEAD],
1030
- ['ruby', RUBY_HALSTEAD],
1031
- ['php', PHP_HALSTEAD],
1032
- ]);
17
+ // Re-export rules for backward compatibility
18
+ export { COMPLEXITY_RULES, HALSTEAD_RULES };
19
+
20
+ // Extensions whose language has complexity rules — used to skip needless WASM init
21
+ const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
1033
22
 
1034
23
  // ─── Halstead Metrics Computation ─────────────────────────────────────────
1035
24
 
@@ -1574,34 +563,7 @@ export function computeAllMetrics(functionNode, langId) {
1574
563
  /**
1575
564
  * Find the function body node in a parse tree that matches a given line range.
1576
565
  */
1577
- export function findFunctionNode(rootNode, startLine, _endLine, rules) {
1578
- // tree-sitter lines are 0-indexed
1579
- const targetStart = startLine - 1;
1580
-
1581
- let best = null;
1582
-
1583
- function search(node) {
1584
- const nodeStart = node.startPosition.row;
1585
- const nodeEnd = node.endPosition.row;
1586
-
1587
- // Prune branches outside range
1588
- if (nodeEnd < targetStart || nodeStart > targetStart + 1) return;
1589
-
1590
- if (rules.functionNodes.has(node.type) && nodeStart === targetStart) {
1591
- // Found a function node at the right position — pick it
1592
- if (!best || nodeEnd - nodeStart < best.endPosition.row - best.startPosition.row) {
1593
- best = node;
1594
- }
1595
- }
1596
-
1597
- for (let i = 0; i < node.childCount; i++) {
1598
- search(node.child(i));
1599
- }
1600
- }
1601
-
1602
- search(rootNode);
1603
- return best;
1604
- }
566
+ export { _findFunctionNode as findFunctionNode };
1605
567
 
1606
568
  /**
1607
569
  * Re-parse changed files with WASM tree-sitter, find function AST subtrees,
@@ -1635,12 +597,7 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
1635
597
  if (needsFallback) {
1636
598
  const { createParsers } = await import('./parser.js');
1637
599
  parsers = await createParsers();
1638
- extToLang = new Map();
1639
- for (const entry of LANGUAGE_REGISTRY) {
1640
- for (const ext of entry.extensions) {
1641
- extToLang.set(ext, entry.id);
1642
- }
1643
- }
600
+ extToLang = buildExtToLangMap();
1644
601
  }
1645
602
 
1646
603
  const { getParser } = await import('./parser.js');
@@ -1797,204 +754,211 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
1797
754
  */
1798
755
  export function complexityData(customDbPath, opts = {}) {
1799
756
  const db = openReadonlyOrFail(customDbPath);
1800
- const sort = opts.sort || 'cognitive';
1801
- const noTests = opts.noTests || false;
1802
- const aboveThreshold = opts.aboveThreshold || false;
1803
- const target = opts.target || null;
1804
- const fileFilter = opts.file || null;
1805
- const kindFilter = opts.kind || null;
1806
-
1807
- // Load thresholds from config
1808
- const config = loadConfig(process.cwd());
1809
- const thresholds = config.manifesto?.rules || {
1810
- cognitive: { warn: 15, fail: null },
1811
- cyclomatic: { warn: 10, fail: null },
1812
- maxNesting: { warn: 4, fail: null },
1813
- maintainabilityIndex: { warn: 20, fail: null },
1814
- };
757
+ try {
758
+ const sort = opts.sort || 'cognitive';
759
+ const noTests = opts.noTests || false;
760
+ const aboveThreshold = opts.aboveThreshold || false;
761
+ const target = opts.target || null;
762
+ const fileFilter = opts.file || null;
763
+ const kindFilter = opts.kind || null;
764
+
765
+ // Load thresholds from config
766
+ const config = loadConfig(process.cwd());
767
+ const thresholds = config.manifesto?.rules || {
768
+ cognitive: { warn: 15, fail: null },
769
+ cyclomatic: { warn: 10, fail: null },
770
+ maxNesting: { warn: 4, fail: null },
771
+ maintainabilityIndex: { warn: 20, fail: null },
772
+ };
1815
773
 
1816
- // Build query
1817
- let where = "WHERE n.kind IN ('function','method')";
1818
- const params = [];
774
+ // Build query
775
+ let where = "WHERE n.kind IN ('function','method')";
776
+ const params = [];
1819
777
 
1820
- if (noTests) {
1821
- where += ` AND n.file NOT LIKE '%.test.%'
778
+ if (noTests) {
779
+ where += ` AND n.file NOT LIKE '%.test.%'
1822
780
  AND n.file NOT LIKE '%.spec.%'
1823
781
  AND n.file NOT LIKE '%__test__%'
1824
782
  AND n.file NOT LIKE '%__tests__%'
1825
783
  AND n.file NOT LIKE '%.stories.%'`;
1826
- }
1827
- if (target) {
1828
- where += ' AND n.name LIKE ?';
1829
- params.push(`%${target}%`);
1830
- }
1831
- if (fileFilter) {
1832
- where += ' AND n.file LIKE ?';
1833
- params.push(`%${fileFilter}%`);
1834
- }
1835
- if (kindFilter) {
1836
- where += ' AND n.kind = ?';
1837
- params.push(kindFilter);
1838
- }
1839
-
1840
- const isValidThreshold = (v) => typeof v === 'number' && Number.isFinite(v);
1841
-
1842
- let having = '';
1843
- if (aboveThreshold) {
1844
- const conditions = [];
1845
- if (isValidThreshold(thresholds.cognitive?.warn)) {
1846
- conditions.push(`fc.cognitive >= ${thresholds.cognitive.warn}`);
1847
784
  }
1848
- if (isValidThreshold(thresholds.cyclomatic?.warn)) {
1849
- conditions.push(`fc.cyclomatic >= ${thresholds.cyclomatic.warn}`);
785
+ if (target) {
786
+ where += ' AND n.name LIKE ?';
787
+ params.push(`%${target}%`);
1850
788
  }
1851
- if (isValidThreshold(thresholds.maxNesting?.warn)) {
1852
- conditions.push(`fc.max_nesting >= ${thresholds.maxNesting.warn}`);
789
+ if (fileFilter) {
790
+ where += ' AND n.file LIKE ?';
791
+ params.push(`%${fileFilter}%`);
1853
792
  }
1854
- if (isValidThreshold(thresholds.maintainabilityIndex?.warn)) {
1855
- conditions.push(
1856
- `fc.maintainability_index > 0 AND fc.maintainability_index <= ${thresholds.maintainabilityIndex.warn}`,
1857
- );
793
+ if (kindFilter) {
794
+ where += ' AND n.kind = ?';
795
+ params.push(kindFilter);
1858
796
  }
1859
- if (conditions.length > 0) {
1860
- having = `AND (${conditions.join(' OR ')})`;
797
+
798
+ const isValidThreshold = (v) => typeof v === 'number' && Number.isFinite(v);
799
+
800
+ let having = '';
801
+ if (aboveThreshold) {
802
+ const conditions = [];
803
+ if (isValidThreshold(thresholds.cognitive?.warn)) {
804
+ conditions.push(`fc.cognitive >= ${thresholds.cognitive.warn}`);
805
+ }
806
+ if (isValidThreshold(thresholds.cyclomatic?.warn)) {
807
+ conditions.push(`fc.cyclomatic >= ${thresholds.cyclomatic.warn}`);
808
+ }
809
+ if (isValidThreshold(thresholds.maxNesting?.warn)) {
810
+ conditions.push(`fc.max_nesting >= ${thresholds.maxNesting.warn}`);
811
+ }
812
+ if (isValidThreshold(thresholds.maintainabilityIndex?.warn)) {
813
+ conditions.push(
814
+ `fc.maintainability_index > 0 AND fc.maintainability_index <= ${thresholds.maintainabilityIndex.warn}`,
815
+ );
816
+ }
817
+ if (conditions.length > 0) {
818
+ having = `AND (${conditions.join(' OR ')})`;
819
+ }
1861
820
  }
1862
- }
1863
821
 
1864
- const orderMap = {
1865
- cognitive: 'fc.cognitive DESC',
1866
- cyclomatic: 'fc.cyclomatic DESC',
1867
- nesting: 'fc.max_nesting DESC',
1868
- mi: 'fc.maintainability_index ASC',
1869
- volume: 'fc.halstead_volume DESC',
1870
- effort: 'fc.halstead_effort DESC',
1871
- bugs: 'fc.halstead_bugs DESC',
1872
- loc: 'fc.loc DESC',
1873
- };
1874
- const orderBy = orderMap[sort] || 'fc.cognitive DESC';
822
+ const orderMap = {
823
+ cognitive: 'fc.cognitive DESC',
824
+ cyclomatic: 'fc.cyclomatic DESC',
825
+ nesting: 'fc.max_nesting DESC',
826
+ mi: 'fc.maintainability_index ASC',
827
+ volume: 'fc.halstead_volume DESC',
828
+ effort: 'fc.halstead_effort DESC',
829
+ bugs: 'fc.halstead_bugs DESC',
830
+ loc: 'fc.loc DESC',
831
+ };
832
+ const orderBy = orderMap[sort] || 'fc.cognitive DESC';
1875
833
 
1876
- let rows;
1877
- try {
1878
- rows = db
1879
- .prepare(
1880
- `SELECT n.name, n.kind, n.file, n.line, n.end_line,
1881
- fc.cognitive, fc.cyclomatic, fc.max_nesting,
1882
- fc.loc, fc.sloc, fc.maintainability_index,
1883
- fc.halstead_volume, fc.halstead_difficulty, fc.halstead_effort, fc.halstead_bugs
1884
- FROM function_complexity fc
1885
- JOIN nodes n ON fc.node_id = n.id
1886
- ${where} ${having}
1887
- ORDER BY ${orderBy}`,
1888
- )
1889
- .all(...params);
1890
- } catch {
1891
- // Check if graph has nodes even though complexity table is missing/empty
1892
- let hasGraph = false;
834
+ let rows;
1893
835
  try {
1894
- hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
836
+ rows = db
837
+ .prepare(
838
+ `SELECT n.name, n.kind, n.file, n.line, n.end_line,
839
+ fc.cognitive, fc.cyclomatic, fc.max_nesting,
840
+ fc.loc, fc.sloc, fc.maintainability_index,
841
+ fc.halstead_volume, fc.halstead_difficulty, fc.halstead_effort, fc.halstead_bugs
842
+ FROM function_complexity fc
843
+ JOIN nodes n ON fc.node_id = n.id
844
+ ${where} ${having}
845
+ ORDER BY ${orderBy}`,
846
+ )
847
+ .all(...params);
1895
848
  } catch {
1896
- /* ignore */
849
+ // Check if graph has nodes even though complexity table is missing/empty
850
+ let hasGraph = false;
851
+ try {
852
+ hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
853
+ } catch {
854
+ /* ignore */
855
+ }
856
+ return { functions: [], summary: null, thresholds, hasGraph };
1897
857
  }
1898
- db.close();
1899
- return { functions: [], summary: null, thresholds, hasGraph };
1900
- }
1901
858
 
1902
- // Post-filter test files if needed (belt-and-suspenders for isTestFile)
1903
- const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
859
+ // Post-filter test files if needed (belt-and-suspenders for isTestFile)
860
+ const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
1904
861
 
1905
- const functions = filtered.map((r) => {
1906
- const exceeds = [];
1907
- if (isValidThreshold(thresholds.cognitive?.warn) && r.cognitive >= thresholds.cognitive.warn)
1908
- exceeds.push('cognitive');
1909
- if (isValidThreshold(thresholds.cyclomatic?.warn) && r.cyclomatic >= thresholds.cyclomatic.warn)
1910
- exceeds.push('cyclomatic');
1911
- if (
1912
- isValidThreshold(thresholds.maxNesting?.warn) &&
1913
- r.max_nesting >= thresholds.maxNesting.warn
1914
- )
1915
- exceeds.push('maxNesting');
1916
- if (
1917
- isValidThreshold(thresholds.maintainabilityIndex?.warn) &&
1918
- r.maintainability_index > 0 &&
1919
- r.maintainability_index <= thresholds.maintainabilityIndex.warn
1920
- )
1921
- exceeds.push('maintainabilityIndex');
1922
-
1923
- return {
1924
- name: r.name,
1925
- kind: r.kind,
1926
- file: r.file,
1927
- line: r.line,
1928
- endLine: r.end_line || null,
1929
- cognitive: r.cognitive,
1930
- cyclomatic: r.cyclomatic,
1931
- maxNesting: r.max_nesting,
1932
- loc: r.loc || 0,
1933
- sloc: r.sloc || 0,
1934
- maintainabilityIndex: r.maintainability_index || 0,
1935
- halstead: {
1936
- volume: r.halstead_volume || 0,
1937
- difficulty: r.halstead_difficulty || 0,
1938
- effort: r.halstead_effort || 0,
1939
- bugs: r.halstead_bugs || 0,
1940
- },
1941
- exceeds: exceeds.length > 0 ? exceeds : undefined,
1942
- };
1943
- });
1944
-
1945
- // Summary stats
1946
- let summary = null;
1947
- try {
1948
- const allRows = db
1949
- .prepare(
1950
- `SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
1951
- FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
1952
- WHERE n.kind IN ('function','method')
1953
- ${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.%'` : ''}`,
862
+ const functions = filtered.map((r) => {
863
+ const exceeds = [];
864
+ if (isValidThreshold(thresholds.cognitive?.warn) && r.cognitive >= thresholds.cognitive.warn)
865
+ exceeds.push('cognitive');
866
+ if (
867
+ isValidThreshold(thresholds.cyclomatic?.warn) &&
868
+ r.cyclomatic >= thresholds.cyclomatic.warn
869
+ )
870
+ exceeds.push('cyclomatic');
871
+ if (
872
+ isValidThreshold(thresholds.maxNesting?.warn) &&
873
+ r.max_nesting >= thresholds.maxNesting.warn
874
+ )
875
+ exceeds.push('maxNesting');
876
+ if (
877
+ isValidThreshold(thresholds.maintainabilityIndex?.warn) &&
878
+ r.maintainability_index > 0 &&
879
+ r.maintainability_index <= thresholds.maintainabilityIndex.warn
1954
880
  )
1955
- .all();
1956
-
1957
- if (allRows.length > 0) {
1958
- const miValues = allRows.map((r) => r.maintainability_index || 0);
1959
- summary = {
1960
- analyzed: allRows.length,
1961
- avgCognitive: +(allRows.reduce((s, r) => s + r.cognitive, 0) / allRows.length).toFixed(1),
1962
- avgCyclomatic: +(allRows.reduce((s, r) => s + r.cyclomatic, 0) / allRows.length).toFixed(1),
1963
- maxCognitive: Math.max(...allRows.map((r) => r.cognitive)),
1964
- maxCyclomatic: Math.max(...allRows.map((r) => r.cyclomatic)),
1965
- avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
1966
- minMI: +Math.min(...miValues).toFixed(1),
1967
- aboveWarn: allRows.filter(
1968
- (r) =>
1969
- (isValidThreshold(thresholds.cognitive?.warn) &&
1970
- r.cognitive >= thresholds.cognitive.warn) ||
1971
- (isValidThreshold(thresholds.cyclomatic?.warn) &&
1972
- r.cyclomatic >= thresholds.cyclomatic.warn) ||
1973
- (isValidThreshold(thresholds.maxNesting?.warn) &&
1974
- r.max_nesting >= thresholds.maxNesting.warn) ||
1975
- (isValidThreshold(thresholds.maintainabilityIndex?.warn) &&
1976
- r.maintainability_index > 0 &&
1977
- r.maintainability_index <= thresholds.maintainabilityIndex.warn),
1978
- ).length,
881
+ exceeds.push('maintainabilityIndex');
882
+
883
+ return {
884
+ name: r.name,
885
+ kind: r.kind,
886
+ file: r.file,
887
+ line: r.line,
888
+ endLine: r.end_line || null,
889
+ cognitive: r.cognitive,
890
+ cyclomatic: r.cyclomatic,
891
+ maxNesting: r.max_nesting,
892
+ loc: r.loc || 0,
893
+ sloc: r.sloc || 0,
894
+ maintainabilityIndex: r.maintainability_index || 0,
895
+ halstead: {
896
+ volume: r.halstead_volume || 0,
897
+ difficulty: r.halstead_difficulty || 0,
898
+ effort: r.halstead_effort || 0,
899
+ bugs: r.halstead_bugs || 0,
900
+ },
901
+ exceeds: exceeds.length > 0 ? exceeds : undefined,
1979
902
  };
1980
- }
1981
- } catch {
1982
- /* ignore */
1983
- }
903
+ });
1984
904
 
1985
- // When summary is null (no complexity rows), check if graph has nodes
1986
- let hasGraph = false;
1987
- if (summary === null) {
905
+ // Summary stats
906
+ let summary = null;
1988
907
  try {
1989
- hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
908
+ const allRows = db
909
+ .prepare(
910
+ `SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
911
+ FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
912
+ WHERE n.kind IN ('function','method')
913
+ ${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.%'` : ''}`,
914
+ )
915
+ .all();
916
+
917
+ if (allRows.length > 0) {
918
+ const miValues = allRows.map((r) => r.maintainability_index || 0);
919
+ summary = {
920
+ analyzed: allRows.length,
921
+ avgCognitive: +(allRows.reduce((s, r) => s + r.cognitive, 0) / allRows.length).toFixed(1),
922
+ avgCyclomatic: +(allRows.reduce((s, r) => s + r.cyclomatic, 0) / allRows.length).toFixed(
923
+ 1,
924
+ ),
925
+ maxCognitive: Math.max(...allRows.map((r) => r.cognitive)),
926
+ maxCyclomatic: Math.max(...allRows.map((r) => r.cyclomatic)),
927
+ avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
928
+ minMI: +Math.min(...miValues).toFixed(1),
929
+ aboveWarn: allRows.filter(
930
+ (r) =>
931
+ (isValidThreshold(thresholds.cognitive?.warn) &&
932
+ r.cognitive >= thresholds.cognitive.warn) ||
933
+ (isValidThreshold(thresholds.cyclomatic?.warn) &&
934
+ r.cyclomatic >= thresholds.cyclomatic.warn) ||
935
+ (isValidThreshold(thresholds.maxNesting?.warn) &&
936
+ r.max_nesting >= thresholds.maxNesting.warn) ||
937
+ (isValidThreshold(thresholds.maintainabilityIndex?.warn) &&
938
+ r.maintainability_index > 0 &&
939
+ r.maintainability_index <= thresholds.maintainabilityIndex.warn),
940
+ ).length,
941
+ };
942
+ }
1990
943
  } catch {
1991
944
  /* ignore */
1992
945
  }
1993
- }
1994
946
 
1995
- db.close();
1996
- const base = { functions, summary, thresholds, hasGraph };
1997
- return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
947
+ // When summary is null (no complexity rows), check if graph has nodes
948
+ let hasGraph = false;
949
+ if (summary === null) {
950
+ try {
951
+ hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
952
+ } catch {
953
+ /* ignore */
954
+ }
955
+ }
956
+
957
+ const base = { functions, summary, thresholds, hasGraph };
958
+ return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
959
+ } finally {
960
+ db.close();
961
+ }
1998
962
  }
1999
963
 
2000
964
  /**
@@ -2083,14 +1047,7 @@ export function* iterComplexity(customDbPath, opts = {}) {
2083
1047
  export function complexity(customDbPath, opts = {}) {
2084
1048
  const data = complexityData(customDbPath, opts);
2085
1049
 
2086
- if (opts.ndjson) {
2087
- printNdjson(data, 'functions');
2088
- return;
2089
- }
2090
- if (opts.json) {
2091
- console.log(JSON.stringify(data, null, 2));
2092
- return;
2093
- }
1050
+ if (outputResult(data, 'functions', opts)) return;
2094
1051
 
2095
1052
  if (data.functions.length === 0) {
2096
1053
  if (data.summary === null) {