@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/README.md
CHANGED
|
@@ -562,14 +562,14 @@ Self-measured on every release via CI ([build benchmarks](generated/benchmarks/B
|
|
|
562
562
|
|
|
563
563
|
| Metric | Latest |
|
|
564
564
|
|---|---|
|
|
565
|
-
| Build speed (native) | **6.
|
|
566
|
-
| Build speed (WASM) | **
|
|
565
|
+
| Build speed (native) | **6.1 ms/file** |
|
|
566
|
+
| Build speed (WASM) | **16.5 ms/file** |
|
|
567
567
|
| Query time | **3ms** |
|
|
568
|
-
| No-op rebuild (native) | **
|
|
569
|
-
| 1-file rebuild (native) | **
|
|
568
|
+
| No-op rebuild (native) | **5ms** |
|
|
569
|
+
| 1-file rebuild (native) | **332ms** |
|
|
570
570
|
| Query: fn-deps | **0.8ms** |
|
|
571
571
|
| Query: path | **0.8ms** |
|
|
572
|
-
| ~50,000 files (est.) | **~
|
|
572
|
+
| ~50,000 files (est.) | **~305.0s build** |
|
|
573
573
|
|
|
574
574
|
Metrics are normalized per file for cross-version comparability. Times above are for a full initial build — incremental rebuilds only re-parse changed files.
|
|
575
575
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optave/codegraph",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -71,13 +71,12 @@
|
|
|
71
71
|
},
|
|
72
72
|
"optionalDependencies": {
|
|
73
73
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
74
|
-
"@optave/codegraph-darwin-arm64": "3.1.
|
|
75
|
-
"@optave/codegraph-darwin-x64": "3.1.
|
|
76
|
-
"@optave/codegraph-linux-arm64-gnu": "3.1.
|
|
77
|
-
"@optave/codegraph-linux-
|
|
78
|
-
"@optave/codegraph-linux-x64-
|
|
79
|
-
"@optave/codegraph-
|
|
80
|
-
"@optave/codegraph-win32-x64-msvc": "3.1.0"
|
|
74
|
+
"@optave/codegraph-darwin-arm64": "3.1.2",
|
|
75
|
+
"@optave/codegraph-darwin-x64": "3.1.2",
|
|
76
|
+
"@optave/codegraph-linux-arm64-gnu": "3.1.2",
|
|
77
|
+
"@optave/codegraph-linux-x64-gnu": "3.1.2",
|
|
78
|
+
"@optave/codegraph-linux-x64-musl": "3.1.2",
|
|
79
|
+
"@optave/codegraph-win32-x64-msvc": "3.1.2"
|
|
81
80
|
},
|
|
82
81
|
"devDependencies": {
|
|
83
82
|
"@biomejs/biome": "^2.4.4",
|
|
@@ -90,7 +89,7 @@
|
|
|
90
89
|
"husky": "^9.1",
|
|
91
90
|
"tree-sitter-c-sharp": "^0.23.1",
|
|
92
91
|
"tree-sitter-cli": "^0.26.5",
|
|
93
|
-
"tree-sitter-go": "^0.
|
|
92
|
+
"tree-sitter-go": "^0.25.0",
|
|
94
93
|
"tree-sitter-java": "^0.23.5",
|
|
95
94
|
"tree-sitter-javascript": "^0.25.0",
|
|
96
95
|
"tree-sitter-php": "^0.24.2",
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified AST analysis engine — orchestrates all analysis passes in one file-iteration loop.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the 4 sequential buildXxx calls in builder.js with a single coordinated pass:
|
|
5
|
+
* - AST node extraction (calls, new, string, regex, throw, await)
|
|
6
|
+
* - Complexity metrics (cognitive, cyclomatic, nesting, Halstead, MI)
|
|
7
|
+
* - CFG construction (basic blocks + edges)
|
|
8
|
+
* - Dataflow analysis (define-use chains, arg flows, mutations)
|
|
9
|
+
*
|
|
10
|
+
* All 4 analyses run as visitors in a single DFS walk via walkWithVisitors.
|
|
11
|
+
*
|
|
12
|
+
* Optimization strategy: for files with WASM trees, run all applicable visitors
|
|
13
|
+
* in a single walkWithVisitors call. Store results in the format that buildXxx
|
|
14
|
+
* functions already expect as pre-computed data (same fields as native engine
|
|
15
|
+
* output). This eliminates redundant tree traversals per file.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { performance } from 'node:perf_hooks';
|
|
20
|
+
import { bulkNodeIdsByFile } from '../db.js';
|
|
21
|
+
import { debug } from '../logger.js';
|
|
22
|
+
import { computeLOCMetrics, computeMaintainabilityIndex } from './metrics.js';
|
|
23
|
+
import {
|
|
24
|
+
AST_TYPE_MAPS,
|
|
25
|
+
CFG_RULES,
|
|
26
|
+
COMPLEXITY_RULES,
|
|
27
|
+
DATAFLOW_RULES,
|
|
28
|
+
HALSTEAD_RULES,
|
|
29
|
+
} from './rules/index.js';
|
|
30
|
+
import { buildExtensionSet, buildExtToLangMap } from './shared.js';
|
|
31
|
+
import { walkWithVisitors } from './visitor.js';
|
|
32
|
+
import { functionName as getFuncName } from './visitor-utils.js';
|
|
33
|
+
import { createAstStoreVisitor } from './visitors/ast-store-visitor.js';
|
|
34
|
+
import { createCfgVisitor } from './visitors/cfg-visitor.js';
|
|
35
|
+
import { createComplexityVisitor } from './visitors/complexity-visitor.js';
|
|
36
|
+
import { createDataflowVisitor } from './visitors/dataflow-visitor.js';
|
|
37
|
+
|
|
38
|
+
// ─── Extension sets for quick language-support checks ────────────────────
|
|
39
|
+
|
|
40
|
+
const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
|
|
41
|
+
const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
|
|
42
|
+
const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
|
|
43
|
+
|
|
44
|
+
// ─── Lazy imports (heavy modules loaded only when needed) ────────────────
|
|
45
|
+
|
|
46
|
+
let _parserModule = null;
|
|
47
|
+
async function getParserModule() {
|
|
48
|
+
if (!_parserModule) _parserModule = await import('../parser.js');
|
|
49
|
+
return _parserModule;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run all enabled AST analyses in a coordinated pass.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} db - open better-sqlite3 database (read-write)
|
|
58
|
+
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, calls, _tree, _langId, ... }>
|
|
59
|
+
* @param {string} rootDir - absolute project root path
|
|
60
|
+
* @param {object} opts - build options (ast, complexity, cfg, dataflow toggles)
|
|
61
|
+
* @param {object} [engineOpts] - engine options
|
|
62
|
+
* @returns {Promise<{ astMs: number, complexityMs: number, cfgMs: number, dataflowMs: number }>}
|
|
63
|
+
*/
|
|
64
|
+
export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
65
|
+
const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
66
|
+
|
|
67
|
+
const doAst = opts.ast !== false;
|
|
68
|
+
const doComplexity = opts.complexity !== false;
|
|
69
|
+
const doCfg = opts.cfg !== false;
|
|
70
|
+
const doDataflow = opts.dataflow !== false;
|
|
71
|
+
|
|
72
|
+
if (!doAst && !doComplexity && !doCfg && !doDataflow) return timing;
|
|
73
|
+
|
|
74
|
+
const extToLang = buildExtToLangMap();
|
|
75
|
+
|
|
76
|
+
// ── WASM pre-parse for files that need it ───────────────────────────
|
|
77
|
+
// CFG now runs as a visitor in the unified walk, so only dataflow
|
|
78
|
+
// triggers WASM pre-parse when no tree exists.
|
|
79
|
+
if (doDataflow) {
|
|
80
|
+
let needsWasmTrees = false;
|
|
81
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
82
|
+
if (symbols._tree) continue;
|
|
83
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
84
|
+
|
|
85
|
+
if (!symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext)) {
|
|
86
|
+
needsWasmTrees = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (needsWasmTrees) {
|
|
92
|
+
try {
|
|
93
|
+
const { ensureWasmTrees } = await getParserModule();
|
|
94
|
+
await ensureWasmTrees(fileSymbols, rootDir);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
debug(`ensureWasmTrees failed: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Phase 7 Optimization: Unified pre-walk ─────────────────────────
|
|
102
|
+
// For files with WASM trees, run all applicable visitors in a SINGLE
|
|
103
|
+
// walkWithVisitors call. Store results in the format that buildXxx
|
|
104
|
+
// functions already expect as pre-computed data (same fields as native
|
|
105
|
+
// engine output). This eliminates ~3 redundant tree traversals per file.
|
|
106
|
+
const t0walk = performance.now();
|
|
107
|
+
|
|
108
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
109
|
+
if (!symbols._tree) continue; // No WASM tree — native path handles it
|
|
110
|
+
|
|
111
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
112
|
+
const langId = symbols._langId || extToLang.get(ext);
|
|
113
|
+
if (!langId) continue;
|
|
114
|
+
|
|
115
|
+
const defs = symbols.definitions || [];
|
|
116
|
+
const visitors = [];
|
|
117
|
+
const walkerOpts = {
|
|
118
|
+
functionNodeTypes: new Set(),
|
|
119
|
+
nestingNodeTypes: new Set(),
|
|
120
|
+
getFunctionName: (_node) => null,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ─ AST-store visitor ─
|
|
124
|
+
const astTypeMap = AST_TYPE_MAPS.get(langId);
|
|
125
|
+
let astVisitor = null;
|
|
126
|
+
if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !symbols.astNodes?.length) {
|
|
127
|
+
const nodeIdMap = new Map();
|
|
128
|
+
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
129
|
+
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
130
|
+
}
|
|
131
|
+
astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
|
|
132
|
+
visitors.push(astVisitor);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─ Complexity visitor (file-level mode) ─
|
|
136
|
+
const cRules = COMPLEXITY_RULES.get(langId);
|
|
137
|
+
const hRules = HALSTEAD_RULES.get(langId);
|
|
138
|
+
let complexityVisitor = null;
|
|
139
|
+
if (doComplexity && cRules) {
|
|
140
|
+
// Only use visitor if some functions lack pre-computed complexity
|
|
141
|
+
const needsWasmComplexity = defs.some(
|
|
142
|
+
(d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity,
|
|
143
|
+
);
|
|
144
|
+
if (needsWasmComplexity) {
|
|
145
|
+
complexityVisitor = createComplexityVisitor(cRules, hRules, {
|
|
146
|
+
fileLevelWalk: true,
|
|
147
|
+
langId,
|
|
148
|
+
});
|
|
149
|
+
visitors.push(complexityVisitor);
|
|
150
|
+
|
|
151
|
+
// Merge nesting nodes for complexity tracking
|
|
152
|
+
// NOTE: do NOT add functionNodes here — funcDepth in the complexity
|
|
153
|
+
// visitor already tracks function-level nesting. Adding them to
|
|
154
|
+
// nestingNodeTypes would inflate context.nestingLevel by +1 inside
|
|
155
|
+
// every function body, double-counting in cognitive += 1 + nestingLevel.
|
|
156
|
+
for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes.add(t);
|
|
157
|
+
|
|
158
|
+
// Provide getFunctionName for complexity visitor
|
|
159
|
+
const dfRules = DATAFLOW_RULES.get(langId);
|
|
160
|
+
walkerOpts.getFunctionName = (node) => {
|
|
161
|
+
// Try complexity rules' function name field first
|
|
162
|
+
const nameNode = node.childForFieldName('name');
|
|
163
|
+
if (nameNode) return nameNode.text;
|
|
164
|
+
// Fall back to dataflow rules' richer name extraction
|
|
165
|
+
if (dfRules) return getFuncName(node, dfRules);
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─ CFG visitor ─
|
|
172
|
+
const cfgRulesForLang = CFG_RULES.get(langId);
|
|
173
|
+
let cfgVisitor = null;
|
|
174
|
+
if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
|
|
175
|
+
// Only use visitor if some functions lack pre-computed CFG
|
|
176
|
+
const needsWasmCfg = defs.some(
|
|
177
|
+
(d) =>
|
|
178
|
+
(d.kind === 'function' || d.kind === 'method') &&
|
|
179
|
+
d.line &&
|
|
180
|
+
d.cfg !== null &&
|
|
181
|
+
!Array.isArray(d.cfg?.blocks),
|
|
182
|
+
);
|
|
183
|
+
if (needsWasmCfg) {
|
|
184
|
+
cfgVisitor = createCfgVisitor(cfgRulesForLang);
|
|
185
|
+
visitors.push(cfgVisitor);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─ Dataflow visitor ─
|
|
190
|
+
const dfRules = DATAFLOW_RULES.get(langId);
|
|
191
|
+
let dataflowVisitor = null;
|
|
192
|
+
if (doDataflow && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
|
|
193
|
+
dataflowVisitor = createDataflowVisitor(dfRules);
|
|
194
|
+
visitors.push(dataflowVisitor);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─ Run unified walk if we have visitors ─
|
|
198
|
+
if (visitors.length === 0) continue;
|
|
199
|
+
|
|
200
|
+
const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
|
|
201
|
+
|
|
202
|
+
// ─ Store AST results (buildAstNodes will find symbols.astNodes and skip its walk) ─
|
|
203
|
+
if (astVisitor) {
|
|
204
|
+
const astRows = results['ast-store'] || [];
|
|
205
|
+
if (astRows.length > 0) {
|
|
206
|
+
// Store in the format buildAstNodes expects for the native path
|
|
207
|
+
symbols.astNodes = astRows;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─ Store complexity results on definitions (buildComplexityMetrics will find def.complexity) ─
|
|
212
|
+
if (complexityVisitor) {
|
|
213
|
+
const complexityResults = results.complexity || [];
|
|
214
|
+
// Match results back to definitions by function start line
|
|
215
|
+
// Store the full result (metrics + funcNode) for O(1) lookup
|
|
216
|
+
const resultByLine = new Map();
|
|
217
|
+
for (const r of complexityResults) {
|
|
218
|
+
if (r.funcNode) {
|
|
219
|
+
const line = r.funcNode.startPosition.row + 1;
|
|
220
|
+
if (!resultByLine.has(line)) resultByLine.set(line, []);
|
|
221
|
+
resultByLine.get(line).push(r);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const def of defs) {
|
|
225
|
+
if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
|
|
226
|
+
const candidates = resultByLine.get(def.line);
|
|
227
|
+
const funcResult = !candidates
|
|
228
|
+
? undefined
|
|
229
|
+
: candidates.length === 1
|
|
230
|
+
? candidates[0]
|
|
231
|
+
: (candidates.find((r) => {
|
|
232
|
+
const n = r.funcNode.childForFieldName('name');
|
|
233
|
+
return n && n.text === def.name;
|
|
234
|
+
}) ?? candidates[0]);
|
|
235
|
+
if (funcResult) {
|
|
236
|
+
const { metrics } = funcResult;
|
|
237
|
+
const loc = computeLOCMetrics(funcResult.funcNode, langId);
|
|
238
|
+
const volume = metrics.halstead ? metrics.halstead.volume : 0;
|
|
239
|
+
const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
240
|
+
const mi = computeMaintainabilityIndex(
|
|
241
|
+
volume,
|
|
242
|
+
metrics.cyclomatic,
|
|
243
|
+
loc.sloc,
|
|
244
|
+
commentRatio,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
def.complexity = {
|
|
248
|
+
cognitive: metrics.cognitive,
|
|
249
|
+
cyclomatic: metrics.cyclomatic,
|
|
250
|
+
maxNesting: metrics.maxNesting,
|
|
251
|
+
halstead: metrics.halstead,
|
|
252
|
+
loc,
|
|
253
|
+
maintainabilityIndex: mi,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─ Store CFG results on definitions (buildCFGData will find def.cfg and skip its walk) ─
|
|
261
|
+
if (cfgVisitor) {
|
|
262
|
+
const cfgResults = results.cfg || [];
|
|
263
|
+
const cfgByLine = new Map();
|
|
264
|
+
for (const r of cfgResults) {
|
|
265
|
+
if (r.funcNode) {
|
|
266
|
+
const line = r.funcNode.startPosition.row + 1;
|
|
267
|
+
if (!cfgByLine.has(line)) cfgByLine.set(line, []);
|
|
268
|
+
cfgByLine.get(line).push(r);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const def of defs) {
|
|
272
|
+
if (
|
|
273
|
+
(def.kind === 'function' || def.kind === 'method') &&
|
|
274
|
+
def.line &&
|
|
275
|
+
!def.cfg?.blocks?.length
|
|
276
|
+
) {
|
|
277
|
+
const candidates = cfgByLine.get(def.line);
|
|
278
|
+
const cfgResult = !candidates
|
|
279
|
+
? undefined
|
|
280
|
+
: candidates.length === 1
|
|
281
|
+
? candidates[0]
|
|
282
|
+
: (candidates.find((r) => {
|
|
283
|
+
const n = r.funcNode.childForFieldName('name');
|
|
284
|
+
return n && n.text === def.name;
|
|
285
|
+
}) ?? candidates[0]);
|
|
286
|
+
if (cfgResult) {
|
|
287
|
+
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
|
|
288
|
+
|
|
289
|
+
// Override complexity's cyclomatic with CFG-derived value (single source of truth)
|
|
290
|
+
// and recompute maintainability index to stay consistent
|
|
291
|
+
if (def.complexity && cfgResult.cyclomatic != null) {
|
|
292
|
+
def.complexity.cyclomatic = cfgResult.cyclomatic;
|
|
293
|
+
const { loc, halstead } = def.complexity;
|
|
294
|
+
const volume = halstead ? halstead.volume : 0;
|
|
295
|
+
const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
296
|
+
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
|
|
297
|
+
volume,
|
|
298
|
+
cfgResult.cyclomatic,
|
|
299
|
+
loc?.sloc ?? 0,
|
|
300
|
+
commentRatio,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ─ Store dataflow results (buildDataflowEdges will find symbols.dataflow and skip its walk) ─
|
|
309
|
+
if (dataflowVisitor) {
|
|
310
|
+
symbols.dataflow = results.dataflow;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
timing._unifiedWalkMs = performance.now() - t0walk;
|
|
315
|
+
|
|
316
|
+
// ── Delegate to buildXxx functions ─────────────────────────────────
|
|
317
|
+
// Each function finds pre-computed data from the unified walk above
|
|
318
|
+
// (or from the native engine) and only does DB writes + native fallback.
|
|
319
|
+
|
|
320
|
+
if (doAst) {
|
|
321
|
+
const t0 = performance.now();
|
|
322
|
+
try {
|
|
323
|
+
const { buildAstNodes } = await import('../ast.js');
|
|
324
|
+
await buildAstNodes(db, fileSymbols, rootDir, engineOpts);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
debug(`buildAstNodes failed: ${err.message}`);
|
|
327
|
+
}
|
|
328
|
+
timing.astMs = performance.now() - t0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (doComplexity) {
|
|
332
|
+
const t0 = performance.now();
|
|
333
|
+
try {
|
|
334
|
+
const { buildComplexityMetrics } = await import('../complexity.js');
|
|
335
|
+
await buildComplexityMetrics(db, fileSymbols, rootDir, engineOpts);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
debug(`buildComplexityMetrics failed: ${err.message}`);
|
|
338
|
+
}
|
|
339
|
+
timing.complexityMs = performance.now() - t0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (doCfg) {
|
|
343
|
+
const t0 = performance.now();
|
|
344
|
+
try {
|
|
345
|
+
const { buildCFGData } = await import('../cfg.js');
|
|
346
|
+
await buildCFGData(db, fileSymbols, rootDir, engineOpts);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
debug(`buildCFGData failed: ${err.message}`);
|
|
349
|
+
}
|
|
350
|
+
timing.cfgMs = performance.now() - t0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (doDataflow) {
|
|
354
|
+
const t0 = performance.now();
|
|
355
|
+
try {
|
|
356
|
+
const { buildDataflowEdges } = await import('../dataflow.js');
|
|
357
|
+
await buildDataflowEdges(db, fileSymbols, rootDir, engineOpts);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
debug(`buildDataflowEdges failed: ${err.message}`);
|
|
360
|
+
}
|
|
361
|
+
timing.dataflowMs = performance.now() - t0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return timing;
|
|
365
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure metric computations extracted from complexity.js.
|
|
3
|
+
*
|
|
4
|
+
* Contains Halstead derived metrics, LOC metrics, and Maintainability Index —
|
|
5
|
+
* all stateless math that can be reused by visitor-based and standalone paths.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── Halstead Derived Metrics ─────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute Halstead derived metrics from raw operator/operand counts.
|
|
12
|
+
*
|
|
13
|
+
* @param {Map<string, number>} operators - operator type/text → count
|
|
14
|
+
* @param {Map<string, number>} operands - operand text → count
|
|
15
|
+
* @returns {{ n1: number, n2: number, bigN1: number, bigN2: number, vocabulary: number, length: number, volume: number, difficulty: number, effort: number, bugs: number }}
|
|
16
|
+
*/
|
|
17
|
+
export function computeHalsteadDerived(operators, operands) {
|
|
18
|
+
const n1 = operators.size;
|
|
19
|
+
const n2 = operands.size;
|
|
20
|
+
let bigN1 = 0;
|
|
21
|
+
for (const c of operators.values()) bigN1 += c;
|
|
22
|
+
let bigN2 = 0;
|
|
23
|
+
for (const c of operands.values()) bigN2 += c;
|
|
24
|
+
|
|
25
|
+
const vocabulary = n1 + n2;
|
|
26
|
+
const length = bigN1 + bigN2;
|
|
27
|
+
const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
|
|
28
|
+
const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
|
|
29
|
+
const effort = difficulty * volume;
|
|
30
|
+
const bugs = volume / 3000;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
n1,
|
|
34
|
+
n2,
|
|
35
|
+
bigN1,
|
|
36
|
+
bigN2,
|
|
37
|
+
vocabulary,
|
|
38
|
+
length,
|
|
39
|
+
volume: +volume.toFixed(2),
|
|
40
|
+
difficulty: +difficulty.toFixed(2),
|
|
41
|
+
effort: +effort.toFixed(2),
|
|
42
|
+
bugs: +bugs.toFixed(4),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── LOC Metrics ──────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const C_STYLE_PREFIXES = ['//', '/*', '*', '*/'];
|
|
49
|
+
|
|
50
|
+
const COMMENT_PREFIXES = new Map([
|
|
51
|
+
['javascript', C_STYLE_PREFIXES],
|
|
52
|
+
['typescript', C_STYLE_PREFIXES],
|
|
53
|
+
['tsx', C_STYLE_PREFIXES],
|
|
54
|
+
['go', C_STYLE_PREFIXES],
|
|
55
|
+
['rust', C_STYLE_PREFIXES],
|
|
56
|
+
['java', C_STYLE_PREFIXES],
|
|
57
|
+
['csharp', C_STYLE_PREFIXES],
|
|
58
|
+
['python', ['#']],
|
|
59
|
+
['ruby', ['#']],
|
|
60
|
+
['php', ['//', '#', '/*', '*', '*/']],
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compute LOC metrics from a function node's source text.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} functionNode - tree-sitter node
|
|
67
|
+
* @param {string} [language] - Language ID (falls back to C-style prefixes)
|
|
68
|
+
* @returns {{ loc: number, sloc: number, commentLines: number }}
|
|
69
|
+
*/
|
|
70
|
+
export function computeLOCMetrics(functionNode, language) {
|
|
71
|
+
const text = functionNode.text;
|
|
72
|
+
const lines = text.split('\n');
|
|
73
|
+
const loc = lines.length;
|
|
74
|
+
const prefixes = (language && COMMENT_PREFIXES.get(language)) || C_STYLE_PREFIXES;
|
|
75
|
+
|
|
76
|
+
let commentLines = 0;
|
|
77
|
+
let blankLines = 0;
|
|
78
|
+
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
if (trimmed === '') {
|
|
82
|
+
blankLines++;
|
|
83
|
+
} else if (prefixes.some((p) => trimmed.startsWith(p))) {
|
|
84
|
+
commentLines++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const sloc = Math.max(1, loc - blankLines - commentLines);
|
|
89
|
+
return { loc, sloc, commentLines };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Maintainability Index ────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compute normalized Maintainability Index (0-100 scale).
|
|
96
|
+
*
|
|
97
|
+
* Original SEI formula: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
|
|
98
|
+
* Microsoft normalization: max(0, min(100, MI * 100/171))
|
|
99
|
+
*
|
|
100
|
+
* @param {number} volume - Halstead volume
|
|
101
|
+
* @param {number} cyclomatic - Cyclomatic complexity
|
|
102
|
+
* @param {number} sloc - Source lines of code
|
|
103
|
+
* @param {number} [commentRatio] - Comment ratio (0-1), optional
|
|
104
|
+
* @returns {number} Normalized MI (0-100)
|
|
105
|
+
*/
|
|
106
|
+
export function computeMaintainabilityIndex(volume, cyclomatic, sloc, commentRatio) {
|
|
107
|
+
const safeVolume = Math.max(volume, 1);
|
|
108
|
+
const safeSLOC = Math.max(sloc, 1);
|
|
109
|
+
|
|
110
|
+
let mi = 171 - 5.2 * Math.log(safeVolume) - 0.23 * cyclomatic - 16.2 * Math.log(safeSLOC);
|
|
111
|
+
|
|
112
|
+
if (commentRatio != null && commentRatio > 0) {
|
|
113
|
+
mi += 50 * Math.sin(Math.sqrt(2.4 * commentRatio));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const normalized = Math.max(0, Math.min(100, (mi * 100) / 171));
|
|
117
|
+
return +normalized.toFixed(1);
|
|
118
|
+
}
|