@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
|
@@ -12,25 +12,18 @@ import { computeConfidence } from '../../resolve.js';
|
|
|
12
12
|
import { BUILTIN_RECEIVERS, batchInsertEdges } from '../helpers.js';
|
|
13
13
|
import { getResolved, isBarrelFile, resolveBarrelExport } from './resolve-imports.js';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
* @param {import('../context.js').PipelineContext} ctx
|
|
17
|
-
*/
|
|
18
|
-
export async function buildEdges(ctx) {
|
|
19
|
-
const { db, fileSymbols, barrelOnlyFiles, rootDir, engineName } = ctx;
|
|
15
|
+
// ── Node lookup setup ───────────────────────────────────────────────────
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
function makeGetNodeIdStmt(db) {
|
|
18
|
+
return {
|
|
22
19
|
get: (name, kind, file, line) => {
|
|
23
20
|
const id = getNodeId(db, name, kind, file, line);
|
|
24
21
|
return id != null ? { id } : undefined;
|
|
25
22
|
},
|
|
26
23
|
};
|
|
24
|
+
}
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
const allNodes = db
|
|
30
|
-
.prepare(
|
|
31
|
-
`SELECT id, name, kind, file, line FROM nodes WHERE kind IN ('function','method','class','interface','struct','type','module','enum','trait')`,
|
|
32
|
-
)
|
|
33
|
-
.all();
|
|
26
|
+
function setupNodeLookups(ctx, allNodes) {
|
|
34
27
|
ctx.nodesByName = new Map();
|
|
35
28
|
for (const node of allNodes) {
|
|
36
29
|
if (!ctx.nodesByName.has(node.name)) ctx.nodesByName.set(node.name, []);
|
|
@@ -42,253 +35,339 @@ export async function buildEdges(ctx) {
|
|
|
42
35
|
if (!ctx.nodesByNameAndFile.has(key)) ctx.nodesByNameAndFile.set(key, []);
|
|
43
36
|
ctx.nodesByNameAndFile.get(key).push(node);
|
|
44
37
|
}
|
|
38
|
+
}
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
const buildEdgesTx = db.transaction(() => {
|
|
48
|
-
const allEdgeRows = [];
|
|
40
|
+
// ── Import edges ────────────────────────────────────────────────────────
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
actualSource &&
|
|
77
|
-
actualSource !== resolvedPath &&
|
|
78
|
-
!resolvedSources.has(actualSource)
|
|
79
|
-
) {
|
|
80
|
-
resolvedSources.add(actualSource);
|
|
81
|
-
const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
|
|
82
|
-
if (actualRow) {
|
|
83
|
-
allEdgeRows.push([
|
|
84
|
-
fileNodeId,
|
|
85
|
-
actualRow.id,
|
|
86
|
-
edgeKind === 'imports-type'
|
|
87
|
-
? 'imports-type'
|
|
88
|
-
: edgeKind === 'dynamic-imports'
|
|
89
|
-
? 'dynamic-imports'
|
|
90
|
-
: 'imports',
|
|
91
|
-
0.9,
|
|
92
|
-
0,
|
|
93
|
-
]);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
42
|
+
function buildImportEdges(ctx, getNodeIdStmt, allEdgeRows) {
|
|
43
|
+
const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
|
|
44
|
+
|
|
45
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
46
|
+
if (barrelOnlyFiles.has(relPath)) continue;
|
|
47
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
48
|
+
if (!fileNodeRow) continue;
|
|
49
|
+
const fileNodeId = fileNodeRow.id;
|
|
50
|
+
|
|
51
|
+
for (const imp of symbols.imports) {
|
|
52
|
+
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
53
|
+
const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
|
|
54
|
+
if (!targetRow) continue;
|
|
55
|
+
|
|
56
|
+
const edgeKind = imp.reexport
|
|
57
|
+
? 'reexports'
|
|
58
|
+
: imp.typeOnly
|
|
59
|
+
? 'imports-type'
|
|
60
|
+
: imp.dynamicImport
|
|
61
|
+
? 'dynamic-imports'
|
|
62
|
+
: 'imports';
|
|
63
|
+
allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
|
|
64
|
+
|
|
65
|
+
if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
|
|
66
|
+
buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
|
|
99
67
|
}
|
|
100
68
|
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
101
71
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
72
|
+
function buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, edgeRows) {
|
|
73
|
+
const resolvedSources = new Set();
|
|
74
|
+
for (const name of imp.names) {
|
|
75
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
76
|
+
const actualSource = resolveBarrelExport(ctx, resolvedPath, cleanName);
|
|
77
|
+
if (actualSource && actualSource !== resolvedPath && !resolvedSources.has(actualSource)) {
|
|
78
|
+
resolvedSources.add(actualSource);
|
|
79
|
+
const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
|
|
80
|
+
if (actualRow) {
|
|
81
|
+
const kind =
|
|
82
|
+
edgeKind === 'imports-type'
|
|
83
|
+
? 'imports-type'
|
|
84
|
+
: edgeKind === 'dynamic-imports'
|
|
85
|
+
? 'dynamic-imports'
|
|
86
|
+
: 'imports';
|
|
87
|
+
edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Call edges (native engine) ──────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native) {
|
|
96
|
+
const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
|
|
97
|
+
const nativeFiles = [];
|
|
98
|
+
|
|
99
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
100
|
+
if (barrelOnlyFiles.has(relPath)) continue;
|
|
101
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
102
|
+
if (!fileNodeRow) continue;
|
|
103
|
+
|
|
104
|
+
const importedNames = buildImportedNamesForNative(ctx, relPath, symbols, rootDir);
|
|
105
|
+
nativeFiles.push({
|
|
106
|
+
file: relPath,
|
|
107
|
+
fileNodeId: fileNodeRow.id,
|
|
108
|
+
definitions: symbols.definitions.map((d) => ({
|
|
109
|
+
name: d.name,
|
|
110
|
+
kind: d.kind,
|
|
111
|
+
line: d.line,
|
|
112
|
+
endLine: d.endLine ?? null,
|
|
113
|
+
})),
|
|
114
|
+
calls: symbols.calls,
|
|
115
|
+
importedNames,
|
|
116
|
+
classes: symbols.classes,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [...BUILTIN_RECEIVERS]);
|
|
121
|
+
for (const e of nativeEdges) {
|
|
122
|
+
allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
|
|
127
|
+
const importedNames = [];
|
|
128
|
+
for (const imp of symbols.imports) {
|
|
129
|
+
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
130
|
+
for (const name of imp.names) {
|
|
131
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
132
|
+
let targetFile = resolvedPath;
|
|
133
|
+
if (isBarrelFile(ctx, resolvedPath)) {
|
|
134
|
+
const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
|
|
135
|
+
if (actual) targetFile = actual;
|
|
136
|
+
}
|
|
137
|
+
importedNames.push({ name: cleanName, file: targetFile });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return importedNames;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Call edges (JS fallback) ────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows) {
|
|
146
|
+
const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
|
|
147
|
+
|
|
148
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
149
|
+
if (barrelOnlyFiles.has(relPath)) continue;
|
|
150
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
151
|
+
if (!fileNodeRow) continue;
|
|
152
|
+
|
|
153
|
+
const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
|
|
154
|
+
const seenCallEdges = new Set();
|
|
155
|
+
|
|
156
|
+
buildFileCallEdges(
|
|
157
|
+
ctx,
|
|
158
|
+
relPath,
|
|
159
|
+
symbols,
|
|
160
|
+
fileNodeRow,
|
|
161
|
+
importedNames,
|
|
162
|
+
seenCallEdges,
|
|
163
|
+
getNodeIdStmt,
|
|
164
|
+
allEdgeRows,
|
|
165
|
+
);
|
|
166
|
+
buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildImportedNamesMap(ctx, relPath, symbols, rootDir) {
|
|
171
|
+
const importedNames = new Map();
|
|
172
|
+
for (const imp of symbols.imports) {
|
|
173
|
+
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
174
|
+
for (const name of imp.names) {
|
|
175
|
+
importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return importedNames;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function findCaller(call, definitions, relPath, getNodeIdStmt, fileNodeRow) {
|
|
182
|
+
let caller = null;
|
|
183
|
+
let callerSpan = Infinity;
|
|
184
|
+
for (const def of definitions) {
|
|
185
|
+
if (def.line <= call.line) {
|
|
186
|
+
const end = def.endLine || Infinity;
|
|
187
|
+
if (call.line <= end) {
|
|
188
|
+
const span = end - def.line;
|
|
189
|
+
if (span < callerSpan) {
|
|
190
|
+
const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
|
|
191
|
+
if (row) {
|
|
192
|
+
caller = row;
|
|
193
|
+
callerSpan = span;
|
|
122
194
|
}
|
|
123
195
|
}
|
|
196
|
+
} else if (!caller) {
|
|
197
|
+
const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
|
|
198
|
+
if (row) caller = row;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return caller || fileNodeRow;
|
|
203
|
+
}
|
|
124
204
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
importedNames,
|
|
136
|
-
classes: symbols.classes,
|
|
137
|
-
});
|
|
205
|
+
function resolveCallTargets(ctx, call, relPath, importedNames) {
|
|
206
|
+
const importedFrom = importedNames.get(call.name);
|
|
207
|
+
let targets;
|
|
208
|
+
|
|
209
|
+
if (importedFrom) {
|
|
210
|
+
targets = ctx.nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
|
|
211
|
+
if (targets.length === 0 && isBarrelFile(ctx, importedFrom)) {
|
|
212
|
+
const actualSource = resolveBarrelExport(ctx, importedFrom, call.name);
|
|
213
|
+
if (actualSource) {
|
|
214
|
+
targets = ctx.nodesByNameAndFile.get(`${call.name}|${actualSource}`) || [];
|
|
138
215
|
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
139
218
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
219
|
+
if (!targets || targets.length === 0) {
|
|
220
|
+
targets = ctx.nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
|
|
221
|
+
if (targets.length === 0) {
|
|
222
|
+
targets = resolveByMethodOrGlobal(ctx, call, relPath);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (targets.length > 1) {
|
|
227
|
+
targets.sort((a, b) => {
|
|
228
|
+
const confA = computeConfidence(relPath, a.file, importedFrom);
|
|
229
|
+
const confB = computeConfidence(relPath, b.file, importedFrom);
|
|
230
|
+
return confB - confA;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { targets, importedFrom };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resolveByMethodOrGlobal(ctx, call, relPath) {
|
|
238
|
+
const methodCandidates = (ctx.nodesByName.get(call.name) || []).filter(
|
|
239
|
+
(n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
|
|
240
|
+
);
|
|
241
|
+
if (methodCandidates.length > 0) return methodCandidates;
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
!call.receiver ||
|
|
245
|
+
call.receiver === 'this' ||
|
|
246
|
+
call.receiver === 'self' ||
|
|
247
|
+
call.receiver === 'super'
|
|
248
|
+
) {
|
|
249
|
+
return (ctx.nodesByName.get(call.name) || []).filter(
|
|
250
|
+
(n) => computeConfidence(relPath, n.file, null) >= 0.5,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function buildFileCallEdges(
|
|
257
|
+
ctx,
|
|
258
|
+
relPath,
|
|
259
|
+
symbols,
|
|
260
|
+
fileNodeRow,
|
|
261
|
+
importedNames,
|
|
262
|
+
seenCallEdges,
|
|
263
|
+
getNodeIdStmt,
|
|
264
|
+
allEdgeRows,
|
|
265
|
+
) {
|
|
266
|
+
for (const call of symbols.calls) {
|
|
267
|
+
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
|
|
268
|
+
|
|
269
|
+
const caller = findCaller(call, symbols.definitions, relPath, getNodeIdStmt, fileNodeRow);
|
|
270
|
+
const isDynamic = call.dynamic ? 1 : 0;
|
|
271
|
+
const { targets, importedFrom } = resolveCallTargets(ctx, call, relPath, importedNames);
|
|
272
|
+
|
|
273
|
+
for (const t of targets) {
|
|
274
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
275
|
+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
|
|
276
|
+
seenCallEdges.add(edgeKey);
|
|
277
|
+
const confidence = computeConfidence(relPath, t.file, importedFrom);
|
|
278
|
+
allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
|
|
143
279
|
}
|
|
144
|
-
}
|
|
145
|
-
// JS fallback
|
|
146
|
-
for (const [relPath, symbols] of fileSymbols) {
|
|
147
|
-
if (barrelOnlyFiles.has(relPath)) continue;
|
|
148
|
-
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
149
|
-
if (!fileNodeRow) continue;
|
|
150
|
-
|
|
151
|
-
const importedNames = new Map();
|
|
152
|
-
for (const imp of symbols.imports) {
|
|
153
|
-
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
154
|
-
for (const name of imp.names) {
|
|
155
|
-
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
156
|
-
importedNames.set(cleanName, resolvedPath);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
280
|
+
}
|
|
159
281
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (row) {
|
|
173
|
-
caller = row;
|
|
174
|
-
callerSpan = span;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} else if (!caller) {
|
|
178
|
-
const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
|
|
179
|
-
if (row) caller = row;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (!caller) caller = fileNodeRow;
|
|
184
|
-
|
|
185
|
-
const isDynamic = call.dynamic ? 1 : 0;
|
|
186
|
-
let targets;
|
|
187
|
-
const importedFrom = importedNames.get(call.name);
|
|
188
|
-
|
|
189
|
-
if (importedFrom) {
|
|
190
|
-
targets = ctx.nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
|
|
191
|
-
if (targets.length === 0 && isBarrelFile(ctx, importedFrom)) {
|
|
192
|
-
const actualSource = resolveBarrelExport(ctx, importedFrom, call.name);
|
|
193
|
-
if (actualSource) {
|
|
194
|
-
targets = ctx.nodesByNameAndFile.get(`${call.name}|${actualSource}`) || [];
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (!targets || targets.length === 0) {
|
|
199
|
-
targets = ctx.nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
|
|
200
|
-
if (targets.length === 0) {
|
|
201
|
-
const methodCandidates = (ctx.nodesByName.get(call.name) || []).filter(
|
|
202
|
-
(n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
|
|
203
|
-
);
|
|
204
|
-
if (methodCandidates.length > 0) {
|
|
205
|
-
targets = methodCandidates;
|
|
206
|
-
} else if (
|
|
207
|
-
!call.receiver ||
|
|
208
|
-
call.receiver === 'this' ||
|
|
209
|
-
call.receiver === 'self' ||
|
|
210
|
-
call.receiver === 'super'
|
|
211
|
-
) {
|
|
212
|
-
targets = (ctx.nodesByName.get(call.name) || []).filter(
|
|
213
|
-
(n) => computeConfidence(relPath, n.file, null) >= 0.5,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
282
|
+
// Receiver edge
|
|
283
|
+
if (
|
|
284
|
+
call.receiver &&
|
|
285
|
+
!BUILTIN_RECEIVERS.has(call.receiver) &&
|
|
286
|
+
call.receiver !== 'this' &&
|
|
287
|
+
call.receiver !== 'self' &&
|
|
288
|
+
call.receiver !== 'super'
|
|
289
|
+
) {
|
|
290
|
+
buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
218
294
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
295
|
+
function buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows) {
|
|
296
|
+
const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']);
|
|
297
|
+
const samefile = ctx.nodesByNameAndFile.get(`${call.receiver}|${relPath}`) || [];
|
|
298
|
+
const candidates = samefile.length > 0 ? samefile : ctx.nodesByName.get(call.receiver) || [];
|
|
299
|
+
const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
|
|
300
|
+
if (receiverNodes.length > 0 && caller) {
|
|
301
|
+
const recvTarget = receiverNodes[0];
|
|
302
|
+
const recvKey = `recv|${caller.id}|${recvTarget.id}`;
|
|
303
|
+
if (!seenCallEdges.has(recvKey)) {
|
|
304
|
+
seenCallEdges.add(recvKey);
|
|
305
|
+
allEdgeRows.push([caller.id, recvTarget.id, 'receiver', 0.7, 0]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
226
309
|
|
|
227
|
-
|
|
228
|
-
const edgeKey = `${caller.id}|${t.id}`;
|
|
229
|
-
if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
|
|
230
|
-
seenCallEdges.add(edgeKey);
|
|
231
|
-
const confidence = computeConfidence(relPath, t.file, importedFrom);
|
|
232
|
-
allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
310
|
+
// ── Class hierarchy edges ───────────────────────────────────────────────
|
|
235
311
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const candidates =
|
|
247
|
-
samefile.length > 0 ? samefile : ctx.nodesByName.get(call.receiver) || [];
|
|
248
|
-
const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
|
|
249
|
-
if (receiverNodes.length > 0 && caller) {
|
|
250
|
-
const recvTarget = receiverNodes[0];
|
|
251
|
-
const recvKey = `recv|${caller.id}|${recvTarget.id}`;
|
|
252
|
-
if (!seenCallEdges.has(recvKey)) {
|
|
253
|
-
seenCallEdges.add(recvKey);
|
|
254
|
-
allEdgeRows.push([caller.id, recvTarget.id, 'receiver', 0.7, 0]);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
312
|
+
function buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows) {
|
|
313
|
+
for (const cls of symbols.classes) {
|
|
314
|
+
if (cls.extends) {
|
|
315
|
+
const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
|
|
316
|
+
(n) => n.kind === 'class',
|
|
317
|
+
);
|
|
318
|
+
const targetRows = (ctx.nodesByName.get(cls.extends) || []).filter((n) => n.kind === 'class');
|
|
319
|
+
if (sourceRow) {
|
|
320
|
+
for (const t of targetRows) {
|
|
321
|
+
allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
|
|
258
322
|
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
259
325
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (cls.implements) {
|
|
276
|
-
const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
|
|
277
|
-
(n) => n.kind === 'class',
|
|
278
|
-
);
|
|
279
|
-
const targetCandidates = ctx.nodesByName.get(cls.implements) || [];
|
|
280
|
-
const targetRows = targetCandidates.filter(
|
|
281
|
-
(n) => n.kind === 'interface' || n.kind === 'class',
|
|
282
|
-
);
|
|
283
|
-
if (sourceRow) {
|
|
284
|
-
for (const t of targetRows) {
|
|
285
|
-
allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
326
|
+
if (cls.implements) {
|
|
327
|
+
const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
|
|
328
|
+
(n) => n.kind === 'class',
|
|
329
|
+
);
|
|
330
|
+
const targetRows = (ctx.nodesByName.get(cls.implements) || []).filter(
|
|
331
|
+
(n) => n.kind === 'interface' || n.kind === 'class',
|
|
332
|
+
);
|
|
333
|
+
if (sourceRow) {
|
|
334
|
+
for (const t of targetRows) {
|
|
335
|
+
allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
|
|
289
336
|
}
|
|
290
337
|
}
|
|
291
338
|
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Main entry point ────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {import('../context.js').PipelineContext} ctx
|
|
346
|
+
*/
|
|
347
|
+
export async function buildEdges(ctx) {
|
|
348
|
+
const { db, engineName } = ctx;
|
|
349
|
+
|
|
350
|
+
const getNodeIdStmt = makeGetNodeIdStmt(db);
|
|
351
|
+
|
|
352
|
+
const allNodes = db
|
|
353
|
+
.prepare(
|
|
354
|
+
`SELECT id, name, kind, file, line FROM nodes WHERE kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`,
|
|
355
|
+
)
|
|
356
|
+
.all();
|
|
357
|
+
setupNodeLookups(ctx, allNodes);
|
|
358
|
+
|
|
359
|
+
const t0 = performance.now();
|
|
360
|
+
const buildEdgesTx = db.transaction(() => {
|
|
361
|
+
const allEdgeRows = [];
|
|
362
|
+
|
|
363
|
+
buildImportEdges(ctx, getNodeIdStmt, allEdgeRows);
|
|
364
|
+
|
|
365
|
+
const native = engineName === 'native' ? loadNative() : null;
|
|
366
|
+
if (native?.buildCallEdges) {
|
|
367
|
+
buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native);
|
|
368
|
+
} else {
|
|
369
|
+
buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows);
|
|
370
|
+
}
|
|
292
371
|
|
|
293
372
|
batchInsertEdges(db, allEdgeRows);
|
|
294
373
|
});
|