@optave/codegraph 3.1.1 → 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/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -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 +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +48 -179
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +20 -19
- 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 +12 -83
- package/src/complexity.js +42 -356
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- 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 +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +23 -19
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/owners.js +1 -56
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/communities.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import Graph from 'graphology';
|
|
3
3
|
import louvain from 'graphology-communities-louvain';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getCallableNodes,
|
|
6
|
+
getCallEdges,
|
|
7
|
+
getFileNodesAll,
|
|
8
|
+
getImportEdges,
|
|
9
|
+
openReadonlyOrFail,
|
|
10
|
+
} from './db.js';
|
|
11
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
5
12
|
import { paginateResult } from './paginate.js';
|
|
6
|
-
import { outputResult } from './result-formatter.js';
|
|
7
|
-
import { isTestFile } from './test-filter.js';
|
|
8
13
|
|
|
9
14
|
// ─── Graph Construction ───────────────────────────────────────────────
|
|
10
15
|
|
|
@@ -22,9 +27,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
22
27
|
|
|
23
28
|
if (opts.functions) {
|
|
24
29
|
// Function-level: nodes = function/method/class symbols, edges = calls
|
|
25
|
-
let nodes = db
|
|
26
|
-
.prepare("SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')")
|
|
27
|
-
.all();
|
|
30
|
+
let nodes = getCallableNodes(db);
|
|
28
31
|
if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
29
32
|
|
|
30
33
|
const nodeIds = new Set();
|
|
@@ -34,7 +37,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
34
37
|
nodeIds.add(n.id);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
const edges = db
|
|
40
|
+
const edges = getCallEdges(db);
|
|
38
41
|
for (const e of edges) {
|
|
39
42
|
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
40
43
|
const src = String(e.source_id);
|
|
@@ -46,7 +49,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
46
49
|
}
|
|
47
50
|
} else {
|
|
48
51
|
// File-level: nodes = files, edges = imports + imports-type (deduplicated, cross-file)
|
|
49
|
-
let nodes = db
|
|
52
|
+
let nodes = getFileNodesAll(db);
|
|
50
53
|
if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
51
54
|
|
|
52
55
|
const nodeIds = new Set();
|
|
@@ -56,9 +59,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
56
59
|
nodeIds.add(n.id);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
const edges = db
|
|
60
|
-
.prepare("SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')")
|
|
61
|
-
.all();
|
|
62
|
+
const edges = getImportEdges(db);
|
|
62
63
|
for (const e of edges) {
|
|
63
64
|
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
64
65
|
const src = String(e.source_id);
|
|
@@ -232,75 +233,3 @@ export function communitySummaryForStats(customDbPath, opts = {}) {
|
|
|
232
233
|
const data = communitiesData(customDbPath, { ...opts, drift: true });
|
|
233
234
|
return data.summary;
|
|
234
235
|
}
|
|
235
|
-
|
|
236
|
-
// ─── CLI Display ──────────────────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* CLI entry point: run community detection and print results.
|
|
240
|
-
*
|
|
241
|
-
* @param {string} [customDbPath]
|
|
242
|
-
* @param {object} [opts]
|
|
243
|
-
*/
|
|
244
|
-
export function communities(customDbPath, opts = {}) {
|
|
245
|
-
const data = communitiesData(customDbPath, opts);
|
|
246
|
-
|
|
247
|
-
if (outputResult(data, 'communities', opts)) return;
|
|
248
|
-
|
|
249
|
-
if (data.summary.communityCount === 0) {
|
|
250
|
-
console.log(
|
|
251
|
-
'\nNo communities detected. The graph may be too small or disconnected.\n' +
|
|
252
|
-
'Run "codegraph build" first to populate the graph.\n',
|
|
253
|
-
);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const mode = opts.functions ? 'Function' : 'File';
|
|
258
|
-
console.log(`\n# ${mode}-Level Communities\n`);
|
|
259
|
-
console.log(
|
|
260
|
-
` ${data.summary.communityCount} communities | ${data.summary.nodeCount} nodes | modularity: ${data.summary.modularity} | drift: ${data.summary.driftScore}%\n`,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
if (!opts.drift) {
|
|
264
|
-
for (const c of data.communities) {
|
|
265
|
-
const dirs = Object.entries(c.directories)
|
|
266
|
-
.sort((a, b) => b[1] - a[1])
|
|
267
|
-
.map(([d, n]) => `${d} (${n})`)
|
|
268
|
-
.join(', ');
|
|
269
|
-
console.log(` Community ${c.id} (${c.size} members): ${dirs}`);
|
|
270
|
-
if (c.members) {
|
|
271
|
-
const shown = c.members.slice(0, 8);
|
|
272
|
-
for (const m of shown) {
|
|
273
|
-
const kind = m.kind ? ` [${m.kind}]` : '';
|
|
274
|
-
console.log(` - ${m.name}${kind} ${m.file}`);
|
|
275
|
-
}
|
|
276
|
-
if (c.members.length > 8) {
|
|
277
|
-
console.log(` ... and ${c.members.length - 8} more`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Drift analysis
|
|
284
|
-
const d = data.drift;
|
|
285
|
-
if (d.splitCandidates.length > 0 || d.mergeCandidates.length > 0) {
|
|
286
|
-
console.log(`\n# Drift Analysis (score: ${data.summary.driftScore}%)\n`);
|
|
287
|
-
|
|
288
|
-
if (d.splitCandidates.length > 0) {
|
|
289
|
-
console.log(' Split candidates (directories spanning multiple communities):');
|
|
290
|
-
for (const s of d.splitCandidates.slice(0, 10)) {
|
|
291
|
-
console.log(` - ${s.directory} → ${s.communityCount} communities`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (d.mergeCandidates.length > 0) {
|
|
296
|
-
console.log(' Merge candidates (communities spanning multiple directories):');
|
|
297
|
-
for (const m of d.mergeCandidates.slice(0, 10)) {
|
|
298
|
-
console.log(
|
|
299
|
-
` - Community ${m.communityId} (${m.size} members) → ${m.directoryCount} dirs: ${m.directories.join(', ')}`,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
console.log();
|
|
306
|
-
}
|
package/src/complexity.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
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';
|
|
3
7
|
import { COMPLEXITY_RULES, HALSTEAD_RULES } from './ast-analysis/rules/index.js';
|
|
4
8
|
import {
|
|
5
9
|
findFunctionNode as _findFunctionNode,
|
|
6
10
|
buildExtensionSet,
|
|
7
11
|
buildExtToLangMap,
|
|
8
12
|
} from './ast-analysis/shared.js';
|
|
13
|
+
import { walkWithVisitors } from './ast-analysis/visitor.js';
|
|
14
|
+
import { createComplexityVisitor } from './ast-analysis/visitors/complexity-visitor.js';
|
|
9
15
|
import { loadConfig } from './config.js';
|
|
10
|
-
import { openReadonlyOrFail } from './db.js';
|
|
16
|
+
import { getFunctionNodeId, openReadonlyOrFail } from './db.js';
|
|
17
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
11
18
|
import { info } from './logger.js';
|
|
12
19
|
import { paginateResult } from './paginate.js';
|
|
13
20
|
|
|
14
|
-
import { outputResult } from './result-formatter.js';
|
|
15
|
-
import { isTestFile } from './test-filter.js';
|
|
16
|
-
|
|
17
21
|
// Re-export rules for backward compatibility
|
|
18
22
|
export { COMPLEXITY_RULES, HALSTEAD_RULES };
|
|
19
23
|
|
|
@@ -95,80 +99,12 @@ export function computeHalsteadMetrics(functionNode, language) {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// ─── LOC Metrics Computation ──────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const COMMENT_PREFIXES = new Map([
|
|
102
|
-
['javascript', C_STYLE_PREFIXES],
|
|
103
|
-
['typescript', C_STYLE_PREFIXES],
|
|
104
|
-
['tsx', C_STYLE_PREFIXES],
|
|
105
|
-
['go', C_STYLE_PREFIXES],
|
|
106
|
-
['rust', C_STYLE_PREFIXES],
|
|
107
|
-
['java', C_STYLE_PREFIXES],
|
|
108
|
-
['csharp', C_STYLE_PREFIXES],
|
|
109
|
-
['python', ['#']],
|
|
110
|
-
['ruby', ['#']],
|
|
111
|
-
['php', ['//', '#', '/*', '*', '*/']],
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Compute LOC metrics from a function node's source text.
|
|
116
|
-
*
|
|
117
|
-
* @param {object} functionNode - tree-sitter node
|
|
118
|
-
* @param {string} [language] - Language ID (falls back to C-style prefixes)
|
|
119
|
-
* @returns {{ loc: number, sloc: number, commentLines: number }}
|
|
120
|
-
*/
|
|
121
|
-
export function computeLOCMetrics(functionNode, language) {
|
|
122
|
-
const text = functionNode.text;
|
|
123
|
-
const lines = text.split('\n');
|
|
124
|
-
const loc = lines.length;
|
|
125
|
-
const prefixes = (language && COMMENT_PREFIXES.get(language)) || C_STYLE_PREFIXES;
|
|
126
|
-
|
|
127
|
-
let commentLines = 0;
|
|
128
|
-
let blankLines = 0;
|
|
129
|
-
|
|
130
|
-
for (const line of lines) {
|
|
131
|
-
const trimmed = line.trim();
|
|
132
|
-
if (trimmed === '') {
|
|
133
|
-
blankLines++;
|
|
134
|
-
} else if (prefixes.some((p) => trimmed.startsWith(p))) {
|
|
135
|
-
commentLines++;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const sloc = Math.max(1, loc - blankLines - commentLines);
|
|
140
|
-
return { loc, sloc, commentLines };
|
|
141
|
-
}
|
|
102
|
+
// Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
|
|
103
|
+
export const computeLOCMetrics = _computeLOCMetrics;
|
|
142
104
|
|
|
143
105
|
// ─── Maintainability Index ────────────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* Compute normalized Maintainability Index (0-100 scale).
|
|
147
|
-
*
|
|
148
|
-
* Original SEI formula: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
|
|
149
|
-
* Microsoft normalization: max(0, min(100, MI * 100/171))
|
|
150
|
-
*
|
|
151
|
-
* @param {number} volume - Halstead volume
|
|
152
|
-
* @param {number} cyclomatic - Cyclomatic complexity
|
|
153
|
-
* @param {number} sloc - Source lines of code
|
|
154
|
-
* @param {number} [commentRatio] - Comment ratio (0-1), optional
|
|
155
|
-
* @returns {number} Normalized MI (0-100)
|
|
156
|
-
*/
|
|
157
|
-
export function computeMaintainabilityIndex(volume, cyclomatic, sloc, commentRatio) {
|
|
158
|
-
// Guard against zero/negative values in logarithms
|
|
159
|
-
const safeVolume = Math.max(volume, 1);
|
|
160
|
-
const safeSLOC = Math.max(sloc, 1);
|
|
161
|
-
|
|
162
|
-
let mi = 171 - 5.2 * Math.log(safeVolume) - 0.23 * cyclomatic - 16.2 * Math.log(safeSLOC);
|
|
163
|
-
|
|
164
|
-
if (commentRatio != null && commentRatio > 0) {
|
|
165
|
-
mi += 50 * Math.sin(Math.sqrt(2.4 * commentRatio));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Microsoft normalization: 0-100 scale
|
|
169
|
-
const normalized = Math.max(0, Math.min(100, (mi * 100) / 171));
|
|
170
|
-
return +normalized.toFixed(1);
|
|
171
|
-
}
|
|
106
|
+
// Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
|
|
107
|
+
export const computeMaintainabilityIndex = _computeMaintainabilityIndex;
|
|
172
108
|
|
|
173
109
|
// ─── Algorithm: Single-Traversal DFS ──────────────────────────────────────
|
|
174
110
|
|
|
@@ -346,6 +282,8 @@ export function computeFunctionComplexity(functionNode, language) {
|
|
|
346
282
|
* traversal, avoiding two separate DFS walks per function node at build time.
|
|
347
283
|
* LOC is text-based (not tree-based) and computed separately (very cheap).
|
|
348
284
|
*
|
|
285
|
+
* Now delegates to the complexity visitor via the unified walker.
|
|
286
|
+
*
|
|
349
287
|
* @param {object} functionNode - tree-sitter node for the function
|
|
350
288
|
* @param {string} langId - Language ID (e.g. 'javascript', 'python')
|
|
351
289
|
* @returns {{ cognitive: number, cyclomatic: number, maxNesting: number, halstead: object|null, loc: object, mi: number } | null}
|
|
@@ -355,207 +293,34 @@ export function computeAllMetrics(functionNode, langId) {
|
|
|
355
293
|
if (!cRules) return null;
|
|
356
294
|
const hRules = HALSTEAD_RULES.get(langId);
|
|
357
295
|
|
|
358
|
-
|
|
359
|
-
let cognitive = 0;
|
|
360
|
-
let cyclomatic = 1; // McCabe starts at 1
|
|
361
|
-
let maxNesting = 0;
|
|
362
|
-
|
|
363
|
-
// ── Halstead state ──
|
|
364
|
-
const operators = hRules ? new Map() : null;
|
|
365
|
-
const operands = hRules ? new Map() : null;
|
|
366
|
-
|
|
367
|
-
function walk(node, nestingLevel, isTopFunction, halsteadSkip) {
|
|
368
|
-
if (!node) return;
|
|
369
|
-
|
|
370
|
-
const type = node.type;
|
|
371
|
-
|
|
372
|
-
// ── Halstead classification ──
|
|
373
|
-
// Propagate skip through type-annotation subtrees (e.g. TS generics, Java type params)
|
|
374
|
-
const skipH = halsteadSkip || (hRules ? hRules.skipTypes.has(type) : false);
|
|
375
|
-
if (hRules && !skipH) {
|
|
376
|
-
// Compound operators (non-leaf): count node type as operator
|
|
377
|
-
if (hRules.compoundOperators.has(type)) {
|
|
378
|
-
operators.set(type, (operators.get(type) || 0) + 1);
|
|
379
|
-
}
|
|
380
|
-
// Leaf nodes: classify as operator or operand
|
|
381
|
-
if (node.childCount === 0) {
|
|
382
|
-
if (hRules.operatorLeafTypes.has(type)) {
|
|
383
|
-
operators.set(type, (operators.get(type) || 0) + 1);
|
|
384
|
-
} else if (hRules.operandLeafTypes.has(type)) {
|
|
385
|
-
const text = node.text;
|
|
386
|
-
operands.set(text, (operands.get(text) || 0) + 1);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ── Complexity: track nesting depth ──
|
|
392
|
-
if (nestingLevel > maxNesting) maxNesting = nestingLevel;
|
|
393
|
-
|
|
394
|
-
// Handle logical operators in binary expressions
|
|
395
|
-
if (type === cRules.logicalNodeType) {
|
|
396
|
-
const op = node.child(1)?.type;
|
|
397
|
-
if (op && cRules.logicalOperators.has(op)) {
|
|
398
|
-
cyclomatic++;
|
|
399
|
-
const parent = node.parent;
|
|
400
|
-
let sameSequence = false;
|
|
401
|
-
if (parent && parent.type === cRules.logicalNodeType) {
|
|
402
|
-
const parentOp = parent.child(1)?.type;
|
|
403
|
-
if (parentOp === op) sameSequence = true;
|
|
404
|
-
}
|
|
405
|
-
if (!sameSequence) cognitive++;
|
|
406
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
407
|
-
walk(node.child(i), nestingLevel, false, skipH);
|
|
408
|
-
}
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Handle optional chaining (cyclomatic only)
|
|
414
|
-
if (type === cRules.optionalChainType) {
|
|
415
|
-
cyclomatic++;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Handle branch/control flow nodes (skip keyword leaf tokens like Ruby's `if`)
|
|
419
|
-
if (cRules.branchNodes.has(type) && node.childCount > 0) {
|
|
420
|
-
// Pattern A: else clause wraps if (JS/C#/Rust)
|
|
421
|
-
if (cRules.elseNodeType && type === cRules.elseNodeType) {
|
|
422
|
-
const firstChild = node.namedChild(0);
|
|
423
|
-
if (firstChild && firstChild.type === cRules.ifNodeType) {
|
|
424
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
425
|
-
walk(node.child(i), nestingLevel, false, skipH);
|
|
426
|
-
}
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
cognitive++;
|
|
430
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
431
|
-
walk(node.child(i), nestingLevel, false, skipH);
|
|
432
|
-
}
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Pattern B: explicit elif node (Python/Ruby/PHP)
|
|
437
|
-
if (cRules.elifNodeType && type === cRules.elifNodeType) {
|
|
438
|
-
cognitive++;
|
|
439
|
-
cyclomatic++;
|
|
440
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
441
|
-
walk(node.child(i), nestingLevel, false, skipH);
|
|
442
|
-
}
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Detect else-if via Pattern A or C
|
|
447
|
-
let isElseIf = false;
|
|
448
|
-
if (type === cRules.ifNodeType) {
|
|
449
|
-
if (cRules.elseViaAlternative) {
|
|
450
|
-
isElseIf =
|
|
451
|
-
node.parent?.type === cRules.ifNodeType &&
|
|
452
|
-
node.parent.childForFieldName('alternative')?.id === node.id;
|
|
453
|
-
} else if (cRules.elseNodeType) {
|
|
454
|
-
isElseIf = node.parent?.type === cRules.elseNodeType;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (isElseIf) {
|
|
459
|
-
cognitive++;
|
|
460
|
-
cyclomatic++;
|
|
461
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
462
|
-
walk(node.child(i), nestingLevel, false, skipH);
|
|
463
|
-
}
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Regular branch node
|
|
468
|
-
cognitive += 1 + nestingLevel;
|
|
469
|
-
cyclomatic++;
|
|
470
|
-
|
|
471
|
-
// Switch-like nodes don't add cyclomatic themselves (cases do)
|
|
472
|
-
if (cRules.switchLikeNodes?.has(type)) {
|
|
473
|
-
cyclomatic--;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (cRules.nestingNodes.has(type)) {
|
|
477
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
478
|
-
walk(node.child(i), nestingLevel + 1, false, skipH);
|
|
479
|
-
}
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
|
|
485
|
-
if (
|
|
486
|
-
cRules.elseViaAlternative &&
|
|
487
|
-
type !== cRules.ifNodeType &&
|
|
488
|
-
node.parent?.type === cRules.ifNodeType &&
|
|
489
|
-
node.parent.childForFieldName('alternative')?.id === node.id
|
|
490
|
-
) {
|
|
491
|
-
cognitive++;
|
|
492
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
493
|
-
walk(node.child(i), nestingLevel, false, skipH);
|
|
494
|
-
}
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Handle case nodes (cyclomatic only, skip keyword leaves)
|
|
499
|
-
if (cRules.caseNodes.has(type) && node.childCount > 0) {
|
|
500
|
-
cyclomatic++;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Handle nested function definitions (increase nesting)
|
|
504
|
-
if (!isTopFunction && cRules.functionNodes.has(type)) {
|
|
505
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
506
|
-
walk(node.child(i), nestingLevel + 1, false, skipH);
|
|
507
|
-
}
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
296
|
+
const visitor = createComplexityVisitor(cRules, hRules, { langId });
|
|
510
297
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
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.
|
|
516
302
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
let halstead = null;
|
|
521
|
-
if (hRules && operators && operands) {
|
|
522
|
-
const n1 = operators.size;
|
|
523
|
-
const n2 = operands.size;
|
|
524
|
-
let bigN1 = 0;
|
|
525
|
-
for (const c of operators.values()) bigN1 += c;
|
|
526
|
-
let bigN2 = 0;
|
|
527
|
-
for (const c of operands.values()) bigN2 += c;
|
|
528
|
-
|
|
529
|
-
const vocabulary = n1 + n2;
|
|
530
|
-
const length = bigN1 + bigN2;
|
|
531
|
-
const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
|
|
532
|
-
const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
|
|
533
|
-
const effort = difficulty * volume;
|
|
534
|
-
const bugs = volume / 3000;
|
|
535
|
-
|
|
536
|
-
halstead = {
|
|
537
|
-
n1,
|
|
538
|
-
n2,
|
|
539
|
-
bigN1,
|
|
540
|
-
bigN2,
|
|
541
|
-
vocabulary,
|
|
542
|
-
length,
|
|
543
|
-
volume: +volume.toFixed(2),
|
|
544
|
-
difficulty: +difficulty.toFixed(2),
|
|
545
|
-
effort: +effort.toFixed(2),
|
|
546
|
-
bugs: +bugs.toFixed(4),
|
|
547
|
-
};
|
|
548
|
-
}
|
|
303
|
+
const results = walkWithVisitors(functionNode, [visitor], langId, {
|
|
304
|
+
nestingNodeTypes: nestingNodes,
|
|
305
|
+
});
|
|
549
306
|
|
|
550
|
-
|
|
551
|
-
const loc = computeLOCMetrics(functionNode, langId);
|
|
307
|
+
const rawResult = results.complexity;
|
|
552
308
|
|
|
553
|
-
//
|
|
554
|
-
|
|
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;
|
|
555
313
|
const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
556
|
-
const mi =
|
|
314
|
+
const mi = _computeMaintainabilityIndex(volume, rawResult.cyclomatic, loc.sloc, commentRatio);
|
|
557
315
|
|
|
558
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
cognitive: rawResult.cognitive,
|
|
318
|
+
cyclomatic: rawResult.cyclomatic,
|
|
319
|
+
maxNesting: rawResult.maxNesting,
|
|
320
|
+
halstead: rawResult.halstead,
|
|
321
|
+
loc,
|
|
322
|
+
mi,
|
|
323
|
+
};
|
|
559
324
|
}
|
|
560
325
|
|
|
561
326
|
// ─── Build-Time: Compute Metrics for Changed Files ────────────────────────
|
|
@@ -612,10 +377,6 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
|
|
|
612
377
|
maintainability_index)
|
|
613
378
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
614
379
|
);
|
|
615
|
-
const getNodeId = db.prepare(
|
|
616
|
-
"SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
|
|
617
|
-
);
|
|
618
|
-
|
|
619
380
|
let analyzed = 0;
|
|
620
381
|
|
|
621
382
|
const tx = db.transaction(() => {
|
|
@@ -662,12 +423,12 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
|
|
|
662
423
|
|
|
663
424
|
// Use pre-computed complexity from native engine if available
|
|
664
425
|
if (def.complexity) {
|
|
665
|
-
const
|
|
666
|
-
if (!
|
|
426
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
427
|
+
if (!nodeId) continue;
|
|
667
428
|
const ch = def.complexity.halstead;
|
|
668
429
|
const cl = def.complexity.loc;
|
|
669
430
|
upsert.run(
|
|
670
|
-
|
|
431
|
+
nodeId,
|
|
671
432
|
def.complexity.cognitive,
|
|
672
433
|
def.complexity.cyclomatic,
|
|
673
434
|
def.complexity.maxNesting ?? 0,
|
|
@@ -700,12 +461,12 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
|
|
|
700
461
|
const metrics = computeAllMetrics(funcNode, langId);
|
|
701
462
|
if (!metrics) continue;
|
|
702
463
|
|
|
703
|
-
const
|
|
704
|
-
if (!
|
|
464
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
465
|
+
if (!nodeId) continue;
|
|
705
466
|
|
|
706
467
|
const h = metrics.halstead;
|
|
707
468
|
upsert.run(
|
|
708
|
-
|
|
469
|
+
nodeId,
|
|
709
470
|
metrics.cognitive,
|
|
710
471
|
metrics.cyclomatic,
|
|
711
472
|
metrics.maxNesting,
|
|
@@ -1040,78 +801,3 @@ export function* iterComplexity(customDbPath, opts = {}) {
|
|
|
1040
801
|
db.close();
|
|
1041
802
|
}
|
|
1042
803
|
}
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* Format complexity output for CLI display.
|
|
1046
|
-
*/
|
|
1047
|
-
export function complexity(customDbPath, opts = {}) {
|
|
1048
|
-
const data = complexityData(customDbPath, opts);
|
|
1049
|
-
|
|
1050
|
-
if (outputResult(data, 'functions', opts)) return;
|
|
1051
|
-
|
|
1052
|
-
if (data.functions.length === 0) {
|
|
1053
|
-
if (data.summary === null) {
|
|
1054
|
-
if (data.hasGraph) {
|
|
1055
|
-
console.log(
|
|
1056
|
-
'\nNo complexity data found, but a graph exists. Run "codegraph build --no-incremental" to populate complexity metrics.\n',
|
|
1057
|
-
);
|
|
1058
|
-
} else {
|
|
1059
|
-
console.log(
|
|
1060
|
-
'\nNo complexity data found. Run "codegraph build" first to analyze your codebase.\n',
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
} else {
|
|
1064
|
-
console.log('\nNo functions match the given filters.\n');
|
|
1065
|
-
}
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
const header = opts.aboveThreshold ? 'Functions Above Threshold' : 'Function Complexity';
|
|
1070
|
-
console.log(`\n# ${header}\n`);
|
|
1071
|
-
|
|
1072
|
-
if (opts.health) {
|
|
1073
|
-
// Health-focused view with Halstead + MI columns
|
|
1074
|
-
console.log(
|
|
1075
|
-
` ${'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)}`,
|
|
1076
|
-
);
|
|
1077
|
-
console.log(
|
|
1078
|
-
` ${'─'.repeat(35)} ${'─'.repeat(25)} ${'─'.repeat(5)} ${'─'.repeat(7)} ${'─'.repeat(6)} ${'─'.repeat(9)} ${'─'.repeat(6)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
|
|
1079
|
-
);
|
|
1080
|
-
|
|
1081
|
-
for (const fn of data.functions) {
|
|
1082
|
-
const name = fn.name.length > 33 ? `${fn.name.slice(0, 32)}…` : fn.name;
|
|
1083
|
-
const file = fn.file.length > 23 ? `…${fn.file.slice(-22)}` : fn.file;
|
|
1084
|
-
const miWarn = fn.exceeds?.includes('maintainabilityIndex') ? '!' : ' ';
|
|
1085
|
-
console.log(
|
|
1086
|
-
` ${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)}`,
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
1089
|
-
} else {
|
|
1090
|
-
// Default view with MI column appended
|
|
1091
|
-
console.log(
|
|
1092
|
-
` ${'Function'.padEnd(40)} ${'File'.padEnd(30)} ${'Cog'.padStart(4)} ${'Cyc'.padStart(4)} ${'Nest'.padStart(5)} ${'MI'.padStart(5)}`,
|
|
1093
|
-
);
|
|
1094
|
-
console.log(
|
|
1095
|
-
` ${'─'.repeat(40)} ${'─'.repeat(30)} ${'─'.repeat(4)} ${'─'.repeat(4)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
|
|
1096
|
-
);
|
|
1097
|
-
|
|
1098
|
-
for (const fn of data.functions) {
|
|
1099
|
-
const name = fn.name.length > 38 ? `${fn.name.slice(0, 37)}…` : fn.name;
|
|
1100
|
-
const file = fn.file.length > 28 ? `…${fn.file.slice(-27)}` : fn.file;
|
|
1101
|
-
const warn = fn.exceeds ? ' !' : '';
|
|
1102
|
-
const mi = fn.maintainabilityIndex > 0 ? String(fn.maintainabilityIndex) : '-';
|
|
1103
|
-
console.log(
|
|
1104
|
-
` ${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}`,
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (data.summary) {
|
|
1110
|
-
const s = data.summary;
|
|
1111
|
-
const miPart = s.avgMI != null ? ` | avg MI: ${s.avgMI}` : '';
|
|
1112
|
-
console.log(
|
|
1113
|
-
`\n ${s.analyzed} functions analyzed | avg cognitive: ${s.avgCognitive} | avg cyclomatic: ${s.avgCyclomatic}${miPart} | ${s.aboveWarn} above threshold`,
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
console.log();
|
|
1117
|
-
}
|
package/src/cycles.js
CHANGED