@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.
- package/README.md +5 -5
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +8 -9
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +26 -166
- package/src/audit.js +2 -88
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +82 -172
- package/src/builder.js +48 -184
- package/src/cfg.js +148 -1174
- package/src/check.js +1 -84
- package/src/cli.js +118 -197
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +22 -96
- package/src/complexity.js +234 -1591
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -1352
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +58 -399
- package/src/embedder.js +158 -174
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +153 -222
- package/src/index.js +53 -16
- package/src/infrastructure/result-formatter.js +21 -0
- package/src/infrastructure/test-filter.js +7 -0
- package/src/kinds.js +50 -0
- package/src/manifesto.js +1 -82
- package/src/mcp.js +37 -20
- package/src/owners.js +127 -182
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1271 -2416
- package/src/sequence.js +179 -223
- package/src/structure.js +211 -269
- package/src/triage.js +117 -212
- package/src/viewer.js +1 -1
- 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
|
|
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
|
-
//
|
|
300
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
1562
|
-
const loc = computeLOCMetrics(functionNode, langId);
|
|
307
|
+
const rawResult = results.complexity;
|
|
1563
308
|
|
|
1564
|
-
//
|
|
1565
|
-
|
|
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 =
|
|
314
|
+
const mi = _computeMaintainabilityIndex(volume, rawResult.cyclomatic, loc.sloc, commentRatio);
|
|
1568
315
|
|
|
1569
|
-
return {
|
|
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
|
|
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 =
|
|
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
|
|
1709
|
-
if (!
|
|
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
|
-
|
|
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
|
|
1747
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
535
|
+
// Build query
|
|
536
|
+
let where = "WHERE n.kind IN ('function','method')";
|
|
537
|
+
const params = [];
|
|
1819
538
|
|
|
1820
|
-
|
|
1821
|
-
|
|
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 (
|
|
1849
|
-
|
|
546
|
+
if (target) {
|
|
547
|
+
where += ' AND n.name LIKE ?';
|
|
548
|
+
params.push(`%${target}%`);
|
|
1850
549
|
}
|
|
1851
|
-
if (
|
|
1852
|
-
|
|
550
|
+
if (fileFilter) {
|
|
551
|
+
where += ' AND n.file LIKE ?';
|
|
552
|
+
params.push(`%${fileFilter}%`);
|
|
1853
553
|
}
|
|
1854
|
-
if (
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
);
|
|
554
|
+
if (kindFilter) {
|
|
555
|
+
where += ' AND n.kind = ?';
|
|
556
|
+
params.push(kindFilter);
|
|
1858
557
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1906
|
-
const
|
|
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
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
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
|
-
|
|
1986
|
-
|
|
1987
|
-
if (summary === null) {
|
|
666
|
+
// Summary stats
|
|
667
|
+
let summary = null;
|
|
1988
668
|
try {
|
|
1989
|
-
|
|
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
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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
|
-
}
|