@optave/codegraph 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) 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/engine.js +365 -0
  5. package/src/ast-analysis/metrics.js +118 -0
  6. package/src/ast-analysis/rules/csharp.js +201 -0
  7. package/src/ast-analysis/rules/go.js +182 -0
  8. package/src/ast-analysis/rules/index.js +82 -0
  9. package/src/ast-analysis/rules/java.js +175 -0
  10. package/src/ast-analysis/rules/javascript.js +246 -0
  11. package/src/ast-analysis/rules/php.js +219 -0
  12. package/src/ast-analysis/rules/python.js +196 -0
  13. package/src/ast-analysis/rules/ruby.js +204 -0
  14. package/src/ast-analysis/rules/rust.js +173 -0
  15. package/src/ast-analysis/shared.js +223 -0
  16. package/src/ast-analysis/visitor-utils.js +176 -0
  17. package/src/ast-analysis/visitor.js +162 -0
  18. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  19. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  20. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  21. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  22. package/src/ast.js +26 -166
  23. package/src/audit.js +2 -88
  24. package/src/batch.js +0 -25
  25. package/src/boundaries.js +1 -1
  26. package/src/branch-compare.js +82 -172
  27. package/src/builder.js +48 -184
  28. package/src/cfg.js +148 -1174
  29. package/src/check.js +1 -84
  30. package/src/cli.js +118 -197
  31. package/src/cochange.js +1 -39
  32. package/src/commands/audit.js +88 -0
  33. package/src/commands/batch.js +26 -0
  34. package/src/commands/branch-compare.js +97 -0
  35. package/src/commands/cfg.js +55 -0
  36. package/src/commands/check.js +82 -0
  37. package/src/commands/cochange.js +37 -0
  38. package/src/commands/communities.js +69 -0
  39. package/src/commands/complexity.js +77 -0
  40. package/src/commands/dataflow.js +110 -0
  41. package/src/commands/flow.js +70 -0
  42. package/src/commands/manifesto.js +77 -0
  43. package/src/commands/owners.js +52 -0
  44. package/src/commands/query.js +21 -0
  45. package/src/commands/sequence.js +33 -0
  46. package/src/commands/structure.js +64 -0
  47. package/src/commands/triage.js +49 -0
  48. package/src/communities.js +22 -96
  49. package/src/complexity.js +234 -1591
  50. package/src/cycles.js +1 -1
  51. package/src/dataflow.js +274 -1352
  52. package/src/db/connection.js +88 -0
  53. package/src/db/migrations.js +312 -0
  54. package/src/db/query-builder.js +280 -0
  55. package/src/db/repository/build-stmts.js +104 -0
  56. package/src/db/repository/cfg.js +83 -0
  57. package/src/db/repository/cochange.js +41 -0
  58. package/src/db/repository/complexity.js +15 -0
  59. package/src/db/repository/dataflow.js +12 -0
  60. package/src/db/repository/edges.js +259 -0
  61. package/src/db/repository/embeddings.js +40 -0
  62. package/src/db/repository/graph-read.js +39 -0
  63. package/src/db/repository/index.js +42 -0
  64. package/src/db/repository/nodes.js +236 -0
  65. package/src/db.js +58 -399
  66. package/src/embedder.js +158 -174
  67. package/src/export.js +1 -1
  68. package/src/extractors/javascript.js +130 -5
  69. package/src/flow.js +153 -222
  70. package/src/index.js +53 -16
  71. package/src/infrastructure/result-formatter.js +21 -0
  72. package/src/infrastructure/test-filter.js +7 -0
  73. package/src/kinds.js +50 -0
  74. package/src/manifesto.js +1 -82
  75. package/src/mcp.js +37 -20
  76. package/src/owners.js +127 -182
  77. package/src/queries-cli.js +866 -0
  78. package/src/queries.js +1271 -2416
  79. package/src/sequence.js +179 -223
  80. package/src/structure.js +211 -269
  81. package/src/triage.js +117 -212
  82. package/src/viewer.js +1 -1
  83. package/src/watcher.js +7 -4
package/src/complexity.js CHANGED
@@ -1,1035 +1,28 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import {
4
+ computeLOCMetrics as _computeLOCMetrics,
5
+ computeMaintainabilityIndex as _computeMaintainabilityIndex,
6
+ } from './ast-analysis/metrics.js';
7
+ import { COMPLEXITY_RULES, HALSTEAD_RULES } from './ast-analysis/rules/index.js';
8
+ import {
9
+ findFunctionNode as _findFunctionNode,
10
+ buildExtensionSet,
11
+ buildExtToLangMap,
12
+ } from './ast-analysis/shared.js';
13
+ import { walkWithVisitors } from './ast-analysis/visitor.js';
14
+ import { createComplexityVisitor } from './ast-analysis/visitors/complexity-visitor.js';
3
15
  import { loadConfig } from './config.js';
