@optave/codegraph 3.1.5 → 3.2.0
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 +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- package/src/shared/kinds.js +1 -1
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ That's it. The graph is ready. Now connect your AI agent.
|
|
|
141
141
|
Connect directly via MCP — your agent gets 30 tools to query the graph:
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
|
-
codegraph mcp #
|
|
144
|
+
codegraph mcp # 33-tool MCP server — AI queries the graph directly
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
Or add codegraph to your agent's instructions (e.g. `CLAUDE.md`):
|
|
@@ -183,7 +183,7 @@ cd codegraph && npm install && npm link
|
|
|
183
183
|
|
|
184
184
|
| | Feature | Description |
|
|
185
185
|
|---|---|---|
|
|
186
|
-
| 🤖 | **MCP server** |
|
|
186
|
+
| 🤖 | **MCP server** | 33-tool MCP server for AI assistants; single-repo by default, opt-in multi-repo |
|
|
187
187
|
| 🎯 | **Deep context** | `context` gives agents source, deps, callers, signature, and tests for a function in one call; `audit --quick` gives structural summaries |
|
|
188
188
|
| 🏷️ | **Node role classification** | Every symbol auto-tagged as `entry`/`core`/`utility`/`adapter`/`dead`/`leaf` based on connectivity — agents instantly know architectural role |
|
|
189
189
|
| 📦 | **Batch querying** | Accept a list of targets and return all results in one JSON payload — enables multi-agent parallel dispatch |
|
|
@@ -258,6 +258,7 @@ codegraph children <name> # List parameters, properties, constants of a
|
|
|
258
258
|
```bash
|
|
259
259
|
codegraph context <name> # Full context: source, deps, callers, signature, tests
|
|
260
260
|
codegraph context <name> --depth 2 --no-tests # Include callee source 2 levels deep
|
|
261
|
+
codegraph brief <file> # Token-efficient file summary: symbols, roles, risk tiers
|
|
261
262
|
codegraph audit <file> --quick # Structural summary: public API, internals, data flow
|
|
262
263
|
codegraph audit <function> --quick # Function summary: signature, calls, callers, tests
|
|
263
264
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optave/codegraph",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
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",
|
|
@@ -76,12 +76,12 @@
|
|
|
76
76
|
},
|
|
77
77
|
"optionalDependencies": {
|
|
78
78
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
79
|
-
"@optave/codegraph-darwin-arm64": "3.
|
|
80
|
-
"@optave/codegraph-darwin-x64": "3.
|
|
81
|
-
"@optave/codegraph-linux-arm64-gnu": "3.
|
|
82
|
-
"@optave/codegraph-linux-x64-gnu": "3.
|
|
83
|
-
"@optave/codegraph-linux-x64-musl": "3.
|
|
84
|
-
"@optave/codegraph-win32-x64-msvc": "3.
|
|
79
|
+
"@optave/codegraph-darwin-arm64": "3.2.0",
|
|
80
|
+
"@optave/codegraph-darwin-x64": "3.2.0",
|
|
81
|
+
"@optave/codegraph-linux-arm64-gnu": "3.2.0",
|
|
82
|
+
"@optave/codegraph-linux-x64-gnu": "3.2.0",
|
|
83
|
+
"@optave/codegraph-linux-x64-musl": "3.2.0",
|
|
84
|
+
"@optave/codegraph-win32-x64-msvc": "3.2.0"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@biomejs/biome": "^2.4.4",
|
|
@@ -50,294 +50,227 @@ async function getParserModule() {
|
|
|
50
50
|
return _parserModule;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// ───
|
|
53
|
+
// ─── WASM pre-parse ─────────────────────────────────────────────────────
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
* Run all enabled AST analyses in a coordinated pass.
|
|
57
|
-
*
|
|
58
|
-
* @param {object} db - open better-sqlite3 database (read-write)
|
|
59
|
-
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, calls, _tree, _langId, ... }>
|
|
60
|
-
* @param {string} rootDir - absolute project root path
|
|
61
|
-
* @param {object} opts - build options (ast, complexity, cfg, dataflow toggles)
|
|
62
|
-
* @param {object} [engineOpts] - engine options
|
|
63
|
-
* @returns {Promise<{ astMs: number, complexityMs: number, cfgMs: number, dataflowMs: number }>}
|
|
64
|
-
*/
|
|
65
|
-
export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
66
|
-
const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
67
|
-
|
|
68
|
-
const doAst = opts.ast !== false;
|
|
55
|
+
async function ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir) {
|
|
69
56
|
const doComplexity = opts.complexity !== false;
|
|
70
57
|
const doCfg = opts.cfg !== false;
|
|
71
58
|
const doDataflow = opts.dataflow !== false;
|
|
72
59
|
|
|
73
|
-
if (!
|
|
74
|
-
|
|
75
|
-
const extToLang = buildExtToLangMap();
|
|
76
|
-
|
|
77
|
-
// ── WASM pre-parse for files that need it ───────────────────────────
|
|
78
|
-
// The native engine only handles parsing (symbols, calls, imports).
|
|
79
|
-
// Complexity, CFG, and dataflow all require a WASM tree-sitter tree
|
|
80
|
-
// for their visitor walks. Without this, incremental rebuilds on the
|
|
81
|
-
// native engine silently lose these analyses for changed files (#468).
|
|
82
|
-
if (doComplexity || doCfg || doDataflow) {
|
|
83
|
-
let needsWasmTrees = false;
|
|
84
|
-
for (const [relPath, symbols] of fileSymbols) {
|
|
85
|
-
if (symbols._tree) continue;
|
|
86
|
-
const ext = path.extname(relPath).toLowerCase();
|
|
87
|
-
const defs = symbols.definitions || [];
|
|
88
|
-
|
|
89
|
-
const needsComplexity =
|
|
90
|
-
doComplexity &&
|
|
91
|
-
COMPLEXITY_EXTENSIONS.has(ext) &&
|
|
92
|
-
defs.some((d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity);
|
|
93
|
-
const needsCfg =
|
|
94
|
-
doCfg &&
|
|
95
|
-
CFG_EXTENSIONS.has(ext) &&
|
|
96
|
-
defs.some(
|
|
97
|
-
(d) =>
|
|
98
|
-
(d.kind === 'function' || d.kind === 'method') &&
|
|
99
|
-
d.line &&
|
|
100
|
-
d.cfg !== null &&
|
|
101
|
-
!Array.isArray(d.cfg?.blocks),
|
|
102
|
-
);
|
|
103
|
-
const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
|
|
104
|
-
|
|
105
|
-
if (needsComplexity || needsCfg || needsDataflow) {
|
|
106
|
-
needsWasmTrees = true;
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (needsWasmTrees) {
|
|
112
|
-
try {
|
|
113
|
-
const { ensureWasmTrees } = await getParserModule();
|
|
114
|
-
await ensureWasmTrees(fileSymbols, rootDir);
|
|
115
|
-
} catch (err) {
|
|
116
|
-
debug(`ensureWasmTrees failed: ${err.message}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── Phase 7 Optimization: Unified pre-walk ─────────────────────────
|
|
122
|
-
// For files with WASM trees, run all applicable visitors in a SINGLE
|
|
123
|
-
// walkWithVisitors call. Store results in the format that buildXxx
|
|
124
|
-
// functions already expect as pre-computed data (same fields as native
|
|
125
|
-
// engine output). This eliminates ~3 redundant tree traversals per file.
|
|
126
|
-
const t0walk = performance.now();
|
|
60
|
+
if (!doComplexity && !doCfg && !doDataflow) return;
|
|
127
61
|
|
|
62
|
+
let needsWasmTrees = false;
|
|
128
63
|
for (const [relPath, symbols] of fileSymbols) {
|
|
129
|
-
if (
|
|
130
|
-
|
|
64
|
+
if (symbols._tree) continue;
|
|
131
65
|
const ext = path.extname(relPath).toLowerCase();
|
|
132
|
-
const langId = symbols._langId || extToLang.get(ext);
|
|
133
|
-
if (!langId) continue;
|
|
134
|
-
|
|
135
66
|
const defs = symbols.definitions || [];
|
|
136
|
-
const visitors = [];
|
|
137
|
-
const walkerOpts = {
|
|
138
|
-
functionNodeTypes: new Set(),
|
|
139
|
-
nestingNodeTypes: new Set(),
|
|
140
|
-
getFunctionName: (_node) => null,
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
// ─ AST-store visitor ─
|
|
144
|
-
const astTypeMap = AST_TYPE_MAPS.get(langId);
|
|
145
|
-
let astVisitor = null;
|
|
146
|
-
if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !symbols.astNodes?.length) {
|
|
147
|
-
const nodeIdMap = new Map();
|
|
148
|
-
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
149
|
-
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
150
|
-
}
|
|
151
|
-
astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
|
|
152
|
-
visitors.push(astVisitor);
|
|
153
|
-
}
|
|
154
67
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
164
|
-
if (needsWasmComplexity) {
|
|
165
|
-
complexityVisitor = createComplexityVisitor(cRules, hRules, {
|
|
166
|
-
fileLevelWalk: true,
|
|
167
|
-
langId,
|
|
168
|
-
});
|
|
169
|
-
visitors.push(complexityVisitor);
|
|
170
|
-
|
|
171
|
-
// Merge nesting nodes for complexity tracking
|
|
172
|
-
// NOTE: do NOT add functionNodes here — funcDepth in the complexity
|
|
173
|
-
// visitor already tracks function-level nesting. Adding them to
|
|
174
|
-
// nestingNodeTypes would inflate context.nestingLevel by +1 inside
|
|
175
|
-
// every function body, double-counting in cognitive += 1 + nestingLevel.
|
|
176
|
-
for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes.add(t);
|
|
177
|
-
|
|
178
|
-
// Provide getFunctionName for complexity visitor
|
|
179
|
-
const dfRules = DATAFLOW_RULES.get(langId);
|
|
180
|
-
walkerOpts.getFunctionName = (node) => {
|
|
181
|
-
// Try complexity rules' function name field first
|
|
182
|
-
const nameNode = node.childForFieldName('name');
|
|
183
|
-
if (nameNode) return nameNode.text;
|
|
184
|
-
// Fall back to dataflow rules' richer name extraction
|
|
185
|
-
if (dfRules) return getFuncName(node, dfRules);
|
|
186
|
-
return null;
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ─ CFG visitor ─
|
|
192
|
-
const cfgRulesForLang = CFG_RULES.get(langId);
|
|
193
|
-
let cfgVisitor = null;
|
|
194
|
-
if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
|
|
195
|
-
// Only use visitor if some functions lack pre-computed CFG
|
|
196
|
-
const needsWasmCfg = defs.some(
|
|
68
|
+
const needsComplexity =
|
|
69
|
+
doComplexity &&
|
|
70
|
+
COMPLEXITY_EXTENSIONS.has(ext) &&
|
|
71
|
+
defs.some((d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity);
|
|
72
|
+
const needsCfg =
|
|
73
|
+
doCfg &&
|
|
74
|
+
CFG_EXTENSIONS.has(ext) &&
|
|
75
|
+
defs.some(
|
|
197
76
|
(d) =>
|
|
198
77
|
(d.kind === 'function' || d.kind === 'method') &&
|
|
199
78
|
d.line &&
|
|
200
79
|
d.cfg !== null &&
|
|
201
80
|
!Array.isArray(d.cfg?.blocks),
|
|
202
81
|
);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
82
|
+
const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
|
|
83
|
+
|
|
84
|
+
if (needsComplexity || needsCfg || needsDataflow) {
|
|
85
|
+
needsWasmTrees = true;
|
|
86
|
+
break;
|
|
207
87
|
}
|
|
88
|
+
}
|
|
208
89
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
90
|
+
if (needsWasmTrees) {
|
|
91
|
+
try {
|
|
92
|
+
const { ensureWasmTrees } = await getParserModule();
|
|
93
|
+
await ensureWasmTrees(fileSymbols, rootDir);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
debug(`ensureWasmTrees failed: ${err.message}`);
|
|
215
96
|
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
216
99
|
|
|
217
|
-
|
|
218
|
-
if (visitors.length === 0) continue;
|
|
100
|
+
// ─── Per-file visitor setup ─────────────────────────────────────────────
|
|
219
101
|
|
|
220
|
-
|
|
102
|
+
function setupVisitors(db, relPath, symbols, langId, opts) {
|
|
103
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
104
|
+
const defs = symbols.definitions || [];
|
|
105
|
+
const doAst = opts.ast !== false;
|
|
106
|
+
const doComplexity = opts.complexity !== false;
|
|
107
|
+
const doCfg = opts.cfg !== false;
|
|
108
|
+
const doDataflow = opts.dataflow !== false;
|
|
221
109
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
110
|
+
const visitors = [];
|
|
111
|
+
const walkerOpts = {
|
|
112
|
+
functionNodeTypes: new Set(),
|
|
113
|
+
nestingNodeTypes: new Set(),
|
|
114
|
+
getFunctionName: (_node) => null,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// AST-store visitor
|
|
118
|
+
let astVisitor = null;
|
|
119
|
+
const astTypeMap = AST_TYPE_MAPS.get(langId);
|
|
120
|
+
if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !symbols.astNodes?.length) {
|
|
121
|
+
const nodeIdMap = new Map();
|
|
122
|
+
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
123
|
+
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
229
124
|
}
|
|
125
|
+
astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
|
|
126
|
+
visitors.push(astVisitor);
|
|
127
|
+
}
|
|
230
128
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const n = r.funcNode.childForFieldName('name');
|
|
253
|
-
return n && n.text === def.name;
|
|
254
|
-
}) ?? candidates[0]);
|
|
255
|
-
if (funcResult) {
|
|
256
|
-
const { metrics } = funcResult;
|
|
257
|
-
const loc = computeLOCMetrics(funcResult.funcNode, langId);
|
|
258
|
-
const volume = metrics.halstead ? metrics.halstead.volume : 0;
|
|
259
|
-
const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
260
|
-
const mi = computeMaintainabilityIndex(
|
|
261
|
-
volume,
|
|
262
|
-
metrics.cyclomatic,
|
|
263
|
-
loc.sloc,
|
|
264
|
-
commentRatio,
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
def.complexity = {
|
|
268
|
-
cognitive: metrics.cognitive,
|
|
269
|
-
cyclomatic: metrics.cyclomatic,
|
|
270
|
-
maxNesting: metrics.maxNesting,
|
|
271
|
-
halstead: metrics.halstead,
|
|
272
|
-
loc,
|
|
273
|
-
maintainabilityIndex: mi,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
129
|
+
// Complexity visitor (file-level mode)
|
|
130
|
+
let complexityVisitor = null;
|
|
131
|
+
const cRules = COMPLEXITY_RULES.get(langId);
|
|
132
|
+
const hRules = HALSTEAD_RULES.get(langId);
|
|
133
|
+
if (doComplexity && cRules) {
|
|
134
|
+
const needsWasmComplexity = defs.some(
|
|
135
|
+
(d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity,
|
|
136
|
+
);
|
|
137
|
+
if (needsWasmComplexity) {
|
|
138
|
+
complexityVisitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
|
|
139
|
+
visitors.push(complexityVisitor);
|
|
140
|
+
|
|
141
|
+
for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes.add(t);
|
|
142
|
+
|
|
143
|
+
const dfRules = DATAFLOW_RULES.get(langId);
|
|
144
|
+
walkerOpts.getFunctionName = (node) => {
|
|
145
|
+
const nameNode = node.childForFieldName('name');
|
|
146
|
+
if (nameNode) return nameNode.text;
|
|
147
|
+
if (dfRules) return getFuncName(node, dfRules);
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
278
150
|
}
|
|
151
|
+
}
|
|
279
152
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def.line &&
|
|
295
|
-
!def.cfg?.blocks?.length
|
|
296
|
-
) {
|
|
297
|
-
const candidates = cfgByLine.get(def.line);
|
|
298
|
-
const cfgResult = !candidates
|
|
299
|
-
? undefined
|
|
300
|
-
: candidates.length === 1
|
|
301
|
-
? candidates[0]
|
|
302
|
-
: (candidates.find((r) => {
|
|
303
|
-
const n = r.funcNode.childForFieldName('name');
|
|
304
|
-
return n && n.text === def.name;
|
|
305
|
-
}) ?? candidates[0]);
|
|
306
|
-
if (cfgResult) {
|
|
307
|
-
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
|
|
308
|
-
|
|
309
|
-
// Override complexity's cyclomatic with CFG-derived value (single source of truth)
|
|
310
|
-
// and recompute maintainability index to stay consistent
|
|
311
|
-
if (def.complexity && cfgResult.cyclomatic != null) {
|
|
312
|
-
def.complexity.cyclomatic = cfgResult.cyclomatic;
|
|
313
|
-
const { loc, halstead } = def.complexity;
|
|
314
|
-
const volume = halstead ? halstead.volume : 0;
|
|
315
|
-
const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
316
|
-
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
|
|
317
|
-
volume,
|
|
318
|
-
cfgResult.cyclomatic,
|
|
319
|
-
loc?.sloc ?? 0,
|
|
320
|
-
commentRatio,
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
153
|
+
// CFG visitor
|
|
154
|
+
let cfgVisitor = null;
|
|
155
|
+
const cfgRulesForLang = CFG_RULES.get(langId);
|
|
156
|
+
if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
|
|
157
|
+
const needsWasmCfg = defs.some(
|
|
158
|
+
(d) =>
|
|
159
|
+
(d.kind === 'function' || d.kind === 'method') &&
|
|
160
|
+
d.line &&
|
|
161
|
+
d.cfg !== null &&
|
|
162
|
+
!Array.isArray(d.cfg?.blocks),
|
|
163
|
+
);
|
|
164
|
+
if (needsWasmCfg) {
|
|
165
|
+
cfgVisitor = createCfgVisitor(cfgRulesForLang);
|
|
166
|
+
visitors.push(cfgVisitor);
|
|
326
167
|
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Dataflow visitor
|
|
171
|
+
let dataflowVisitor = null;
|
|
172
|
+
const dfRules = DATAFLOW_RULES.get(langId);
|
|
173
|
+
if (doDataflow && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
|
|
174
|
+
dataflowVisitor = createDataflowVisitor(dfRules);
|
|
175
|
+
visitors.push(dataflowVisitor);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor };
|
|
179
|
+
}
|
|
327
180
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
181
|
+
// ─── Result storage helpers ─────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function storeComplexityResults(results, defs, langId) {
|
|
184
|
+
const complexityResults = results.complexity || [];
|
|
185
|
+
const resultByLine = new Map();
|
|
186
|
+
for (const r of complexityResults) {
|
|
187
|
+
if (r.funcNode) {
|
|
188
|
+
const line = r.funcNode.startPosition.row + 1;
|
|
189
|
+
if (!resultByLine.has(line)) resultByLine.set(line, []);
|
|
190
|
+
resultByLine.get(line).push(r);
|
|
331
191
|
}
|
|
332
192
|
}
|
|
193
|
+
for (const def of defs) {
|
|
194
|
+
if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
|
|
195
|
+
const candidates = resultByLine.get(def.line);
|
|
196
|
+
const funcResult = !candidates
|
|
197
|
+
? undefined
|
|
198
|
+
: candidates.length === 1
|
|
199
|
+
? candidates[0]
|
|
200
|
+
: (candidates.find((r) => {
|
|
201
|
+
const n = r.funcNode.childForFieldName('name');
|
|
202
|
+
return n && n.text === def.name;
|
|
203
|
+
}) ?? candidates[0]);
|
|
204
|
+
if (funcResult) {
|
|
205
|
+
const { metrics } = funcResult;
|
|
206
|
+
const loc = computeLOCMetrics(funcResult.funcNode, langId);
|
|
207
|
+
const volume = metrics.halstead ? metrics.halstead.volume : 0;
|
|
208
|
+
const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
209
|
+
const mi = computeMaintainabilityIndex(volume, metrics.cyclomatic, loc.sloc, commentRatio);
|
|
210
|
+
|
|
211
|
+
def.complexity = {
|
|
212
|
+
cognitive: metrics.cognitive,
|
|
213
|
+
cyclomatic: metrics.cyclomatic,
|
|
214
|
+
maxNesting: metrics.maxNesting,
|
|
215
|
+
halstead: metrics.halstead,
|
|
216
|
+
loc,
|
|
217
|
+
maintainabilityIndex: mi,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
333
223
|
|
|
334
|
-
|
|
224
|
+
function storeCfgResults(results, defs) {
|
|
225
|
+
const cfgResults = results.cfg || [];
|
|
226
|
+
const cfgByLine = new Map();
|
|
227
|
+
for (const r of cfgResults) {
|
|
228
|
+
if (r.funcNode) {
|
|
229
|
+
const line = r.funcNode.startPosition.row + 1;
|
|
230
|
+
if (!cfgByLine.has(line)) cfgByLine.set(line, []);
|
|
231
|
+
cfgByLine.get(line).push(r);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const def of defs) {
|
|
235
|
+
if (
|
|
236
|
+
(def.kind === 'function' || def.kind === 'method') &&
|
|
237
|
+
def.line &&
|
|
238
|
+
!def.cfg?.blocks?.length
|
|
239
|
+
) {
|
|
240
|
+
const candidates = cfgByLine.get(def.line);
|
|
241
|
+
const cfgResult = !candidates
|
|
242
|
+
? undefined
|
|
243
|
+
: candidates.length === 1
|
|
244
|
+
? candidates[0]
|
|
245
|
+
: (candidates.find((r) => {
|
|
246
|
+
const n = r.funcNode.childForFieldName('name');
|
|
247
|
+
return n && n.text === def.name;
|
|
248
|
+
}) ?? candidates[0]);
|
|
249
|
+
if (cfgResult) {
|
|
250
|
+
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
|
|
251
|
+
|
|
252
|
+
// Override complexity's cyclomatic with CFG-derived value (single source of truth)
|
|
253
|
+
if (def.complexity && cfgResult.cyclomatic != null) {
|
|
254
|
+
def.complexity.cyclomatic = cfgResult.cyclomatic;
|
|
255
|
+
const { loc, halstead } = def.complexity;
|
|
256
|
+
const volume = halstead ? halstead.volume : 0;
|
|
257
|
+
const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
258
|
+
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
|
|
259
|
+
volume,
|
|
260
|
+
cfgResult.cyclomatic,
|
|
261
|
+
loc?.sloc ?? 0,
|
|
262
|
+
commentRatio,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
335
269
|
|
|
336
|
-
|
|
337
|
-
// Each function finds pre-computed data from the unified walk above
|
|
338
|
-
// (or from the native engine) and only does DB writes + native fallback.
|
|
270
|
+
// ─── Build delegation ───────────────────────────────────────────────────
|
|
339
271
|
|
|
340
|
-
|
|
272
|
+
async function delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing) {
|
|
273
|
+
if (opts.ast !== false) {
|
|
341
274
|
const t0 = performance.now();
|
|
342
275
|
try {
|
|
343
276
|
const { buildAstNodes } = await import('../features/ast.js');
|
|
@@ -348,7 +281,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
|
348
281
|
timing.astMs = performance.now() - t0;
|
|
349
282
|
}
|
|
350
283
|
|
|
351
|
-
if (
|
|
284
|
+
if (opts.complexity !== false) {
|
|
352
285
|
const t0 = performance.now();
|
|
353
286
|
try {
|
|
354
287
|
const { buildComplexityMetrics } = await import('../features/complexity.js');
|
|
@@ -359,7 +292,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
|
359
292
|
timing.complexityMs = performance.now() - t0;
|
|
360
293
|
}
|
|
361
294
|
|
|
362
|
-
if (
|
|
295
|
+
if (opts.cfg !== false) {
|
|
363
296
|
const t0 = performance.now();
|
|
364
297
|
try {
|
|
365
298
|
const { buildCFGData } = await import('../features/cfg.js');
|
|
@@ -370,7 +303,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
|
370
303
|
timing.cfgMs = performance.now() - t0;
|
|
371
304
|
}
|
|
372
305
|
|
|
373
|
-
if (
|
|
306
|
+
if (opts.dataflow !== false) {
|
|
374
307
|
const t0 = performance.now();
|
|
375
308
|
try {
|
|
376
309
|
const { buildDataflowEdges } = await import('../features/dataflow.js');
|
|
@@ -380,6 +313,67 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
|
380
313
|
}
|
|
381
314
|
timing.dataflowMs = performance.now() - t0;
|
|
382
315
|
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Run all enabled AST analyses in a coordinated pass.
|
|
322
|
+
*
|
|
323
|
+
* @param {object} db - open better-sqlite3 database (read-write)
|
|
324
|
+
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, calls, _tree, _langId, ... }>
|
|
325
|
+
* @param {string} rootDir - absolute project root path
|
|
326
|
+
* @param {object} opts - build options (ast, complexity, cfg, dataflow toggles)
|
|
327
|
+
* @param {object} [engineOpts] - engine options
|
|
328
|
+
* @returns {Promise<{ astMs: number, complexityMs: number, cfgMs: number, dataflowMs: number }>}
|
|
329
|
+
*/
|
|
330
|
+
export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
331
|
+
const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
332
|
+
|
|
333
|
+
const doAst = opts.ast !== false;
|
|
334
|
+
const doComplexity = opts.complexity !== false;
|
|
335
|
+
const doCfg = opts.cfg !== false;
|
|
336
|
+
const doDataflow = opts.dataflow !== false;
|
|
337
|
+
|
|
338
|
+
if (!doAst && !doComplexity && !doCfg && !doDataflow) return timing;
|
|
339
|
+
|
|
340
|
+
const extToLang = buildExtToLangMap();
|
|
341
|
+
|
|
342
|
+
// WASM pre-parse for files that need it
|
|
343
|
+
await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
|
|
344
|
+
|
|
345
|
+
// Unified pre-walk: run all applicable visitors in a single DFS per file
|
|
346
|
+
const t0walk = performance.now();
|
|
347
|
+
|
|
348
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
349
|
+
if (!symbols._tree) continue;
|
|
350
|
+
|
|
351
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
352
|
+
const langId = symbols._langId || extToLang.get(ext);
|
|
353
|
+
if (!langId) continue;
|
|
354
|
+
|
|
355
|
+
const { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor } =
|
|
356
|
+
setupVisitors(db, relPath, symbols, langId, opts);
|
|
357
|
+
|
|
358
|
+
if (visitors.length === 0) continue;
|
|
359
|
+
|
|
360
|
+
const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
|
|
361
|
+
const defs = symbols.definitions || [];
|
|
362
|
+
|
|
363
|
+
if (astVisitor) {
|
|
364
|
+
const astRows = results['ast-store'] || [];
|
|
365
|
+
if (astRows.length > 0) symbols.astNodes = astRows;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (complexityVisitor) storeComplexityResults(results, defs, langId);
|
|
369
|
+
if (cfgVisitor) storeCfgResults(results, defs);
|
|
370
|
+
if (dataflowVisitor) symbols.dataflow = results.dataflow;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
timing._unifiedWalkMs = performance.now() - t0walk;
|
|
374
|
+
|
|
375
|
+
// Delegate to buildXxx functions for DB writes + native fallback
|
|
376
|
+
await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
|
|
383
377
|
|
|
384
378
|
return timing;
|
|
385
379
|
}
|
|
@@ -176,18 +176,6 @@ export function findFunctionNode(rootNode, startLine, _endLine, rules) {
|
|
|
176
176
|
return best;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/**
|
|
180
|
-
* Truncate a string to a maximum length, appending an ellipsis if truncated.
|
|
181
|
-
*
|
|
182
|
-
* @param {string} str - Input string
|
|
183
|
-
* @param {number} [max=200] - Maximum length
|
|
184
|
-
* @returns {string}
|
|
185
|
-
*/
|
|
186
|
-
export function truncate(str, max = 200) {
|
|
187
|
-
if (!str) return '';
|
|
188
|
-
return str.length > max ? `${str.slice(0, max)}…` : str;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
179
|
// ─── Extension / Language Mapping ─────────────────────────────────────────
|
|
192
180
|
|
|
193
181
|
/**
|