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