4
- import { openReadonlyOrFail } from './db.js';
16
+ import { getFunctionNodeId, openReadonlyOrFail } from './db.js';
17
+ import { isTestFile } from './infrastructure/test-filter.js';
5
18
  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
- ]);
19
+ import { paginateResult } from './paginate.js';
298
20
 
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
- }
21
+ // Re-export rules for backward compatibility
22
+ export { COMPLEXITY_RULES, HALSTEAD_RULES };
306
23
 
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
- ]);
24
+ // Extensions whose language has complexity rules — used to skip needless WASM init
25
+ const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
1033
26
 
1034
27
  // ─── Halstead Metrics Computation ─────────────────────────────────────────
1035
28
 
@@ -1106,80 +99,12 @@ export function computeHalsteadMetrics(functionNode, language) {
1106
99
  }
1107
100
 
1108
101
  // ─── LOC Metrics Computation ──────────────────────────────────────────────
1109
-
1110
- const C_STYLE_PREFIXES = ['//', '/*', '*', '*/'];
1111
-
1112
- const COMMENT_PREFIXES = new Map([
1113
- ['javascript', C_STYLE_PREFIXES],
1114
- ['typescript', C_STYLE_PREFIXES],
1115
- ['tsx', C_STYLE_PREFIXES],
1116
- ['go', C_STYLE_PREFIXES],
1117
- ['rust', C_STYLE_PREFIXES],
1118
- ['java', C_STYLE_PREFIXES],
1119
- ['csharp', C_STYLE_PREFIXES],
1120
- ['python', ['#']],
1121
- ['ruby', ['#']],
1122
- ['php', ['//', '#', '/*', '*', '*/']],
1123
- ]);
1124
-
1125
- /**
1126
- * Compute LOC metrics from a function node's source text.
1127
- *
1128
- * @param {object} functionNode - tree-sitter node
1129
- * @param {string} [language] - Language ID (falls back to C-style prefixes)
1130
- * @returns {{ loc: number, sloc: number, commentLines: number }}
1131
- */
1132
- export function computeLOCMetrics(functionNode, language) {
1133
- const text = functionNode.text;
1134
- const lines = text.split('\n');
1135
- const loc = lines.length;
1136
- const prefixes = (language && COMMENT_PREFIXES.get(language)) || C_STYLE_PREFIXES;
1137
-
1138
- let commentLines = 0;
1139
- let blankLines = 0;
1140
-
1141
- for (const line of lines) {
1142
- const trimmed = line.trim();
1143
- if (trimmed === '') {
1144
- blankLines++;
1145
- } else if (prefixes.some((p) => trimmed.startsWith(p))) {
1146
- commentLines++;
1147
- }
1148
- }
1149
-
1150
- const sloc = Math.max(1, loc - blankLines - commentLines);
1151
- return { loc, sloc, commentLines };
1152
- }
102
+ // Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
103
+ export const computeLOCMetrics = _computeLOCMetrics;
1153
104
 
1154
105
  // ─── Maintainability Index ────────────────────────────────────────────────
1155
-
1156
- /**
1157
- * Compute normalized Maintainability Index (0-100 scale).
1158
- *
1159
- * Original SEI formula: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
1160
- * Microsoft normalization: max(0, min(100, MI * 100/171))
1161
- *
1162
- * @param {number} volume - Halstead volume
1163
- * @param {number} cyclomatic - Cyclomatic complexity
1164
- * @param {number} sloc - Source lines of code
1165
- * @param {number} [commentRatio] - Comment ratio (0-1), optional
1166
- * @returns {number} Normalized MI (0-100)
1167
- */
1168
- export function computeMaintainabilityIndex(volume, cyclomatic, sloc, commentRatio) {
1169
- // Guard against zero/negative values in logarithms
1170
- const safeVolume = Math.max(volume, 1);
1171
- const safeSLOC = Math.max(sloc, 1);
1172
-
1173
- let mi = 171 - 5.2 * Math.log(safeVolume) - 0.23 * cyclomatic - 16.2 * Math.log(safeSLOC);
1174
-
1175
- if (commentRatio != null && commentRatio > 0) {
1176
- mi += 50 * Math.sin(Math.sqrt(2.4 * commentRatio));
1177
- }
1178
-
1179
- // Microsoft normalization: 0-100 scale
1180
- const normalized = Math.max(0, Math.min(100, (mi * 100) / 171));
1181
- return +normalized.toFixed(1);
1182
- }
106
+ // Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
107
+ export const computeMaintainabilityIndex = _computeMaintainabilityIndex;
1183
108
 
1184
109
  // ─── Algorithm: Single-Traversal DFS ──────────────────────────────────────
1185
110
 
@@ -1357,6 +282,8 @@ export function computeFunctionComplexity(functionNode, language) {
1357
282
  * traversal, avoiding two separate DFS walks per function node at build time.
1358
283
  * LOC is text-based (not tree-based) and computed separately (very cheap).
1359
284
  *
285
+ * Now delegates to the complexity visitor via the unified walker.
286
+ *
1360
287
  * @param {object} functionNode - tree-sitter node for the function
1361
288
  * @param {string} langId - Language ID (e.g. 'javascript', 'python')
1362
289
  * @returns {{ cognitive: number, cyclomatic: number, maxNesting: number, halstead: object|null, loc: object, mi: number } | null}
@@ -1366,207 +293,34 @@ export function computeAllMetrics(functionNode, langId) {
1366
293
  if (!cRules) return null;
1367
294
  const hRules = HALSTEAD_RULES.get(langId);
1368
295
 
1369
- // ── Complexity state ──
1370
- let cognitive = 0;
1371
- let cyclomatic = 1; // McCabe starts at 1
1372
- let maxNesting = 0;
1373
-
1374
- // ── Halstead state ──
1375
- const operators = hRules ? new Map() : null;
1376
- const operands = hRules ? new Map() : null;
1377
-
1378
- function walk(node, nestingLevel, isTopFunction, halsteadSkip) {
1379
- if (!node) return;
296
+ const visitor = createComplexityVisitor(cRules, hRules, { langId });
1380
297
 
1381
- const type = node.type;
298
+ const nestingNodes = new Set(cRules.nestingNodes);
299
+ // NOTE: do NOT add functionNodes here — in function-level mode the walker
300
+ // walks a single function node, and adding it to nestingNodeTypes would
301
+ // inflate context.nestingLevel by +1 for the entire body.
1382
302
 
1383
- // ── Halstead classification ──
1384
- // Propagate skip through type-annotation subtrees (e.g. TS generics, Java type params)
1385
- const skipH = halsteadSkip || (hRules ? hRules.skipTypes.has(type) : false);
1386
- if (hRules && !skipH) {
1387
- // Compound operators (non-leaf): count node type as operator
1388
- if (hRules.compoundOperators.has(type)) {
1389
- operators.set(type, (operators.get(type) || 0) + 1);
1390
- }
1391
- // Leaf nodes: classify as operator or operand
1392
- if (node.childCount === 0) {
1393
- if (hRules.operatorLeafTypes.has(type)) {
1394
- operators.set(type, (operators.get(type) || 0) + 1);
1395
- } else if (hRules.operandLeafTypes.has(type)) {
1396
- const text = node.text;
1397
- operands.set(text, (operands.get(text) || 0) + 1);
1398
- }
1399
- }
1400
- }
1401
-
1402
- // ── Complexity: track nesting depth ──
1403
- if (nestingLevel > maxNesting) maxNesting = nestingLevel;
1404
-
1405
- // Handle logical operators in binary expressions
1406
- if (type === cRules.logicalNodeType) {
1407
- const op = node.child(1)?.type;
1408
- if (op && cRules.logicalOperators.has(op)) {
1409
- cyclomatic++;
1410
- const parent = node.parent;
1411
- let sameSequence = false;
1412
- if (parent && parent.type === cRules.logicalNodeType) {
1413
- const parentOp = parent.child(1)?.type;
1414
- if (parentOp === op) sameSequence = true;
1415
- }
1416
- if (!sameSequence) cognitive++;
1417
- for (let i = 0; i < node.childCount; i++) {
1418
- walk(node.child(i), nestingLevel, false, skipH);
1419
- }
1420
- return;
1421
- }
1422
- }
1423
-
1424
- // Handle optional chaining (cyclomatic only)
1425
- if (type === cRules.optionalChainType) {
1426
- cyclomatic++;
1427
- }
1428
-
1429
- // Handle branch/control flow nodes (skip keyword leaf tokens like Ruby's `if`)
1430
- if (cRules.branchNodes.has(type) && node.childCount > 0) {
1431
- // Pattern A: else clause wraps if (JS/C#/Rust)
1432
- if (cRules.elseNodeType && type === cRules.elseNodeType) {
1433
- const firstChild = node.namedChild(0);
1434
- if (firstChild && firstChild.type === cRules.ifNodeType) {
1435
- for (let i = 0; i < node.childCount; i++) {
1436
- walk(node.child(i), nestingLevel, false, skipH);
1437
- }
1438
- return;
1439
- }
1440
- cognitive++;
1441
- for (let i = 0; i < node.childCount; i++) {
1442
- walk(node.child(i), nestingLevel, false, skipH);
1443
- }
1444
- return;
1445
- }
1446
-
1447
- // Pattern B: explicit elif node (Python/Ruby/PHP)
1448
- if (cRules.elifNodeType && type === cRules.elifNodeType) {
1449
- cognitive++;
1450
- cyclomatic++;
1451
- for (let i = 0; i < node.childCount; i++) {
1452
- walk(node.child(i), nestingLevel, false, skipH);
1453
- }
1454
- return;
1455
- }
1456
-
1457
- // Detect else-if via Pattern A or C
1458
- let isElseIf = false;
1459
- if (type === cRules.ifNodeType) {
1460
- if (cRules.elseViaAlternative) {
1461
- isElseIf =
1462
- node.parent?.type === cRules.ifNodeType &&
1463
- node.parent.childForFieldName('alternative')?.id === node.id;
1464
- } else if (cRules.elseNodeType) {
1465
- isElseIf = node.parent?.type === cRules.elseNodeType;
1466
- }
1467
- }
1468
-
1469
- if (isElseIf) {
1470
- cognitive++;
1471
- cyclomatic++;
1472
- for (let i = 0; i < node.childCount; i++) {
1473
- walk(node.child(i), nestingLevel, false, skipH);
1474
- }
1475
- return;
1476
- }
1477
-
1478
- // Regular branch node
1479
- cognitive += 1 + nestingLevel;
1480
- cyclomatic++;
1481
-
1482
- // Switch-like nodes don't add cyclomatic themselves (cases do)
1483
- if (cRules.switchLikeNodes?.has(type)) {
1484
- cyclomatic--;
1485
- }
1486
-
1487
- if (cRules.nestingNodes.has(type)) {
1488
- for (let i = 0; i < node.childCount; i++) {
1489
- walk(node.child(i), nestingLevel + 1, false, skipH);
1490
- }
1491
- return;
1492
- }
1493
- }
1494
-
1495
- // Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
1496
- if (
1497
- cRules.elseViaAlternative &&
1498
- type !== cRules.ifNodeType &&
1499
- node.parent?.type === cRules.ifNodeType &&
1500
- node.parent.childForFieldName('alternative')?.id === node.id
1501
- ) {
1502
- cognitive++;
1503
- for (let i = 0; i < node.childCount; i++) {
1504
- walk(node.child(i), nestingLevel, false, skipH);
1505
- }
1506
- return;
1507
- }
1508
-
1509
- // Handle case nodes (cyclomatic only, skip keyword leaves)
1510
- if (cRules.caseNodes.has(type) && node.childCount > 0) {
1511
- cyclomatic++;
1512
- }
1513
-
1514
- // Handle nested function definitions (increase nesting)
1515
- if (!isTopFunction && cRules.functionNodes.has(type)) {
1516
- for (let i = 0; i < node.childCount; i++) {
1517
- walk(node.child(i), nestingLevel + 1, false, skipH);
1518
- }
1519
- return;
1520
- }
1521
-
1522
- // Walk children
1523
- for (let i = 0; i < node.childCount; i++) {
1524
- walk(node.child(i), nestingLevel, false, skipH);
1525
- }
1526
- }
1527
-
1528
- walk(functionNode, 0, true, false);
1529
-
1530
- // ── Compute Halstead derived metrics ──
1531
- let halstead = null;
1532
- if (hRules && operators && operands) {
1533
- const n1 = operators.size;
1534
- const n2 = operands.size;
1535
- let bigN1 = 0;
1536
- for (const c of operators.values()) bigN1 += c;
1537
- let bigN2 = 0;
1538
- for (const c of operands.values()) bigN2 += c;
1539
-
1540
- const vocabulary = n1 + n2;
1541
- const length = bigN1 + bigN2;
1542
- const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
1543
- const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
1544
- const effort = difficulty * volume;
1545
- const bugs = volume / 3000;
1546
-
1547
- halstead = {
1548
- n1,
1549
- n2,
1550
- bigN1,
1551
- bigN2,
1552
- vocabulary,
1553
- length,
1554
- volume: +volume.toFixed(2),
1555
- difficulty: +difficulty.toFixed(2),
1556
- effort: +effort.toFixed(2),
1557
- bugs: +bugs.toFixed(4),
1558
- };
1559
- }
303
+ const results = walkWithVisitors(functionNode, [visitor], langId, {
304
+ nestingNodeTypes: nestingNodes,
305
+ });
1560
306
 
1561
- // ── LOC metrics (text-based, cheap) ──
1562
- const loc = computeLOCMetrics(functionNode, langId);
307
+ const rawResult = results.complexity;
1563
308
 
1564
- // ── Maintainability Index ──
1565
- const volume = halstead ? halstead.volume : 0;
309
+ // The visitor's finish() in function-level mode returns the raw metrics
310
+ // but without LOC (needs the functionNode text). Compute LOC + MI here.
311
+ const loc = _computeLOCMetrics(functionNode, langId);
312
+ const volume = rawResult.halstead ? rawResult.halstead.volume : 0;
1566
313
  const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
1567
- const mi = computeMaintainabilityIndex(volume, cyclomatic, loc.sloc, commentRatio);
314
+ const mi = _computeMaintainabilityIndex(volume, rawResult.cyclomatic, loc.sloc, commentRatio);
1568
315
 
1569
- return { cognitive, cyclomatic, maxNesting, halstead, loc, mi };
316
+ return {
317
+ cognitive: rawResult.cognitive,
318
+ cyclomatic: rawResult.cyclomatic,
319
+ maxNesting: rawResult.maxNesting,
320
+ halstead: rawResult.halstead,
321
+ loc,
322
+ mi,
323
+ };
1570
324
  }
1571
325
 
1572
326
  // ─── Build-Time: Compute Metrics for Changed Files ────────────────────────
@@ -1574,34 +328,7 @@ export function computeAllMetrics(functionNode, langId) {
1574
328
  /**
1575
329
  * Find the function body node in a parse tree that matches a given line range.
1576
330
  */
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
- }
331
+ export { _findFunctionNode as findFunctionNode };
1605
332
 
1606
333
  /**
1607
334
  * Re-parse changed files with WASM tree-sitter, find function AST subtrees,
@@ -1635,12 +362,7 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
1635
362
  if (needsFallback) {
1636
363
  const { createParsers } = await import('./parser.js');
1637
364
  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
- }
365
+ extToLang = buildExtToLangMap();
1644
366
  }
1645
367
 
1646
368
  const { getParser } = await import('./parser.js');
@@ -1655,10 +377,6 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
1655
377
  maintainability_index)
1656
378
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1657
379
  );
1658
- const getNodeId = db.prepare(
1659
- "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
1660
- );
1661
-
1662
380
  let analyzed = 0;
1663
381
 
1664
382
  const tx = db.transaction(() => {
@@ -1705,12 +423,12 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
1705
423
 
1706
424
  // Use pre-computed complexity from native engine if available
1707
425
  if (def.complexity) {
1708
- const row = getNodeId.get(def.name, relPath, def.line);
1709
- if (!row) continue;
426
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
427
+ if (!nodeId) continue;
1710
428
  const ch = def.complexity.halstead;
1711
429
  const cl = def.complexity.loc;
1712
430
  upsert.run(
1713
- row.id,
431
+ nodeId,
1714
432
  def.complexity.cognitive,
1715
433
  def.complexity.cyclomatic,
1716
434
  def.complexity.maxNesting ?? 0,
@@ -1743,12 +461,12 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
1743
461
  const metrics = computeAllMetrics(funcNode, langId);
1744
462
  if (!metrics) continue;
1745
463
 
1746
- const row = getNodeId.get(def.name, relPath, def.line);
1747
- if (!row) continue;
464
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
465
+ if (!nodeId) continue;
1748
466
 
1749
467
  const h = metrics.halstead;
1750
468
  upsert.run(
1751
- row.id,
469
+ nodeId,
1752
470
  metrics.cognitive,
1753
471
  metrics.cyclomatic,
1754
472
  metrics.maxNesting,
@@ -1797,204 +515,211 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
1797
515
  */
1798
516
  export function complexityData(customDbPath, opts = {}) {
1799
517
  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
- };
518
+ try {
519
+ const sort = opts.sort || 'cognitive';
520
+ const noTests = opts.noTests || false;
521
+ const aboveThreshold = opts.aboveThreshold || false;
522
+ const target = opts.target || null;
523
+ const fileFilter = opts.file || null;
524
+ const kindFilter = opts.kind || null;
525
+
526
+ // Load thresholds from config
527
+ const config = loadConfig(process.cwd());
528
+ const thresholds = config.manifesto?.rules || {
529
+ cognitive: { warn: 15, fail: null },
530
+ cyclomatic: { warn: 10, fail: null },
531
+ maxNesting: { warn: 4, fail: null },
532
+ maintainabilityIndex: { warn: 20, fail: null },
533
+ };
1815
534
 
1816
- // Build query
1817
- let where = "WHERE n.kind IN ('function','method')";
1818
- const params = [];
535
+ // Build query
536
+ let where = "WHERE n.kind IN ('function','method')";
537
+ const params = [];
1819
538
 
1820
- if (noTests) {
1821
- where += ` AND n.file NOT LIKE '%.test.%'
539
+ if (noTests) {
540
+ where += ` AND n.file NOT LIKE '%.test.%'
1822
541
  AND n.file NOT LIKE '%.spec.%'
1823
542
  AND n.file NOT LIKE '%__test__%'
1824
543
  AND n.file NOT LIKE '%__tests__%'
1825
544
  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
545
  }
1848
- if (isValidThreshold(thresholds.cyclomatic?.warn)) {
1849
- conditions.push(`fc.cyclomatic >= ${thresholds.cyclomatic.warn}`);
546
+ if (target) {
547
+ where += ' AND n.name LIKE ?';
548
+ params.push(`%${target}%`);
1850
549
  }
1851
- if (isValidThreshold(thresholds.maxNesting?.warn)) {
1852
- conditions.push(`fc.max_nesting >= ${thresholds.maxNesting.warn}`);
550
+ if (fileFilter) {
551
+ where += ' AND n.file LIKE ?';
552
+ params.push(`%${fileFilter}%`);
1853
553
  }
1854
- if (isValidThreshold(thresholds.maintainabilityIndex?.warn)) {
1855
- conditions.push(
1856
- `fc.maintainability_index > 0 AND fc.maintainability_index <= ${thresholds.maintainabilityIndex.warn}`,
1857
- );
554
+ if (kindFilter) {
555
+ where += ' AND n.kind = ?';
556
+ params.push(kindFilter);
1858
557
  }
1859
- if (conditions.length > 0) {
1860
- having = `AND (${conditions.join(' OR ')})`;
558
+
559
+ const isValidThreshold = (v) => typeof v === 'number' && Number.isFinite(v);
560
+
561
+ let having = '';
562
+ if (aboveThreshold) {
563
+ const conditions = [];
564
+ if (isValidThreshold(thresholds.cognitive?.warn)) {
565
+ conditions.push(`fc.cognitive >= ${thresholds.cognitive.warn}`);
566
+ }
567
+ if (isValidThreshold(thresholds.cyclomatic?.warn)) {
568
+ conditions.push(`fc.cyclomatic >= ${thresholds.cyclomatic.warn}`);
569
+ }
570
+ if (isValidThreshold(thresholds.maxNesting?.warn)) {
571
+ conditions.push(`fc.max_nesting >= ${thresholds.maxNesting.warn}`);
572
+ }
573
+ if (isValidThreshold(thresholds.maintainabilityIndex?.warn)) {
574
+ conditions.push(
575
+ `fc.maintainability_index > 0 AND fc.maintainability_index <= ${thresholds.maintainabilityIndex.warn}`,
576
+ );
577
+ }
578
+ if (conditions.length > 0) {
579
+ having = `AND (${conditions.join(' OR ')})`;
580
+ }
1861
581
  }
1862
- }
1863
582
 
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';
583
+ const orderMap = {
584
+ cognitive: 'fc.cognitive DESC',
585
+ cyclomatic: 'fc.cyclomatic DESC',
586
+ nesting: 'fc.max_nesting DESC',
587
+ mi: 'fc.maintainability_index ASC',
588
+ volume: 'fc.halstead_volume DESC',
589
+ effort: 'fc.halstead_effort DESC',
590
+ bugs: 'fc.halstead_bugs DESC',
591
+ loc: 'fc.loc DESC',
592
+ };
593
+ const orderBy = orderMap[sort] || 'fc.cognitive DESC';
1875
594
 
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;
595
+ let rows;
1893
596
  try {
1894
- hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
597
+ rows = db
598
+ .prepare(
599
+ `SELECT n.name, n.kind, n.file, n.line, n.end_line,
600
+ fc.cognitive, fc.cyclomatic, fc.max_nesting,
601
+ fc.loc, fc.sloc, fc.maintainability_index,
602
+ fc.halstead_volume, fc.halstead_difficulty, fc.halstead_effort, fc.halstead_bugs
603
+ FROM function_complexity fc
604
+ JOIN nodes n ON fc.node_id = n.id
605
+ ${where} ${having}
606
+ ORDER BY ${orderBy}`,
607
+ )
608
+ .all(...params);
1895
609
  } catch {
1896
- /* ignore */
610
+ // Check if graph has nodes even though complexity table is missing/empty
611
+ let hasGraph = false;
612
+ try {
613
+ hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
614
+ } catch {
615
+ /* ignore */
616
+ }
617
+ return { functions: [], summary: null, thresholds, hasGraph };
1897
618
  }
1898
- db.close();
1899
- return { functions: [], summary: null, thresholds, hasGraph };
1900
- }
1901
-
1902
- // Post-filter test files if needed (belt-and-suspenders for isTestFile)
1903
- const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
1904
619
 
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
- });
620
+ // Post-filter test files if needed (belt-and-suspenders for isTestFile)
621
+ const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
1944
622
 
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.%'` : ''}`,
623
+ const functions = filtered.map((r) => {
624
+ const exceeds = [];
625
+ if (isValidThreshold(thresholds.cognitive?.warn) && r.cognitive >= thresholds.cognitive.warn)
626
+ exceeds.push('cognitive');
627
+ if (
628
+ isValidThreshold(thresholds.cyclomatic?.warn) &&
629
+ r.cyclomatic >= thresholds.cyclomatic.warn
630
+ )
631
+ exceeds.push('cyclomatic');
632
+ if (
633
+ isValidThreshold(thresholds.maxNesting?.warn) &&
634
+ r.max_nesting >= thresholds.maxNesting.warn
1954
635
  )
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,
636
+ exceeds.push('maxNesting');
637
+ if (
638
+ isValidThreshold(thresholds.maintainabilityIndex?.warn) &&
639
+ r.maintainability_index > 0 &&
640
+ r.maintainability_index <= thresholds.maintainabilityIndex.warn
641
+ )
642
+ exceeds.push('maintainabilityIndex');
643
+
644
+ return {
645
+ name: r.name,
646
+ kind: r.kind,
647
+ file: r.file,
648
+ line: r.line,
649
+ endLine: r.end_line || null,
650
+ cognitive: r.cognitive,
651
+ cyclomatic: r.cyclomatic,
652
+ maxNesting: r.max_nesting,
653
+ loc: r.loc || 0,
654
+ sloc: r.sloc || 0,
655
+ maintainabilityIndex: r.maintainability_index || 0,
656
+ halstead: {
657
+ volume: r.halstead_volume || 0,
658
+ difficulty: r.halstead_difficulty || 0,
659
+ effort: r.halstead_effort || 0,
660
+ bugs: r.halstead_bugs || 0,
661
+ },
662
+ exceeds: exceeds.length > 0 ? exceeds : undefined,
1979
663
  };
1980
- }
1981
- } catch {
1982
- /* ignore */
1983
- }
664
+ });
1984
665
 
1985
- // When summary is null (no complexity rows), check if graph has nodes
1986
- let hasGraph = false;
1987
- if (summary === null) {
666
+ // Summary stats
667
+ let summary = null;
1988
668
  try {
1989
- hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
669
+ const allRows = db
670
+ .prepare(
671
+ `SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
672
+ FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
673
+ WHERE n.kind IN ('function','method')
674
+ ${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.%'` : ''}`,
675
+ )
676
+ .all();
677
+
678
+ if (allRows.length > 0) {
679
+ const miValues = allRows.map((r) => r.maintainability_index || 0);
680
+ summary = {
681
+ analyzed: allRows.length,
682
+ avgCognitive: +(allRows.reduce((s, r) => s + r.cognitive, 0) / allRows.length).toFixed(1),
683
+ avgCyclomatic: +(allRows.reduce((s, r) => s + r.cyclomatic, 0) / allRows.length).toFixed(
684
+ 1,
685
+ ),
686
+ maxCognitive: Math.max(...allRows.map((r) => r.cognitive)),
687
+ maxCyclomatic: Math.max(...allRows.map((r) => r.cyclomatic)),
688
+ avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
689
+ minMI: +Math.min(...miValues).toFixed(1),
690
+ aboveWarn: allRows.filter(
691
+ (r) =>
692
+ (isValidThreshold(thresholds.cognitive?.warn) &&
693
+ r.cognitive >= thresholds.cognitive.warn) ||
694
+ (isValidThreshold(thresholds.cyclomatic?.warn) &&
695
+ r.cyclomatic >= thresholds.cyclomatic.warn) ||
696
+ (isValidThreshold(thresholds.maxNesting?.warn) &&
697
+ r.max_nesting >= thresholds.maxNesting.warn) ||
698
+ (isValidThreshold(thresholds.maintainabilityIndex?.warn) &&
699
+ r.maintainability_index > 0 &&
700
+ r.maintainability_index <= thresholds.maintainabilityIndex.warn),
701
+ ).length,
702
+ };
703
+ }
1990
704
  } catch {
1991
705
  /* ignore */
1992
706
  }
1993
- }
1994
707
 
1995
- db.close();
1996
- const base = { functions, summary, thresholds, hasGraph };
1997
- return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
708
+ // When summary is null (no complexity rows), check if graph has nodes
709
+ let hasGraph = false;
710
+ if (summary === null) {
711
+ try {
712
+ hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
713
+ } catch {
714
+ /* ignore */
715
+ }
716
+ }
717
+
718
+ const base = { functions, summary, thresholds, hasGraph };
719
+ return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
720
+ } finally {
721
+ db.close();
722
+ }
1998
723
  }
1999
724
 
2000
725
  /**
@@ -2076,85 +801,3 @@ export function* iterComplexity(customDbPath, opts = {}) {
2076
801
  db.close();
2077
802
  }
2078
803
  }
2079
-
2080
- /**
2081
- * Format complexity output for CLI display.
2082
- */
2083
- export function complexity(customDbPath, opts = {}) {
2084
- const data = complexityData(customDbPath, opts);
2085
-
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
- }
2094
-
2095
- if (data.functions.length === 0) {
2096
- if (data.summary === null) {
2097
- if (data.hasGraph) {
2098
- console.log(
2099
- '\nNo complexity data found, but a graph exists. Run "codegraph build --no-incremental" to populate complexity metrics.\n',
2100
- );
2101
- } else {
2102
- console.log(
2103
- '\nNo complexity data found. Run "codegraph build" first to analyze your codebase.\n',
2104
- );
2105
- }
2106
- } else {
2107
- console.log('\nNo functions match the given filters.\n');
2108
- }
2109
- return;
2110
- }
2111
-
2112
- const header = opts.aboveThreshold ? 'Functions Above Threshold' : 'Function Complexity';
2113
- console.log(`\n# ${header}\n`);
2114
-
2115
- if (opts.health) {
2116
- // Health-focused view with Halstead + MI columns
2117
- console.log(
2118
- ` ${'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)}`,
2119
- );
2120
- console.log(
2121
- ` ${'─'.repeat(35)} ${'─'.repeat(25)} ${'─'.repeat(5)} ${'─'.repeat(7)} ${'─'.repeat(6)} ${'─'.repeat(9)} ${'─'.repeat(6)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
2122
- );
2123
-
2124
- for (const fn of data.functions) {
2125
- const name = fn.name.length > 33 ? `${fn.name.slice(0, 32)}…` : fn.name;
2126
- const file = fn.file.length > 23 ? `…${fn.file.slice(-22)}` : fn.file;
2127
- const miWarn = fn.exceeds?.includes('maintainabilityIndex') ? '!' : ' ';
2128
- console.log(
2129
- ` ${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)}`,
2130
- );
2131
- }
2132
- } else {
2133
- // Default view with MI column appended
2134
- console.log(
2135
- ` ${'Function'.padEnd(40)} ${'File'.padEnd(30)} ${'Cog'.padStart(4)} ${'Cyc'.padStart(4)} ${'Nest'.padStart(5)} ${'MI'.padStart(5)}`,
2136
- );
2137
- console.log(
2138
- ` ${'─'.repeat(40)} ${'─'.repeat(30)} ${'─'.repeat(4)} ${'─'.repeat(4)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
2139
- );
2140
-
2141
- for (const fn of data.functions) {
2142
- const name = fn.name.length > 38 ? `${fn.name.slice(0, 37)}…` : fn.name;
2143
- const file = fn.file.length > 28 ? `…${fn.file.slice(-27)}` : fn.file;
2144
- const warn = fn.exceeds ? ' !' : '';
2145
- const mi = fn.maintainabilityIndex > 0 ? String(fn.maintainabilityIndex) : '-';
2146
- console.log(
2147
- ` ${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}`,
2148
- );
2149
- }
2150
- }
2151
-
2152
- if (data.summary) {
2153
- const s = data.summary;
2154
- const miPart = s.avgMI != null ? ` | avg MI: ${s.avgMI}` : '';
2155
- console.log(
2156
- `\n ${s.analyzed} functions analyzed | avg cognitive: ${s.avgCognitive} | avg cyclomatic: ${s.avgCyclomatic}${miPart} | ${s.aboveWarn} above threshold`,
2157
- );
2158
- }
2159
- console.log();
2160
- }