@dreamtree-org/graphify 1.0.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/LICENSE +21 -0
- package/README.md +69 -0
- package/bin/graphify-mcp.js +7 -0
- package/bin/graphify.js +2 -0
- package/dist/chunk-5ANIDX3G.js +364 -0
- package/dist/chunk-5ANIDX3G.js.map +1 -0
- package/dist/chunk-DG5FECXV.js +2474 -0
- package/dist/chunk-DG5FECXV.js.map +1 -0
- package/dist/cli/index.cjs +3250 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +267 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +2759 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +301 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.cjs +336 -0
- package/dist/mcp/server.cjs.map +1 -0
- package/dist/mcp/server.d.cts +22 -0
- package/dist/mcp/server.d.ts +22 -0
- package/dist/mcp/server.js +88 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +81 -0
- package/src/skill/SKILL.md +74 -0
|
@@ -0,0 +1,2474 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml,
|
|
3
|
+
sanitizeLabel,
|
|
4
|
+
validateGraphPath
|
|
5
|
+
} from "./chunk-5ANIDX3G.js";
|
|
6
|
+
|
|
7
|
+
// src/detect.ts
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
11
|
+
"node_modules",
|
|
12
|
+
".git",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
"out",
|
|
16
|
+
"graphify-out",
|
|
17
|
+
".next",
|
|
18
|
+
".venv",
|
|
19
|
+
"venv",
|
|
20
|
+
"__pycache__",
|
|
21
|
+
".cache",
|
|
22
|
+
"coverage",
|
|
23
|
+
".turbo"
|
|
24
|
+
]);
|
|
25
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
26
|
+
".ts",
|
|
27
|
+
".tsx",
|
|
28
|
+
".js",
|
|
29
|
+
".jsx",
|
|
30
|
+
".mjs",
|
|
31
|
+
".cjs",
|
|
32
|
+
".mts",
|
|
33
|
+
".cts",
|
|
34
|
+
".py",
|
|
35
|
+
".go",
|
|
36
|
+
".rs",
|
|
37
|
+
".java",
|
|
38
|
+
".cs",
|
|
39
|
+
".rb",
|
|
40
|
+
".c",
|
|
41
|
+
".h",
|
|
42
|
+
".cpp",
|
|
43
|
+
".hpp",
|
|
44
|
+
".cc",
|
|
45
|
+
".php",
|
|
46
|
+
".kt",
|
|
47
|
+
".swift",
|
|
48
|
+
".scala",
|
|
49
|
+
".lua",
|
|
50
|
+
".sh",
|
|
51
|
+
".sql"
|
|
52
|
+
]);
|
|
53
|
+
var DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".txt", ".rst", ".adoc", ".org", ".json", ".yaml", ".yml", ".toml"]);
|
|
54
|
+
var PAPER_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".tex", ".bib"]);
|
|
55
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".bmp", ".ico"]);
|
|
56
|
+
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm"]);
|
|
57
|
+
var SENSITIVE_NAME_PATTERNS = [
|
|
58
|
+
/^\.env(\..*)?$/i,
|
|
59
|
+
/^\.npmrc$/i,
|
|
60
|
+
/^\.pypirc$/i,
|
|
61
|
+
/credentials/i,
|
|
62
|
+
/secret/i,
|
|
63
|
+
/^id_rsa$/i,
|
|
64
|
+
/^id_ed25519$/i,
|
|
65
|
+
/\.pem$/i,
|
|
66
|
+
/\.key$/i,
|
|
67
|
+
/\.pfx$/i,
|
|
68
|
+
/\.p12$/i,
|
|
69
|
+
/^\.aws\//i,
|
|
70
|
+
/service[-_]?account.*\.json$/i
|
|
71
|
+
];
|
|
72
|
+
function isSensitiveName(name) {
|
|
73
|
+
return SENSITIVE_NAME_PATTERNS.some((pattern) => pattern.test(name));
|
|
74
|
+
}
|
|
75
|
+
function categorize(ext) {
|
|
76
|
+
const lower = ext.toLowerCase();
|
|
77
|
+
if (CODE_EXTENSIONS.has(lower)) return "code";
|
|
78
|
+
if (DOCUMENT_EXTENSIONS.has(lower)) return "document";
|
|
79
|
+
if (PAPER_EXTENSIONS.has(lower)) return "paper";
|
|
80
|
+
if (IMAGE_EXTENSIONS.has(lower)) return "image";
|
|
81
|
+
if (VIDEO_EXTENSIONS.has(lower)) return "video";
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function countWords(content) {
|
|
85
|
+
const trimmed = content.trim();
|
|
86
|
+
if (trimmed.length === 0) return 0;
|
|
87
|
+
return trimmed.split(/\s+/).length;
|
|
88
|
+
}
|
|
89
|
+
function readTextSafely(filePath) {
|
|
90
|
+
const buf = fs.readFileSync(filePath);
|
|
91
|
+
return buf.toString("utf-8");
|
|
92
|
+
}
|
|
93
|
+
function collectFiles(root) {
|
|
94
|
+
const scanRoot = path.resolve(root);
|
|
95
|
+
const stat = fs.statSync(scanRoot);
|
|
96
|
+
if (!stat.isDirectory()) {
|
|
97
|
+
throw new Error(`collectFiles: not a directory: ${scanRoot}`);
|
|
98
|
+
}
|
|
99
|
+
const files = {
|
|
100
|
+
code: [],
|
|
101
|
+
document: [],
|
|
102
|
+
paper: [],
|
|
103
|
+
image: [],
|
|
104
|
+
video: []
|
|
105
|
+
};
|
|
106
|
+
const skippedSensitive = [];
|
|
107
|
+
let totalWords = 0;
|
|
108
|
+
const stack = [scanRoot];
|
|
109
|
+
const textCategories = /* @__PURE__ */ new Set(["code", "document", "paper"]);
|
|
110
|
+
while (stack.length > 0) {
|
|
111
|
+
const dir = stack.pop();
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
115
|
+
} catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (entry.isSymbolicLink()) continue;
|
|
121
|
+
const fullPath = path.join(dir, entry.name);
|
|
122
|
+
const relPath = path.relative(scanRoot, fullPath);
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
125
|
+
stack.push(fullPath);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!entry.isFile()) continue;
|
|
129
|
+
if (isSensitiveName(entry.name)) {
|
|
130
|
+
skippedSensitive.push(relPath);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const ext = path.extname(entry.name);
|
|
134
|
+
const category = categorize(ext);
|
|
135
|
+
if (category === null) continue;
|
|
136
|
+
files[category].push(relPath);
|
|
137
|
+
if (textCategories.has(category)) {
|
|
138
|
+
try {
|
|
139
|
+
const content = readTextSafely(fullPath);
|
|
140
|
+
totalWords += countWords(content);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const category of Object.keys(files)) {
|
|
147
|
+
files[category].sort();
|
|
148
|
+
}
|
|
149
|
+
skippedSensitive.sort();
|
|
150
|
+
const totalFiles = Object.values(files).reduce((sum, list) => sum + list.length, 0);
|
|
151
|
+
return {
|
|
152
|
+
scanRoot,
|
|
153
|
+
files,
|
|
154
|
+
totalFiles,
|
|
155
|
+
totalWords,
|
|
156
|
+
skippedSensitive
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/extract.ts
|
|
161
|
+
import * as path4 from "path";
|
|
162
|
+
|
|
163
|
+
// src/extractors/csharp.ts
|
|
164
|
+
import * as fs2 from "fs/promises";
|
|
165
|
+
|
|
166
|
+
// src/extractors/common.ts
|
|
167
|
+
import * as path2 from "path";
|
|
168
|
+
function namedChildren(node) {
|
|
169
|
+
return node.namedChildren.filter((c) => c !== null);
|
|
170
|
+
}
|
|
171
|
+
function descendantsOfType(node, types) {
|
|
172
|
+
return node.descendantsOfType(types).filter((c) => c !== null);
|
|
173
|
+
}
|
|
174
|
+
function toPosix(p) {
|
|
175
|
+
return p.split(path2.sep).join("/");
|
|
176
|
+
}
|
|
177
|
+
function lineOf(node) {
|
|
178
|
+
return `L${node.startPosition.row + 1}`;
|
|
179
|
+
}
|
|
180
|
+
function entityId(sourceFile, qualifiedName) {
|
|
181
|
+
return `${toPosix(sourceFile)}::${qualifiedName}`;
|
|
182
|
+
}
|
|
183
|
+
function externalId(name) {
|
|
184
|
+
return `external:${name}`;
|
|
185
|
+
}
|
|
186
|
+
function resolveModuleRef(sourceFile, specifier) {
|
|
187
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
188
|
+
const dir = path2.posix.dirname(toPosix(sourceFile));
|
|
189
|
+
const joined = specifier.startsWith("/") ? specifier : path2.posix.join(dir, specifier);
|
|
190
|
+
const normalized = path2.posix.normalize(joined);
|
|
191
|
+
return { id: normalized, label: specifier, external: false };
|
|
192
|
+
}
|
|
193
|
+
return { id: `module:${specifier}`, label: specifier, external: true };
|
|
194
|
+
}
|
|
195
|
+
var ExtractionBuilder = class {
|
|
196
|
+
nodeIds = /* @__PURE__ */ new Set();
|
|
197
|
+
nodes = [];
|
|
198
|
+
edges = [];
|
|
199
|
+
addNode(node) {
|
|
200
|
+
if (this.nodeIds.has(node.id)) return;
|
|
201
|
+
this.nodeIds.add(node.id);
|
|
202
|
+
this.nodes.push(node);
|
|
203
|
+
}
|
|
204
|
+
hasNode(id) {
|
|
205
|
+
return this.nodeIds.has(id);
|
|
206
|
+
}
|
|
207
|
+
addEdge(edge) {
|
|
208
|
+
this.edges.push(edge);
|
|
209
|
+
}
|
|
210
|
+
build() {
|
|
211
|
+
return { nodes: this.nodes, edges: this.edges };
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/extractors/wasmLoader.ts
|
|
216
|
+
import { createRequire } from "module";
|
|
217
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
218
|
+
var require2 = createRequire(import.meta.url);
|
|
219
|
+
var initPromise = null;
|
|
220
|
+
function ensureInitialized() {
|
|
221
|
+
if (!initPromise) {
|
|
222
|
+
initPromise = Parser.init();
|
|
223
|
+
}
|
|
224
|
+
return initPromise;
|
|
225
|
+
}
|
|
226
|
+
var languageCache = /* @__PURE__ */ new Map();
|
|
227
|
+
async function loadLanguage(grammarName) {
|
|
228
|
+
await ensureInitialized();
|
|
229
|
+
let cached = languageCache.get(grammarName);
|
|
230
|
+
if (!cached) {
|
|
231
|
+
const wasmPath = require2.resolve(`tree-sitter-wasms/out/tree-sitter-${grammarName}.wasm`);
|
|
232
|
+
cached = Language.load(wasmPath);
|
|
233
|
+
languageCache.set(grammarName, cached);
|
|
234
|
+
}
|
|
235
|
+
return cached;
|
|
236
|
+
}
|
|
237
|
+
async function createParser(grammarName) {
|
|
238
|
+
const language = await loadLanguage(grammarName);
|
|
239
|
+
const parser = new Parser();
|
|
240
|
+
parser.setLanguage(language);
|
|
241
|
+
return parser;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/extractors/csharp.ts
|
|
245
|
+
async function extractCSharp(filePath) {
|
|
246
|
+
const parser = await createParser("c_sharp");
|
|
247
|
+
const raw = await fs2.readFile(filePath);
|
|
248
|
+
const content = raw.toString("utf-8");
|
|
249
|
+
const tree = parser.parse(content);
|
|
250
|
+
if (!tree) {
|
|
251
|
+
throw new Error(`extractCSharp: parser produced no tree for ${filePath}`);
|
|
252
|
+
}
|
|
253
|
+
const root = tree.rootNode;
|
|
254
|
+
const sourceFile = toPosix(filePath);
|
|
255
|
+
const builder = new ExtractionBuilder();
|
|
256
|
+
const fileId = sourceFile;
|
|
257
|
+
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
258
|
+
const nameIndex = /* @__PURE__ */ new Map();
|
|
259
|
+
const functionNodeMap = /* @__PURE__ */ new Map();
|
|
260
|
+
const classMethods = /* @__PURE__ */ new Map();
|
|
261
|
+
const enclosingClassOf = /* @__PURE__ */ new Map();
|
|
262
|
+
const importIndex = /* @__PURE__ */ new Map();
|
|
263
|
+
function addModuleNode(ref) {
|
|
264
|
+
if (builder.hasNode(ref.id)) return;
|
|
265
|
+
builder.addNode({
|
|
266
|
+
id: ref.id,
|
|
267
|
+
label: ref.label,
|
|
268
|
+
sourceFile: ref.external ? "<external>" : ref.id,
|
|
269
|
+
sourceLocation: "L1"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function resolveHeritageTarget(name) {
|
|
273
|
+
const resolved = nameIndex.get(name);
|
|
274
|
+
if (resolved) return resolved;
|
|
275
|
+
const extId = externalId(name);
|
|
276
|
+
if (!builder.hasNode(extId)) {
|
|
277
|
+
builder.addNode({ id: extId, label: name, sourceFile: "<external>", sourceLocation: "L0" });
|
|
278
|
+
}
|
|
279
|
+
return extId;
|
|
280
|
+
}
|
|
281
|
+
function handleUsingDirective(node) {
|
|
282
|
+
const children = namedChildren(node);
|
|
283
|
+
const first = children[0];
|
|
284
|
+
if (!first) return;
|
|
285
|
+
let aliasName;
|
|
286
|
+
let targetNode;
|
|
287
|
+
if (first.type === "name_equals") {
|
|
288
|
+
aliasName = namedChildren(first)[0]?.text;
|
|
289
|
+
targetNode = children[1];
|
|
290
|
+
} else {
|
|
291
|
+
targetNode = first;
|
|
292
|
+
}
|
|
293
|
+
if (!targetNode) return;
|
|
294
|
+
const ref = resolveModuleRef(sourceFile, targetNode.text);
|
|
295
|
+
addModuleNode(ref);
|
|
296
|
+
if (aliasName) importIndex.set(aliasName, ref);
|
|
297
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
298
|
+
}
|
|
299
|
+
function handleMethodDeclaration(typeNode, typeId, typeName, member) {
|
|
300
|
+
const methodName = member.childForFieldName("name")?.text ?? "(anonymous)";
|
|
301
|
+
const methodId = entityId(sourceFile, `${typeName}.${methodName}`);
|
|
302
|
+
builder.addNode({
|
|
303
|
+
id: methodId,
|
|
304
|
+
label: `method ${typeName}.${methodName}`,
|
|
305
|
+
sourceFile,
|
|
306
|
+
sourceLocation: lineOf(member)
|
|
307
|
+
});
|
|
308
|
+
builder.addEdge({ source: typeId, target: methodId, relation: "method", confidence: "EXTRACTED" });
|
|
309
|
+
let methods = classMethods.get(typeNode.id);
|
|
310
|
+
if (!methods) {
|
|
311
|
+
methods = /* @__PURE__ */ new Map();
|
|
312
|
+
classMethods.set(typeNode.id, methods);
|
|
313
|
+
}
|
|
314
|
+
methods.set(methodName, methodId);
|
|
315
|
+
functionNodeMap.set(member.id, methodId);
|
|
316
|
+
enclosingClassOf.set(member.id, typeNode.id);
|
|
317
|
+
}
|
|
318
|
+
function handleBaseList(typeId, node) {
|
|
319
|
+
const baseList = node.childForFieldName("bases");
|
|
320
|
+
if (!baseList) return;
|
|
321
|
+
const entries = namedChildren(baseList);
|
|
322
|
+
entries.forEach((entry, index) => {
|
|
323
|
+
builder.addEdge({
|
|
324
|
+
source: typeId,
|
|
325
|
+
target: resolveHeritageTarget(entry.text),
|
|
326
|
+
relation: index === 0 ? "inherits" : "implements",
|
|
327
|
+
confidence: "EXTRACTED"
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function handleClassDeclaration(node) {
|
|
332
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
333
|
+
const classId = entityId(sourceFile, name);
|
|
334
|
+
builder.addNode({ id: classId, label: `class ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
335
|
+
builder.addEdge({ source: fileId, target: classId, relation: "contains", confidence: "EXTRACTED" });
|
|
336
|
+
nameIndex.set(name, classId);
|
|
337
|
+
functionNodeMap.set(node.id, classId);
|
|
338
|
+
classMethods.set(node.id, /* @__PURE__ */ new Map());
|
|
339
|
+
handleBaseList(classId, node);
|
|
340
|
+
const body = node.childForFieldName("body");
|
|
341
|
+
if (body) {
|
|
342
|
+
for (const member of namedChildren(body)) {
|
|
343
|
+
if (member.type !== "method_declaration") continue;
|
|
344
|
+
handleMethodDeclaration(node, classId, name, member);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function handleInterfaceDeclaration(node) {
|
|
349
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
350
|
+
const id = entityId(sourceFile, name);
|
|
351
|
+
builder.addNode({ id, label: `interface ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
352
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
353
|
+
nameIndex.set(name, id);
|
|
354
|
+
handleBaseList(id, node);
|
|
355
|
+
}
|
|
356
|
+
function walkTopLevel(node) {
|
|
357
|
+
for (const child of namedChildren(node)) {
|
|
358
|
+
switch (child.type) {
|
|
359
|
+
case "using_directive":
|
|
360
|
+
handleUsingDirective(child);
|
|
361
|
+
break;
|
|
362
|
+
case "class_declaration":
|
|
363
|
+
handleClassDeclaration(child);
|
|
364
|
+
break;
|
|
365
|
+
case "interface_declaration":
|
|
366
|
+
handleInterfaceDeclaration(child);
|
|
367
|
+
break;
|
|
368
|
+
case "namespace_declaration": {
|
|
369
|
+
const nsBody = child.childForFieldName("body");
|
|
370
|
+
if (nsBody) walkTopLevel(nsBody);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "file_scoped_namespace_declaration":
|
|
374
|
+
walkTopLevel(child);
|
|
375
|
+
break;
|
|
376
|
+
default:
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
walkTopLevel(root);
|
|
382
|
+
function closestTrackedCaller(node) {
|
|
383
|
+
let current = node.parent;
|
|
384
|
+
while (current) {
|
|
385
|
+
const tracked = functionNodeMap.get(current.id);
|
|
386
|
+
if (tracked) return tracked;
|
|
387
|
+
current = current.parent;
|
|
388
|
+
}
|
|
389
|
+
return fileId;
|
|
390
|
+
}
|
|
391
|
+
function closestEnclosingClassMethods(node) {
|
|
392
|
+
let current = node.parent;
|
|
393
|
+
while (current) {
|
|
394
|
+
if (current.type === "method_declaration") {
|
|
395
|
+
const typeNodeId = enclosingClassOf.get(current.id);
|
|
396
|
+
if (typeNodeId !== void 0) return classMethods.get(typeNodeId);
|
|
397
|
+
}
|
|
398
|
+
current = current.parent;
|
|
399
|
+
}
|
|
400
|
+
return void 0;
|
|
401
|
+
}
|
|
402
|
+
for (const call of descendantsOfType(root, ["invocation_expression"])) {
|
|
403
|
+
const fn = call.childForFieldName("function");
|
|
404
|
+
if (!fn) continue;
|
|
405
|
+
let targetId = null;
|
|
406
|
+
let confidence = "EXTRACTED";
|
|
407
|
+
if (fn.type === "identifier") {
|
|
408
|
+
const name = fn.text;
|
|
409
|
+
const methodId = closestEnclosingClassMethods(call)?.get(name);
|
|
410
|
+
if (methodId) {
|
|
411
|
+
targetId = methodId;
|
|
412
|
+
} else {
|
|
413
|
+
const viaImport = importIndex.get(name);
|
|
414
|
+
if (viaImport) {
|
|
415
|
+
targetId = viaImport.id;
|
|
416
|
+
confidence = "INFERRED";
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} else if (fn.type === "member_access_expression") {
|
|
420
|
+
const expression = fn.childForFieldName("expression");
|
|
421
|
+
const name = fn.childForFieldName("name")?.text;
|
|
422
|
+
if (expression && name) {
|
|
423
|
+
if (expression.type === "this_expression") {
|
|
424
|
+
const methodId = closestEnclosingClassMethods(call)?.get(name);
|
|
425
|
+
if (methodId) targetId = methodId;
|
|
426
|
+
} else if (expression.type === "identifier") {
|
|
427
|
+
const viaImport = importIndex.get(expression.text);
|
|
428
|
+
if (viaImport) {
|
|
429
|
+
targetId = viaImport.id;
|
|
430
|
+
confidence = "INFERRED";
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!targetId) continue;
|
|
436
|
+
const callerId = closestTrackedCaller(call);
|
|
437
|
+
builder.addEdge({ source: callerId, target: targetId, relation: "calls", confidence });
|
|
438
|
+
}
|
|
439
|
+
return builder.build();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/extractors/go.ts
|
|
443
|
+
import * as fs3 from "fs/promises";
|
|
444
|
+
function stripQuotes(text) {
|
|
445
|
+
if (text.length >= 2) {
|
|
446
|
+
const first = text[0];
|
|
447
|
+
const last = text[text.length - 1];
|
|
448
|
+
if ((first === '"' || first === "`") && first === last) {
|
|
449
|
+
return text.slice(1, -1);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return text;
|
|
453
|
+
}
|
|
454
|
+
function defaultPackageQualifier(importPath) {
|
|
455
|
+
const segments = importPath.split("/");
|
|
456
|
+
return segments[segments.length - 1] ?? importPath;
|
|
457
|
+
}
|
|
458
|
+
async function extractGo(filePath) {
|
|
459
|
+
const parser = await createParser("go");
|
|
460
|
+
const raw = await fs3.readFile(filePath);
|
|
461
|
+
const content = raw.toString("utf-8");
|
|
462
|
+
const tree = parser.parse(content);
|
|
463
|
+
if (!tree) {
|
|
464
|
+
throw new Error(`extractGo: parser produced no tree for ${filePath}`);
|
|
465
|
+
}
|
|
466
|
+
const root = tree.rootNode;
|
|
467
|
+
const sourceFile = toPosix(filePath);
|
|
468
|
+
const builder = new ExtractionBuilder();
|
|
469
|
+
const fileId = sourceFile;
|
|
470
|
+
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
471
|
+
const nameIndex = /* @__PURE__ */ new Map();
|
|
472
|
+
const functionNodeMap = /* @__PURE__ */ new Map();
|
|
473
|
+
const typeMethods = /* @__PURE__ */ new Map();
|
|
474
|
+
const receiverInfo = /* @__PURE__ */ new Map();
|
|
475
|
+
const importIndex = /* @__PURE__ */ new Map();
|
|
476
|
+
function addModuleNode(ref) {
|
|
477
|
+
if (builder.hasNode(ref.id)) return;
|
|
478
|
+
builder.addNode({
|
|
479
|
+
id: ref.id,
|
|
480
|
+
label: ref.label,
|
|
481
|
+
sourceFile: ref.external ? "<external>" : ref.id,
|
|
482
|
+
sourceLocation: "L1"
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function resolveTypeTarget(name) {
|
|
486
|
+
const resolved = nameIndex.get(name);
|
|
487
|
+
if (resolved) return resolved;
|
|
488
|
+
const extId = externalId(name);
|
|
489
|
+
if (!builder.hasNode(extId)) {
|
|
490
|
+
builder.addNode({ id: extId, label: name, sourceFile: "<external>", sourceLocation: "L0" });
|
|
491
|
+
}
|
|
492
|
+
return extId;
|
|
493
|
+
}
|
|
494
|
+
function handleImportSpec(spec) {
|
|
495
|
+
const pathNode = spec.childForFieldName("path");
|
|
496
|
+
if (!pathNode) return;
|
|
497
|
+
const specifier = stripQuotes(pathNode.text);
|
|
498
|
+
const ref = resolveModuleRef(sourceFile, specifier);
|
|
499
|
+
addModuleNode(ref);
|
|
500
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
501
|
+
const aliasNode = spec.childForFieldName("name");
|
|
502
|
+
const alias = aliasNode?.text;
|
|
503
|
+
if (alias && alias !== "_" && alias !== ".") {
|
|
504
|
+
importIndex.set(alias, ref);
|
|
505
|
+
} else if (!alias) {
|
|
506
|
+
importIndex.set(defaultPackageQualifier(specifier), ref);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function handleImportDeclaration(node) {
|
|
510
|
+
for (const child of namedChildren(node)) {
|
|
511
|
+
if (child.type === "import_spec") {
|
|
512
|
+
handleImportSpec(child);
|
|
513
|
+
} else if (child.type === "import_spec_list") {
|
|
514
|
+
for (const spec of namedChildren(child)) {
|
|
515
|
+
if (spec.type === "import_spec") handleImportSpec(spec);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function handleFunctionDeclaration(node) {
|
|
521
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
522
|
+
const id = entityId(sourceFile, name);
|
|
523
|
+
builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
524
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
525
|
+
nameIndex.set(name, id);
|
|
526
|
+
functionNodeMap.set(node.id, id);
|
|
527
|
+
}
|
|
528
|
+
function handleTypeDeclaration(node) {
|
|
529
|
+
for (const spec of namedChildren(node)) {
|
|
530
|
+
if (spec.type !== "type_spec") continue;
|
|
531
|
+
const name = spec.childForFieldName("name")?.text;
|
|
532
|
+
if (!name) continue;
|
|
533
|
+
const underlying = spec.childForFieldName("type");
|
|
534
|
+
const kind = underlying?.type === "struct_type" ? "struct" : underlying?.type === "interface_type" ? "interface" : "type";
|
|
535
|
+
const id = entityId(sourceFile, name);
|
|
536
|
+
builder.addNode({ id, label: `${kind} ${name}`, sourceFile, sourceLocation: lineOf(spec) });
|
|
537
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
538
|
+
nameIndex.set(name, id);
|
|
539
|
+
typeMethods.set(id, /* @__PURE__ */ new Map());
|
|
540
|
+
if (underlying?.type === "struct_type") {
|
|
541
|
+
const fieldList = namedChildren(underlying).find((c) => c.type === "field_declaration_list");
|
|
542
|
+
if (fieldList) {
|
|
543
|
+
for (const field of namedChildren(fieldList)) {
|
|
544
|
+
if (field.type !== "field_declaration") continue;
|
|
545
|
+
if (field.childForFieldName("name")) continue;
|
|
546
|
+
const embeddedType = namedChildren(field)[0];
|
|
547
|
+
if (!embeddedType) continue;
|
|
548
|
+
const base = embeddedType.type === "pointer_type" ? namedChildren(embeddedType)[0] : embeddedType;
|
|
549
|
+
if (!base) continue;
|
|
550
|
+
builder.addEdge({
|
|
551
|
+
source: id,
|
|
552
|
+
target: resolveTypeTarget(base.text),
|
|
553
|
+
relation: "embeds",
|
|
554
|
+
confidence: "EXTRACTED"
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function receiverTypeName(receiver) {
|
|
562
|
+
const paramDecl = namedChildren(receiver)[0];
|
|
563
|
+
if (!paramDecl) return null;
|
|
564
|
+
let typeNode = paramDecl.childForFieldName("type");
|
|
565
|
+
if (typeNode?.type === "pointer_type") {
|
|
566
|
+
typeNode = namedChildren(typeNode)[0] ?? null;
|
|
567
|
+
}
|
|
568
|
+
return typeNode?.type === "type_identifier" ? typeNode.text : null;
|
|
569
|
+
}
|
|
570
|
+
function receiverVarName(receiver) {
|
|
571
|
+
const paramDecl = namedChildren(receiver)[0];
|
|
572
|
+
return paramDecl?.childForFieldName("name")?.text ?? null;
|
|
573
|
+
}
|
|
574
|
+
function handleMethodDeclaration(node) {
|
|
575
|
+
const receiver = node.childForFieldName("receiver");
|
|
576
|
+
const methodName = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
577
|
+
if (!receiver) return;
|
|
578
|
+
const typeName = receiverTypeName(receiver);
|
|
579
|
+
if (!typeName) return;
|
|
580
|
+
const typeId = resolveTypeTarget(typeName);
|
|
581
|
+
const methodId = entityId(sourceFile, `${typeName}.${methodName}`);
|
|
582
|
+
builder.addNode({ id: methodId, label: `method ${typeName}.${methodName}`, sourceFile, sourceLocation: lineOf(node) });
|
|
583
|
+
builder.addEdge({ source: typeId, target: methodId, relation: "method", confidence: "EXTRACTED" });
|
|
584
|
+
let methods = typeMethods.get(typeId);
|
|
585
|
+
if (!methods) {
|
|
586
|
+
methods = /* @__PURE__ */ new Map();
|
|
587
|
+
typeMethods.set(typeId, methods);
|
|
588
|
+
}
|
|
589
|
+
methods.set(methodName, methodId);
|
|
590
|
+
functionNodeMap.set(node.id, methodId);
|
|
591
|
+
const varName = receiverVarName(receiver);
|
|
592
|
+
if (varName) receiverInfo.set(node.id, { varName, typeId });
|
|
593
|
+
}
|
|
594
|
+
for (const child of namedChildren(root)) {
|
|
595
|
+
switch (child.type) {
|
|
596
|
+
case "import_declaration":
|
|
597
|
+
handleImportDeclaration(child);
|
|
598
|
+
break;
|
|
599
|
+
case "type_declaration":
|
|
600
|
+
handleTypeDeclaration(child);
|
|
601
|
+
break;
|
|
602
|
+
case "function_declaration":
|
|
603
|
+
handleFunctionDeclaration(child);
|
|
604
|
+
break;
|
|
605
|
+
case "method_declaration":
|
|
606
|
+
handleMethodDeclaration(child);
|
|
607
|
+
break;
|
|
608
|
+
default:
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function closestTrackedCaller(node) {
|
|
613
|
+
let current = node.parent;
|
|
614
|
+
while (current) {
|
|
615
|
+
const tracked = functionNodeMap.get(current.id);
|
|
616
|
+
if (tracked) return tracked;
|
|
617
|
+
current = current.parent;
|
|
618
|
+
}
|
|
619
|
+
return fileId;
|
|
620
|
+
}
|
|
621
|
+
function closestEnclosingReceiver(node) {
|
|
622
|
+
let current = node.parent;
|
|
623
|
+
while (current) {
|
|
624
|
+
if (current.type === "method_declaration") {
|
|
625
|
+
return receiverInfo.get(current.id);
|
|
626
|
+
}
|
|
627
|
+
current = current.parent;
|
|
628
|
+
}
|
|
629
|
+
return void 0;
|
|
630
|
+
}
|
|
631
|
+
for (const call of descendantsOfType(root, ["call_expression"])) {
|
|
632
|
+
const fn = call.childForFieldName("function");
|
|
633
|
+
if (!fn) continue;
|
|
634
|
+
let targetId = null;
|
|
635
|
+
let confidence = "EXTRACTED";
|
|
636
|
+
if (fn.type === "identifier") {
|
|
637
|
+
const sameFile = nameIndex.get(fn.text);
|
|
638
|
+
if (sameFile) targetId = sameFile;
|
|
639
|
+
} else if (fn.type === "selector_expression") {
|
|
640
|
+
const operand = fn.childForFieldName("operand");
|
|
641
|
+
const property = fn.childForFieldName("field")?.text;
|
|
642
|
+
if (operand && property && operand.type === "identifier") {
|
|
643
|
+
const receiver = closestEnclosingReceiver(call);
|
|
644
|
+
if (receiver && receiver.varName === operand.text) {
|
|
645
|
+
const methodId = typeMethods.get(receiver.typeId)?.get(property);
|
|
646
|
+
if (methodId) targetId = methodId;
|
|
647
|
+
} else {
|
|
648
|
+
const viaImport = importIndex.get(operand.text);
|
|
649
|
+
if (viaImport) {
|
|
650
|
+
targetId = viaImport.id;
|
|
651
|
+
confidence = "INFERRED";
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (!targetId) continue;
|
|
657
|
+
const callerId = closestTrackedCaller(call);
|
|
658
|
+
builder.addEdge({ source: callerId, target: targetId, relation: "calls", confidence });
|
|
659
|
+
}
|
|
660
|
+
return builder.build();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/extractors/java.ts
|
|
664
|
+
import * as fs4 from "fs/promises";
|
|
665
|
+
async function extractJava(filePath) {
|
|
666
|
+
const parser = await createParser("java");
|
|
667
|
+
const raw = await fs4.readFile(filePath);
|
|
668
|
+
const content = raw.toString("utf-8");
|
|
669
|
+
const tree = parser.parse(content);
|
|
670
|
+
if (!tree) {
|
|
671
|
+
throw new Error(`extractJava: parser produced no tree for ${filePath}`);
|
|
672
|
+
}
|
|
673
|
+
const root = tree.rootNode;
|
|
674
|
+
const sourceFile = toPosix(filePath);
|
|
675
|
+
const builder = new ExtractionBuilder();
|
|
676
|
+
const fileId = sourceFile;
|
|
677
|
+
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
678
|
+
const nameIndex = /* @__PURE__ */ new Map();
|
|
679
|
+
const functionNodeMap = /* @__PURE__ */ new Map();
|
|
680
|
+
const classMethods = /* @__PURE__ */ new Map();
|
|
681
|
+
const enclosingClassOf = /* @__PURE__ */ new Map();
|
|
682
|
+
const importIndex = /* @__PURE__ */ new Map();
|
|
683
|
+
function addModuleNode(ref) {
|
|
684
|
+
if (builder.hasNode(ref.id)) return;
|
|
685
|
+
builder.addNode({
|
|
686
|
+
id: ref.id,
|
|
687
|
+
label: ref.label,
|
|
688
|
+
sourceFile: ref.external ? "<external>" : ref.id,
|
|
689
|
+
sourceLocation: "L1"
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
function resolveHeritageTarget(name) {
|
|
693
|
+
const resolved = nameIndex.get(name);
|
|
694
|
+
if (resolved) return resolved;
|
|
695
|
+
const extId = externalId(name);
|
|
696
|
+
if (!builder.hasNode(extId)) {
|
|
697
|
+
builder.addNode({ id: extId, label: name, sourceFile: "<external>", sourceLocation: "L0" });
|
|
698
|
+
}
|
|
699
|
+
return extId;
|
|
700
|
+
}
|
|
701
|
+
function handleImportDeclaration(node) {
|
|
702
|
+
const hasWildcard = namedChildren(node).some((c) => c.type === "asterisk");
|
|
703
|
+
const scoped = namedChildren(node).find((c) => c.type === "scoped_identifier" || c.type === "identifier");
|
|
704
|
+
if (!scoped) return;
|
|
705
|
+
const fullPath = scoped.text;
|
|
706
|
+
const ref = resolveModuleRef(sourceFile, fullPath);
|
|
707
|
+
addModuleNode(ref);
|
|
708
|
+
if (hasWildcard) {
|
|
709
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const segments = fullPath.split(".");
|
|
713
|
+
const localName = segments[segments.length - 1];
|
|
714
|
+
if (localName) importIndex.set(localName, ref);
|
|
715
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports_from", confidence: "EXTRACTED" });
|
|
716
|
+
}
|
|
717
|
+
function handleMethodDeclaration(classNode, classId, className, member) {
|
|
718
|
+
const methodName = member.childForFieldName("name")?.text ?? "(anonymous)";
|
|
719
|
+
const methodId = entityId(sourceFile, `${className}.${methodName}`);
|
|
720
|
+
builder.addNode({
|
|
721
|
+
id: methodId,
|
|
722
|
+
label: `method ${className}.${methodName}`,
|
|
723
|
+
sourceFile,
|
|
724
|
+
sourceLocation: lineOf(member)
|
|
725
|
+
});
|
|
726
|
+
builder.addEdge({ source: classId, target: methodId, relation: "method", confidence: "EXTRACTED" });
|
|
727
|
+
let methods = classMethods.get(classNode.id);
|
|
728
|
+
if (!methods) {
|
|
729
|
+
methods = /* @__PURE__ */ new Map();
|
|
730
|
+
classMethods.set(classNode.id, methods);
|
|
731
|
+
}
|
|
732
|
+
methods.set(methodName, methodId);
|
|
733
|
+
functionNodeMap.set(member.id, methodId);
|
|
734
|
+
enclosingClassOf.set(member.id, classNode.id);
|
|
735
|
+
}
|
|
736
|
+
function handleClassDeclaration(node) {
|
|
737
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
738
|
+
const classId = entityId(sourceFile, name);
|
|
739
|
+
builder.addNode({ id: classId, label: `class ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
740
|
+
builder.addEdge({ source: fileId, target: classId, relation: "contains", confidence: "EXTRACTED" });
|
|
741
|
+
nameIndex.set(name, classId);
|
|
742
|
+
functionNodeMap.set(node.id, classId);
|
|
743
|
+
classMethods.set(node.id, /* @__PURE__ */ new Map());
|
|
744
|
+
const superclass = node.childForFieldName("superclass");
|
|
745
|
+
if (superclass) {
|
|
746
|
+
const base = namedChildren(superclass)[0];
|
|
747
|
+
if (base) {
|
|
748
|
+
builder.addEdge({
|
|
749
|
+
source: classId,
|
|
750
|
+
target: resolveHeritageTarget(base.text),
|
|
751
|
+
relation: "inherits",
|
|
752
|
+
confidence: "EXTRACTED"
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const interfaces = node.childForFieldName("interfaces");
|
|
757
|
+
if (interfaces) {
|
|
758
|
+
const typeList = namedChildren(interfaces).find((c) => c.type === "type_list");
|
|
759
|
+
for (const iface of typeList ? namedChildren(typeList) : []) {
|
|
760
|
+
builder.addEdge({
|
|
761
|
+
source: classId,
|
|
762
|
+
target: resolveHeritageTarget(iface.text),
|
|
763
|
+
relation: "implements",
|
|
764
|
+
confidence: "EXTRACTED"
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const body = node.childForFieldName("body");
|
|
769
|
+
if (body) {
|
|
770
|
+
for (const member of namedChildren(body)) {
|
|
771
|
+
if (member.type !== "method_declaration") continue;
|
|
772
|
+
handleMethodDeclaration(node, classId, name, member);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function handleInterfaceDeclaration(node) {
|
|
777
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
778
|
+
const id = entityId(sourceFile, name);
|
|
779
|
+
builder.addNode({ id, label: `interface ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
780
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
781
|
+
nameIndex.set(name, id);
|
|
782
|
+
const extendsInterfaces = namedChildren(node).find((c) => c.type === "extends_interfaces");
|
|
783
|
+
if (extendsInterfaces) {
|
|
784
|
+
const typeList = namedChildren(extendsInterfaces).find((c) => c.type === "type_list");
|
|
785
|
+
for (const parent of typeList ? namedChildren(typeList) : []) {
|
|
786
|
+
builder.addEdge({
|
|
787
|
+
source: id,
|
|
788
|
+
target: resolveHeritageTarget(parent.text),
|
|
789
|
+
relation: "inherits",
|
|
790
|
+
confidence: "EXTRACTED"
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
for (const child of namedChildren(root)) {
|
|
796
|
+
switch (child.type) {
|
|
797
|
+
case "import_declaration":
|
|
798
|
+
handleImportDeclaration(child);
|
|
799
|
+
break;
|
|
800
|
+
case "class_declaration":
|
|
801
|
+
handleClassDeclaration(child);
|
|
802
|
+
break;
|
|
803
|
+
case "interface_declaration":
|
|
804
|
+
handleInterfaceDeclaration(child);
|
|
805
|
+
break;
|
|
806
|
+
default:
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function closestTrackedCaller(node) {
|
|
811
|
+
let current = node.parent;
|
|
812
|
+
while (current) {
|
|
813
|
+
const tracked = functionNodeMap.get(current.id);
|
|
814
|
+
if (tracked) return tracked;
|
|
815
|
+
current = current.parent;
|
|
816
|
+
}
|
|
817
|
+
return fileId;
|
|
818
|
+
}
|
|
819
|
+
function closestEnclosingClassMethods(node) {
|
|
820
|
+
let current = node.parent;
|
|
821
|
+
while (current) {
|
|
822
|
+
if (current.type === "method_declaration") {
|
|
823
|
+
const classNodeId = enclosingClassOf.get(current.id);
|
|
824
|
+
if (classNodeId !== void 0) return classMethods.get(classNodeId);
|
|
825
|
+
}
|
|
826
|
+
current = current.parent;
|
|
827
|
+
}
|
|
828
|
+
return void 0;
|
|
829
|
+
}
|
|
830
|
+
for (const call of descendantsOfType(root, ["method_invocation"])) {
|
|
831
|
+
const name = call.childForFieldName("name")?.text;
|
|
832
|
+
if (!name) continue;
|
|
833
|
+
const object = call.childForFieldName("object");
|
|
834
|
+
let targetId = null;
|
|
835
|
+
let confidence = "EXTRACTED";
|
|
836
|
+
if (!object) {
|
|
837
|
+
const methodId = closestEnclosingClassMethods(call)?.get(name);
|
|
838
|
+
if (methodId) {
|
|
839
|
+
targetId = methodId;
|
|
840
|
+
} else {
|
|
841
|
+
const viaImport = importIndex.get(name);
|
|
842
|
+
if (viaImport) {
|
|
843
|
+
targetId = viaImport.id;
|
|
844
|
+
confidence = "INFERRED";
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} else if (object.type === "this") {
|
|
848
|
+
const methodId = closestEnclosingClassMethods(call)?.get(name);
|
|
849
|
+
if (methodId) targetId = methodId;
|
|
850
|
+
} else if (object.type === "identifier") {
|
|
851
|
+
const viaImport = importIndex.get(object.text);
|
|
852
|
+
if (viaImport) {
|
|
853
|
+
targetId = viaImport.id;
|
|
854
|
+
confidence = "INFERRED";
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (!targetId) continue;
|
|
858
|
+
const callerId = closestTrackedCaller(call);
|
|
859
|
+
builder.addEdge({ source: callerId, target: targetId, relation: "calls", confidence });
|
|
860
|
+
}
|
|
861
|
+
return builder.build();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/extractors/python.ts
|
|
865
|
+
import * as fs5 from "fs/promises";
|
|
866
|
+
async function extractPython(filePath) {
|
|
867
|
+
const parser = await createParser("python");
|
|
868
|
+
const raw = await fs5.readFile(filePath);
|
|
869
|
+
const content = raw.toString("utf-8");
|
|
870
|
+
const tree = parser.parse(content);
|
|
871
|
+
if (!tree) {
|
|
872
|
+
throw new Error(`extractPython: parser produced no tree for ${filePath}`);
|
|
873
|
+
}
|
|
874
|
+
const root = tree.rootNode;
|
|
875
|
+
const sourceFile = toPosix(filePath);
|
|
876
|
+
const builder = new ExtractionBuilder();
|
|
877
|
+
const fileId = sourceFile;
|
|
878
|
+
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
879
|
+
const nameIndex = /* @__PURE__ */ new Map();
|
|
880
|
+
const functionNodeMap = /* @__PURE__ */ new Map();
|
|
881
|
+
const classMethods = /* @__PURE__ */ new Map();
|
|
882
|
+
const enclosingClassOf = /* @__PURE__ */ new Map();
|
|
883
|
+
const importIndex = /* @__PURE__ */ new Map();
|
|
884
|
+
function addModuleNode(ref) {
|
|
885
|
+
if (builder.hasNode(ref.id)) return;
|
|
886
|
+
builder.addNode({
|
|
887
|
+
id: ref.id,
|
|
888
|
+
label: ref.label,
|
|
889
|
+
sourceFile: ref.external ? "<external>" : ref.id,
|
|
890
|
+
sourceLocation: "L1"
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function resolveHeritageTarget(name) {
|
|
894
|
+
const resolved = nameIndex.get(name);
|
|
895
|
+
if (resolved) return resolved;
|
|
896
|
+
const extId = externalId(name);
|
|
897
|
+
if (!builder.hasNode(extId)) {
|
|
898
|
+
builder.addNode({ id: extId, label: name, sourceFile: "<external>", sourceLocation: "L0" });
|
|
899
|
+
}
|
|
900
|
+
return extId;
|
|
901
|
+
}
|
|
902
|
+
function unwrapDecorated(node) {
|
|
903
|
+
if (node.type !== "decorated_definition") return node;
|
|
904
|
+
return namedChildren(node).find((c) => c.type === "function_definition" || c.type === "class_definition") ?? null;
|
|
905
|
+
}
|
|
906
|
+
function relativeSpecifier(relImport) {
|
|
907
|
+
let dots = 0;
|
|
908
|
+
for (const ch of relImport.text) {
|
|
909
|
+
if (ch !== ".") break;
|
|
910
|
+
dots++;
|
|
911
|
+
}
|
|
912
|
+
const dotted = namedChildren(relImport).find((c) => c.type === "dotted_name");
|
|
913
|
+
const up = dots - 1;
|
|
914
|
+
let base = up === 0 ? "." : Array.from({ length: up }, () => "..").join("/");
|
|
915
|
+
if (dotted) {
|
|
916
|
+
base = `${base}/${dotted.text.split(".").join("/")}`;
|
|
917
|
+
}
|
|
918
|
+
return base;
|
|
919
|
+
}
|
|
920
|
+
function moduleSpecifierOf(node) {
|
|
921
|
+
if (node.type === "relative_import") return relativeSpecifier(node);
|
|
922
|
+
return node.text;
|
|
923
|
+
}
|
|
924
|
+
function handleFunctionDefinition(node) {
|
|
925
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
926
|
+
const id = entityId(sourceFile, name);
|
|
927
|
+
builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
928
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
929
|
+
nameIndex.set(name, id);
|
|
930
|
+
functionNodeMap.set(node.id, id);
|
|
931
|
+
}
|
|
932
|
+
function handleClassDefinition(node) {
|
|
933
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
934
|
+
const classId = entityId(sourceFile, name);
|
|
935
|
+
builder.addNode({ id: classId, label: `class ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
936
|
+
builder.addEdge({ source: fileId, target: classId, relation: "contains", confidence: "EXTRACTED" });
|
|
937
|
+
nameIndex.set(name, classId);
|
|
938
|
+
functionNodeMap.set(node.id, classId);
|
|
939
|
+
const superclasses = node.childForFieldName("superclasses");
|
|
940
|
+
if (superclasses) {
|
|
941
|
+
for (const base of namedChildren(superclasses)) {
|
|
942
|
+
if (base.type !== "identifier" && base.type !== "attribute") continue;
|
|
943
|
+
builder.addEdge({
|
|
944
|
+
source: classId,
|
|
945
|
+
target: resolveHeritageTarget(base.text),
|
|
946
|
+
relation: "inherits",
|
|
947
|
+
confidence: "EXTRACTED"
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const methods = /* @__PURE__ */ new Map();
|
|
952
|
+
classMethods.set(node.id, methods);
|
|
953
|
+
const body = node.childForFieldName("body");
|
|
954
|
+
if (body) {
|
|
955
|
+
for (const rawMember of namedChildren(body)) {
|
|
956
|
+
const member = unwrapDecorated(rawMember);
|
|
957
|
+
if (!member || member.type !== "function_definition") continue;
|
|
958
|
+
const methodName = member.childForFieldName("name")?.text ?? "(anonymous)";
|
|
959
|
+
const methodId = entityId(sourceFile, `${name}.${methodName}`);
|
|
960
|
+
builder.addNode({
|
|
961
|
+
id: methodId,
|
|
962
|
+
label: `method ${name}.${methodName}`,
|
|
963
|
+
sourceFile,
|
|
964
|
+
sourceLocation: lineOf(member)
|
|
965
|
+
});
|
|
966
|
+
builder.addEdge({ source: classId, target: methodId, relation: "method", confidence: "EXTRACTED" });
|
|
967
|
+
methods.set(methodName, methodId);
|
|
968
|
+
functionNodeMap.set(member.id, methodId);
|
|
969
|
+
enclosingClassOf.set(member.id, node.id);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function handleImportStatement(node) {
|
|
974
|
+
for (const item of namedChildren(node)) {
|
|
975
|
+
if (item.type === "dotted_name") {
|
|
976
|
+
const ref = resolveModuleRef(sourceFile, item.text);
|
|
977
|
+
addModuleNode(ref);
|
|
978
|
+
const bindingName = namedChildren(item)[0]?.text ?? item.text;
|
|
979
|
+
importIndex.set(bindingName, ref);
|
|
980
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
981
|
+
} else if (item.type === "aliased_import") {
|
|
982
|
+
const dotted = item.childForFieldName("name");
|
|
983
|
+
const alias = item.childForFieldName("alias")?.text;
|
|
984
|
+
if (!dotted || !alias) continue;
|
|
985
|
+
const ref = resolveModuleRef(sourceFile, dotted.text);
|
|
986
|
+
addModuleNode(ref);
|
|
987
|
+
importIndex.set(alias, ref);
|
|
988
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function handleImportFromStatement(node) {
|
|
993
|
+
const moduleNode = node.childForFieldName("module_name");
|
|
994
|
+
if (!moduleNode) return;
|
|
995
|
+
const ref = resolveModuleRef(sourceFile, moduleSpecifierOf(moduleNode));
|
|
996
|
+
addModuleNode(ref);
|
|
997
|
+
let sawItem = false;
|
|
998
|
+
for (const child of namedChildren(node)) {
|
|
999
|
+
if (child.id === moduleNode.id) continue;
|
|
1000
|
+
if (child.type === "dotted_name") {
|
|
1001
|
+
sawItem = true;
|
|
1002
|
+
importIndex.set(child.text, ref);
|
|
1003
|
+
} else if (child.type === "aliased_import") {
|
|
1004
|
+
sawItem = true;
|
|
1005
|
+
const alias = child.childForFieldName("alias")?.text;
|
|
1006
|
+
const name = child.childForFieldName("name")?.text;
|
|
1007
|
+
if (alias) importIndex.set(alias, ref);
|
|
1008
|
+
else if (name) importIndex.set(name, ref);
|
|
1009
|
+
} else if (child.type === "wildcard_import") {
|
|
1010
|
+
sawItem = true;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (sawItem) {
|
|
1014
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports_from", confidence: "EXTRACTED" });
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
for (const rawChild of namedChildren(root)) {
|
|
1018
|
+
if (rawChild.type === "import_statement") {
|
|
1019
|
+
handleImportStatement(rawChild);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
if (rawChild.type === "import_from_statement") {
|
|
1023
|
+
handleImportFromStatement(rawChild);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const effective = unwrapDecorated(rawChild);
|
|
1027
|
+
if (!effective) continue;
|
|
1028
|
+
switch (effective.type) {
|
|
1029
|
+
case "function_definition":
|
|
1030
|
+
handleFunctionDefinition(effective);
|
|
1031
|
+
break;
|
|
1032
|
+
case "class_definition":
|
|
1033
|
+
handleClassDefinition(effective);
|
|
1034
|
+
break;
|
|
1035
|
+
default:
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function closestTrackedCaller(node) {
|
|
1040
|
+
let current = node.parent;
|
|
1041
|
+
while (current) {
|
|
1042
|
+
const tracked = functionNodeMap.get(current.id);
|
|
1043
|
+
if (tracked) return tracked;
|
|
1044
|
+
current = current.parent;
|
|
1045
|
+
}
|
|
1046
|
+
return fileId;
|
|
1047
|
+
}
|
|
1048
|
+
function closestEnclosingClassMethods(node) {
|
|
1049
|
+
let current = node.parent;
|
|
1050
|
+
while (current) {
|
|
1051
|
+
if (current.type === "function_definition") {
|
|
1052
|
+
const classNodeId = enclosingClassOf.get(current.id);
|
|
1053
|
+
if (classNodeId !== void 0) return classMethods.get(classNodeId);
|
|
1054
|
+
}
|
|
1055
|
+
current = current.parent;
|
|
1056
|
+
}
|
|
1057
|
+
return void 0;
|
|
1058
|
+
}
|
|
1059
|
+
for (const call of descendantsOfType(root, ["call"])) {
|
|
1060
|
+
const fn = call.childForFieldName("function");
|
|
1061
|
+
if (!fn) continue;
|
|
1062
|
+
let targetId = null;
|
|
1063
|
+
let confidence = "EXTRACTED";
|
|
1064
|
+
if (fn.type === "identifier") {
|
|
1065
|
+
const name = fn.text;
|
|
1066
|
+
const sameFile = nameIndex.get(name);
|
|
1067
|
+
if (sameFile) {
|
|
1068
|
+
targetId = sameFile;
|
|
1069
|
+
} else {
|
|
1070
|
+
const viaImport = importIndex.get(name);
|
|
1071
|
+
if (viaImport) {
|
|
1072
|
+
targetId = viaImport.id;
|
|
1073
|
+
confidence = "INFERRED";
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
} else if (fn.type === "attribute") {
|
|
1077
|
+
const object = fn.childForFieldName("object");
|
|
1078
|
+
const property = fn.childForFieldName("attribute")?.text;
|
|
1079
|
+
if (object && property) {
|
|
1080
|
+
if (object.type === "identifier" && object.text === "self") {
|
|
1081
|
+
const methodId = closestEnclosingClassMethods(call)?.get(property);
|
|
1082
|
+
if (methodId) targetId = methodId;
|
|
1083
|
+
} else if (object.type === "identifier") {
|
|
1084
|
+
const viaImport = importIndex.get(object.text);
|
|
1085
|
+
if (viaImport) {
|
|
1086
|
+
targetId = viaImport.id;
|
|
1087
|
+
confidence = "INFERRED";
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (!targetId) continue;
|
|
1093
|
+
const callerId = closestTrackedCaller(call);
|
|
1094
|
+
builder.addEdge({ source: callerId, target: targetId, relation: "calls", confidence });
|
|
1095
|
+
}
|
|
1096
|
+
return builder.build();
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/extractors/ruby.ts
|
|
1100
|
+
import * as fs6 from "fs/promises";
|
|
1101
|
+
function conventionalConstantName(specifier) {
|
|
1102
|
+
const lastSegment = specifier.split("/").filter(Boolean).pop() ?? specifier;
|
|
1103
|
+
return lastSegment.split("_").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
1104
|
+
}
|
|
1105
|
+
function stringLiteralContent(node) {
|
|
1106
|
+
if (node.type !== "string") return null;
|
|
1107
|
+
const content = namedChildren(node).find((c) => c.type === "string_content");
|
|
1108
|
+
return content ? content.text : "";
|
|
1109
|
+
}
|
|
1110
|
+
async function extractRuby(filePath) {
|
|
1111
|
+
const parser = await createParser("ruby");
|
|
1112
|
+
const raw = await fs6.readFile(filePath);
|
|
1113
|
+
const content = raw.toString("utf-8");
|
|
1114
|
+
const tree = parser.parse(content);
|
|
1115
|
+
if (!tree) {
|
|
1116
|
+
throw new Error(`extractRuby: parser produced no tree for ${filePath}`);
|
|
1117
|
+
}
|
|
1118
|
+
const root = tree.rootNode;
|
|
1119
|
+
const sourceFile = toPosix(filePath);
|
|
1120
|
+
const builder = new ExtractionBuilder();
|
|
1121
|
+
const fileId = sourceFile;
|
|
1122
|
+
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
1123
|
+
const nameIndex = /* @__PURE__ */ new Map();
|
|
1124
|
+
const functionNodeMap = /* @__PURE__ */ new Map();
|
|
1125
|
+
const typeMethods = /* @__PURE__ */ new Map();
|
|
1126
|
+
const enclosingTypeOf = /* @__PURE__ */ new Map();
|
|
1127
|
+
const importIndex = /* @__PURE__ */ new Map();
|
|
1128
|
+
function addModuleNode(ref) {
|
|
1129
|
+
if (builder.hasNode(ref.id)) return;
|
|
1130
|
+
builder.addNode({
|
|
1131
|
+
id: ref.id,
|
|
1132
|
+
label: ref.label,
|
|
1133
|
+
sourceFile: ref.external ? "<external>" : ref.id,
|
|
1134
|
+
sourceLocation: "L1"
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
function resolveHeritageTarget(name) {
|
|
1138
|
+
const resolved = nameIndex.get(name);
|
|
1139
|
+
if (resolved) return resolved;
|
|
1140
|
+
const extId = externalId(name);
|
|
1141
|
+
if (!builder.hasNode(extId)) {
|
|
1142
|
+
builder.addNode({ id: extId, label: name, sourceFile: "<external>", sourceLocation: "L0" });
|
|
1143
|
+
}
|
|
1144
|
+
return extId;
|
|
1145
|
+
}
|
|
1146
|
+
function handleTopLevelMethod(node) {
|
|
1147
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1148
|
+
const id = entityId(sourceFile, name);
|
|
1149
|
+
builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
1150
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1151
|
+
nameIndex.set(name, id);
|
|
1152
|
+
functionNodeMap.set(node.id, id);
|
|
1153
|
+
}
|
|
1154
|
+
function handleClassOrModule(node, kind) {
|
|
1155
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1156
|
+
const id = entityId(sourceFile, name);
|
|
1157
|
+
builder.addNode({ id, label: `${kind} ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
1158
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1159
|
+
nameIndex.set(name, id);
|
|
1160
|
+
typeMethods.set(id, /* @__PURE__ */ new Map());
|
|
1161
|
+
if (kind === "class") {
|
|
1162
|
+
const superclass = node.childForFieldName("superclass");
|
|
1163
|
+
const base = superclass ? namedChildren(superclass)[0] : void 0;
|
|
1164
|
+
if (base) {
|
|
1165
|
+
builder.addEdge({
|
|
1166
|
+
source: id,
|
|
1167
|
+
target: resolveHeritageTarget(base.text),
|
|
1168
|
+
relation: "inherits",
|
|
1169
|
+
confidence: "EXTRACTED"
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const body = node.childForFieldName("body");
|
|
1174
|
+
if (!body) return;
|
|
1175
|
+
const methods = typeMethods.get(id);
|
|
1176
|
+
for (const member of namedChildren(body)) {
|
|
1177
|
+
if (member.type === "method") {
|
|
1178
|
+
const methodName = member.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1179
|
+
const methodId = entityId(sourceFile, `${name}.${methodName}`);
|
|
1180
|
+
builder.addNode({
|
|
1181
|
+
id: methodId,
|
|
1182
|
+
label: `method ${name}.${methodName}`,
|
|
1183
|
+
sourceFile,
|
|
1184
|
+
sourceLocation: lineOf(member)
|
|
1185
|
+
});
|
|
1186
|
+
builder.addEdge({ source: id, target: methodId, relation: "method", confidence: "EXTRACTED" });
|
|
1187
|
+
methods?.set(methodName, methodId);
|
|
1188
|
+
functionNodeMap.set(member.id, methodId);
|
|
1189
|
+
enclosingTypeOf.set(member.id, id);
|
|
1190
|
+
} else if (member.type === "call") {
|
|
1191
|
+
const methodField = member.childForFieldName("method")?.text;
|
|
1192
|
+
if (methodField !== "include" && methodField !== "extend" && methodField !== "prepend") continue;
|
|
1193
|
+
const args = member.childForFieldName("arguments");
|
|
1194
|
+
for (const arg of args ? namedChildren(args) : []) {
|
|
1195
|
+
if (arg.type !== "constant" && arg.type !== "scoped_constant") continue;
|
|
1196
|
+
builder.addEdge({
|
|
1197
|
+
source: id,
|
|
1198
|
+
target: resolveHeritageTarget(arg.text),
|
|
1199
|
+
relation: "mixes_in",
|
|
1200
|
+
confidence: "EXTRACTED"
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
for (const child of namedChildren(root)) {
|
|
1207
|
+
switch (child.type) {
|
|
1208
|
+
case "method":
|
|
1209
|
+
handleTopLevelMethod(child);
|
|
1210
|
+
break;
|
|
1211
|
+
case "class":
|
|
1212
|
+
handleClassOrModule(child, "class");
|
|
1213
|
+
break;
|
|
1214
|
+
case "module":
|
|
1215
|
+
handleClassOrModule(child, "module");
|
|
1216
|
+
break;
|
|
1217
|
+
default:
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function closestTrackedCaller(node) {
|
|
1222
|
+
let current = node.parent;
|
|
1223
|
+
while (current) {
|
|
1224
|
+
const tracked = functionNodeMap.get(current.id);
|
|
1225
|
+
if (tracked) return tracked;
|
|
1226
|
+
current = current.parent;
|
|
1227
|
+
}
|
|
1228
|
+
return fileId;
|
|
1229
|
+
}
|
|
1230
|
+
function closestEnclosingTypeMethods(node) {
|
|
1231
|
+
let current = node.parent;
|
|
1232
|
+
while (current) {
|
|
1233
|
+
if (current.type === "method") {
|
|
1234
|
+
const typeId = enclosingTypeOf.get(current.id);
|
|
1235
|
+
if (typeId !== void 0) return typeMethods.get(typeId);
|
|
1236
|
+
}
|
|
1237
|
+
current = current.parent;
|
|
1238
|
+
}
|
|
1239
|
+
return void 0;
|
|
1240
|
+
}
|
|
1241
|
+
const allCalls = descendantsOfType(root, ["call"]);
|
|
1242
|
+
const requireCallIds = /* @__PURE__ */ new Set();
|
|
1243
|
+
for (const call of allCalls) {
|
|
1244
|
+
const methodName = call.childForFieldName("method")?.text;
|
|
1245
|
+
if (methodName !== "require" && methodName !== "require_relative") continue;
|
|
1246
|
+
const args = call.childForFieldName("arguments");
|
|
1247
|
+
const soleArg = args ? namedChildren(args) : [];
|
|
1248
|
+
if (soleArg.length !== 1) continue;
|
|
1249
|
+
const literal = stringLiteralContent(soleArg[0]);
|
|
1250
|
+
if (literal === null) continue;
|
|
1251
|
+
requireCallIds.add(call.id);
|
|
1252
|
+
const ref = resolveModuleRef(sourceFile, literal);
|
|
1253
|
+
addModuleNode(ref);
|
|
1254
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
1255
|
+
importIndex.set(conventionalConstantName(literal), ref);
|
|
1256
|
+
}
|
|
1257
|
+
for (const call of allCalls) {
|
|
1258
|
+
if (requireCallIds.has(call.id)) continue;
|
|
1259
|
+
const methodName = call.childForFieldName("method")?.text;
|
|
1260
|
+
if (!methodName) continue;
|
|
1261
|
+
const receiver = call.childForFieldName("receiver");
|
|
1262
|
+
let targetId = null;
|
|
1263
|
+
let confidence = "EXTRACTED";
|
|
1264
|
+
if (!receiver) {
|
|
1265
|
+
const viaEnclosingType = closestEnclosingTypeMethods(call)?.get(methodName);
|
|
1266
|
+
if (viaEnclosingType) {
|
|
1267
|
+
targetId = viaEnclosingType;
|
|
1268
|
+
} else {
|
|
1269
|
+
const sameFile = nameIndex.get(methodName);
|
|
1270
|
+
if (sameFile) targetId = sameFile;
|
|
1271
|
+
}
|
|
1272
|
+
} else if (receiver.type === "self") {
|
|
1273
|
+
const methodId = closestEnclosingTypeMethods(call)?.get(methodName);
|
|
1274
|
+
if (methodId) targetId = methodId;
|
|
1275
|
+
} else if (receiver.type === "constant" || receiver.type === "scoped_constant") {
|
|
1276
|
+
const sameFileTypeId = nameIndex.get(receiver.text);
|
|
1277
|
+
const methodId = sameFileTypeId ? typeMethods.get(sameFileTypeId)?.get(methodName) : void 0;
|
|
1278
|
+
if (methodId) {
|
|
1279
|
+
targetId = methodId;
|
|
1280
|
+
} else {
|
|
1281
|
+
const viaImport = importIndex.get(receiver.text);
|
|
1282
|
+
if (viaImport) {
|
|
1283
|
+
targetId = viaImport.id;
|
|
1284
|
+
confidence = "INFERRED";
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (!targetId) continue;
|
|
1289
|
+
const callerId = closestTrackedCaller(call);
|
|
1290
|
+
builder.addEdge({ source: callerId, target: targetId, relation: "calls", confidence });
|
|
1291
|
+
}
|
|
1292
|
+
return builder.build();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/extractors/rust.ts
|
|
1296
|
+
import * as fs7 from "fs/promises";
|
|
1297
|
+
function convertUsePath(segments) {
|
|
1298
|
+
const first = segments[0];
|
|
1299
|
+
if (first === "crate" || first === "self") {
|
|
1300
|
+
const rest = segments.slice(1);
|
|
1301
|
+
return rest.length ? `./${rest.join("/")}` : ".";
|
|
1302
|
+
}
|
|
1303
|
+
if (first === "super") {
|
|
1304
|
+
let i = 0;
|
|
1305
|
+
while (segments[i] === "super") i++;
|
|
1306
|
+
const ups = Array.from({ length: i }, () => "..").join("/");
|
|
1307
|
+
const rest = segments.slice(i);
|
|
1308
|
+
return rest.length ? `${ups}/${rest.join("/")}` : ups;
|
|
1309
|
+
}
|
|
1310
|
+
return segments.join("::");
|
|
1311
|
+
}
|
|
1312
|
+
async function extractRust(filePath) {
|
|
1313
|
+
const parser = await createParser("rust");
|
|
1314
|
+
const raw = await fs7.readFile(filePath);
|
|
1315
|
+
const content = raw.toString("utf-8");
|
|
1316
|
+
const tree = parser.parse(content);
|
|
1317
|
+
if (!tree) {
|
|
1318
|
+
throw new Error(`extractRust: parser produced no tree for ${filePath}`);
|
|
1319
|
+
}
|
|
1320
|
+
const root = tree.rootNode;
|
|
1321
|
+
const sourceFile = toPosix(filePath);
|
|
1322
|
+
const builder = new ExtractionBuilder();
|
|
1323
|
+
const fileId = sourceFile;
|
|
1324
|
+
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
1325
|
+
const nameIndex = /* @__PURE__ */ new Map();
|
|
1326
|
+
const functionNodeMap = /* @__PURE__ */ new Map();
|
|
1327
|
+
const typeMethods = /* @__PURE__ */ new Map();
|
|
1328
|
+
const enclosingTypeOf = /* @__PURE__ */ new Map();
|
|
1329
|
+
const importIndex = /* @__PURE__ */ new Map();
|
|
1330
|
+
function addModuleNode(ref) {
|
|
1331
|
+
if (builder.hasNode(ref.id)) return;
|
|
1332
|
+
builder.addNode({
|
|
1333
|
+
id: ref.id,
|
|
1334
|
+
label: ref.label,
|
|
1335
|
+
sourceFile: ref.external ? "<external>" : ref.id,
|
|
1336
|
+
sourceLocation: "L1"
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
function resolveTypeTarget(name) {
|
|
1340
|
+
const resolved = nameIndex.get(name);
|
|
1341
|
+
if (resolved) return resolved;
|
|
1342
|
+
const extId = externalId(name);
|
|
1343
|
+
if (!builder.hasNode(extId)) {
|
|
1344
|
+
builder.addNode({ id: extId, label: name, sourceFile: "<external>", sourceLocation: "L0" });
|
|
1345
|
+
}
|
|
1346
|
+
return extId;
|
|
1347
|
+
}
|
|
1348
|
+
function processUseArgument(node) {
|
|
1349
|
+
switch (node.type) {
|
|
1350
|
+
case "identifier": {
|
|
1351
|
+
const ref = resolveModuleRef(sourceFile, convertUsePath([node.text]));
|
|
1352
|
+
addModuleNode(ref);
|
|
1353
|
+
importIndex.set(node.text, ref);
|
|
1354
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
case "scoped_identifier": {
|
|
1358
|
+
const pathNode = node.childForFieldName("path");
|
|
1359
|
+
const nameNode = node.childForFieldName("name");
|
|
1360
|
+
if (!pathNode || !nameNode) break;
|
|
1361
|
+
const ref = resolveModuleRef(sourceFile, convertUsePath(pathNode.text.split("::")));
|
|
1362
|
+
addModuleNode(ref);
|
|
1363
|
+
importIndex.set(nameNode.text, ref);
|
|
1364
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports_from", confidence: "EXTRACTED" });
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
case "use_as_clause": {
|
|
1368
|
+
const pathNode = node.childForFieldName("path");
|
|
1369
|
+
const aliasNode = node.childForFieldName("alias");
|
|
1370
|
+
if (!pathNode || !aliasNode) break;
|
|
1371
|
+
const ref = resolveModuleRef(sourceFile, convertUsePath(pathNode.text.split("::")));
|
|
1372
|
+
addModuleNode(ref);
|
|
1373
|
+
importIndex.set(aliasNode.text, ref);
|
|
1374
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
case "scoped_use_list": {
|
|
1378
|
+
const pathNode = node.childForFieldName("path");
|
|
1379
|
+
const listNode = node.childForFieldName("list");
|
|
1380
|
+
if (!listNode) break;
|
|
1381
|
+
const prefixSegments = pathNode ? pathNode.text.split("::") : [];
|
|
1382
|
+
const ref = resolveModuleRef(sourceFile, convertUsePath(prefixSegments));
|
|
1383
|
+
addModuleNode(ref);
|
|
1384
|
+
let sawItem = false;
|
|
1385
|
+
for (const item of namedChildren(listNode)) {
|
|
1386
|
+
sawItem = true;
|
|
1387
|
+
if (item.type === "identifier") {
|
|
1388
|
+
importIndex.set(item.text, ref);
|
|
1389
|
+
} else if (item.type === "use_as_clause") {
|
|
1390
|
+
const alias = item.childForFieldName("alias")?.text;
|
|
1391
|
+
if (alias) importIndex.set(alias, ref);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (sawItem) {
|
|
1395
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports_from", confidence: "EXTRACTED" });
|
|
1396
|
+
}
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
default:
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function handleStructItem(node) {
|
|
1404
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1405
|
+
const id = entityId(sourceFile, name);
|
|
1406
|
+
builder.addNode({ id, label: `struct ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
1407
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1408
|
+
nameIndex.set(name, id);
|
|
1409
|
+
typeMethods.set(id, /* @__PURE__ */ new Map());
|
|
1410
|
+
}
|
|
1411
|
+
function handleTraitItem(node) {
|
|
1412
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1413
|
+
const id = entityId(sourceFile, name);
|
|
1414
|
+
builder.addNode({ id, label: `trait ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
1415
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1416
|
+
nameIndex.set(name, id);
|
|
1417
|
+
}
|
|
1418
|
+
function handleFunctionItem(node) {
|
|
1419
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1420
|
+
const id = entityId(sourceFile, name);
|
|
1421
|
+
builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
1422
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1423
|
+
nameIndex.set(name, id);
|
|
1424
|
+
functionNodeMap.set(node.id, id);
|
|
1425
|
+
}
|
|
1426
|
+
function handleImplItem(node) {
|
|
1427
|
+
const typeName = node.childForFieldName("type")?.text;
|
|
1428
|
+
if (!typeName) return;
|
|
1429
|
+
const typeId = resolveTypeTarget(typeName);
|
|
1430
|
+
const traitName = node.childForFieldName("trait")?.text;
|
|
1431
|
+
if (traitName) {
|
|
1432
|
+
builder.addEdge({
|
|
1433
|
+
source: typeId,
|
|
1434
|
+
target: resolveTypeTarget(traitName),
|
|
1435
|
+
relation: "implements",
|
|
1436
|
+
confidence: "EXTRACTED"
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
let methods = typeMethods.get(typeId);
|
|
1440
|
+
if (!methods) {
|
|
1441
|
+
methods = /* @__PURE__ */ new Map();
|
|
1442
|
+
typeMethods.set(typeId, methods);
|
|
1443
|
+
}
|
|
1444
|
+
const body = node.childForFieldName("body");
|
|
1445
|
+
if (!body) return;
|
|
1446
|
+
for (const member of namedChildren(body)) {
|
|
1447
|
+
if (member.type !== "function_item") continue;
|
|
1448
|
+
const methodName = member.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1449
|
+
const methodId = entityId(sourceFile, `${typeName}.${methodName}`);
|
|
1450
|
+
builder.addNode({
|
|
1451
|
+
id: methodId,
|
|
1452
|
+
label: `method ${typeName}.${methodName}`,
|
|
1453
|
+
sourceFile,
|
|
1454
|
+
sourceLocation: lineOf(member)
|
|
1455
|
+
});
|
|
1456
|
+
builder.addEdge({ source: typeId, target: methodId, relation: "method", confidence: "EXTRACTED" });
|
|
1457
|
+
methods.set(methodName, methodId);
|
|
1458
|
+
functionNodeMap.set(member.id, methodId);
|
|
1459
|
+
enclosingTypeOf.set(member.id, typeId);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const implNodes = [];
|
|
1463
|
+
for (const child of namedChildren(root)) {
|
|
1464
|
+
switch (child.type) {
|
|
1465
|
+
case "use_declaration": {
|
|
1466
|
+
const arg = child.childForFieldName("argument");
|
|
1467
|
+
if (arg) processUseArgument(arg);
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
case "struct_item":
|
|
1471
|
+
handleStructItem(child);
|
|
1472
|
+
break;
|
|
1473
|
+
case "trait_item":
|
|
1474
|
+
handleTraitItem(child);
|
|
1475
|
+
break;
|
|
1476
|
+
case "function_item":
|
|
1477
|
+
handleFunctionItem(child);
|
|
1478
|
+
break;
|
|
1479
|
+
case "impl_item":
|
|
1480
|
+
implNodes.push(child);
|
|
1481
|
+
break;
|
|
1482
|
+
default:
|
|
1483
|
+
break;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
for (const impl of implNodes) handleImplItem(impl);
|
|
1487
|
+
function closestTrackedCaller(node) {
|
|
1488
|
+
let current = node.parent;
|
|
1489
|
+
while (current) {
|
|
1490
|
+
const tracked = functionNodeMap.get(current.id);
|
|
1491
|
+
if (tracked) return tracked;
|
|
1492
|
+
current = current.parent;
|
|
1493
|
+
}
|
|
1494
|
+
return fileId;
|
|
1495
|
+
}
|
|
1496
|
+
function closestEnclosingTypeMethods(node) {
|
|
1497
|
+
let current = node.parent;
|
|
1498
|
+
while (current) {
|
|
1499
|
+
if (current.type === "function_item") {
|
|
1500
|
+
const typeId = enclosingTypeOf.get(current.id);
|
|
1501
|
+
if (typeId !== void 0) return typeMethods.get(typeId);
|
|
1502
|
+
}
|
|
1503
|
+
current = current.parent;
|
|
1504
|
+
}
|
|
1505
|
+
return void 0;
|
|
1506
|
+
}
|
|
1507
|
+
for (const call of descendantsOfType(root, ["call_expression"])) {
|
|
1508
|
+
const fn = call.childForFieldName("function");
|
|
1509
|
+
if (!fn) continue;
|
|
1510
|
+
let targetId = null;
|
|
1511
|
+
let confidence = "EXTRACTED";
|
|
1512
|
+
if (fn.type === "identifier") {
|
|
1513
|
+
const name = fn.text;
|
|
1514
|
+
const sameFile = nameIndex.get(name);
|
|
1515
|
+
if (sameFile) {
|
|
1516
|
+
targetId = sameFile;
|
|
1517
|
+
} else {
|
|
1518
|
+
const viaImport = importIndex.get(name);
|
|
1519
|
+
if (viaImport) {
|
|
1520
|
+
targetId = viaImport.id;
|
|
1521
|
+
confidence = "INFERRED";
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
} else if (fn.type === "field_expression") {
|
|
1525
|
+
const value = fn.childForFieldName("value");
|
|
1526
|
+
const field = fn.childForFieldName("field")?.text;
|
|
1527
|
+
if (value && field) {
|
|
1528
|
+
if (value.type === "self") {
|
|
1529
|
+
const methodId = closestEnclosingTypeMethods(call)?.get(field);
|
|
1530
|
+
if (methodId) targetId = methodId;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
} else if (fn.type === "scoped_identifier") {
|
|
1534
|
+
const path7 = fn.childForFieldName("path")?.text;
|
|
1535
|
+
const name = fn.childForFieldName("name")?.text;
|
|
1536
|
+
if (path7 && name) {
|
|
1537
|
+
const viaImport = importIndex.get(path7);
|
|
1538
|
+
if (viaImport) {
|
|
1539
|
+
targetId = viaImport.id;
|
|
1540
|
+
confidence = "INFERRED";
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (!targetId) continue;
|
|
1545
|
+
const callerId = closestTrackedCaller(call);
|
|
1546
|
+
builder.addEdge({ source: callerId, target: targetId, relation: "calls", confidence });
|
|
1547
|
+
}
|
|
1548
|
+
return builder.build();
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/extractors/typescript.ts
|
|
1552
|
+
import * as fs8 from "fs/promises";
|
|
1553
|
+
import * as path3 from "path";
|
|
1554
|
+
function grammarForExtension(ext) {
|
|
1555
|
+
const lower = ext.toLowerCase();
|
|
1556
|
+
if (lower === ".tsx") return "tsx";
|
|
1557
|
+
if (lower === ".jsx") return "javascript";
|
|
1558
|
+
if (lower === ".ts" || lower === ".mts" || lower === ".cts") return "typescript";
|
|
1559
|
+
return "javascript";
|
|
1560
|
+
}
|
|
1561
|
+
function stripQuotes2(text) {
|
|
1562
|
+
if (text.length >= 2) {
|
|
1563
|
+
const first = text[0];
|
|
1564
|
+
const last = text[text.length - 1];
|
|
1565
|
+
if ((first === '"' || first === "'" || first === "`") && first === last) {
|
|
1566
|
+
return text.slice(1, -1);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return text;
|
|
1570
|
+
}
|
|
1571
|
+
function firstStringArgument(call) {
|
|
1572
|
+
const args = call.childForFieldName("arguments");
|
|
1573
|
+
if (!args) return null;
|
|
1574
|
+
const first = namedChildren(args)[0];
|
|
1575
|
+
if (!first || first.type !== "string") return null;
|
|
1576
|
+
return stripQuotes2(first.text);
|
|
1577
|
+
}
|
|
1578
|
+
async function extractTypeScript(filePath) {
|
|
1579
|
+
const ext = path3.extname(filePath);
|
|
1580
|
+
const grammar = grammarForExtension(ext);
|
|
1581
|
+
const parser = await createParser(grammar);
|
|
1582
|
+
const raw = await fs8.readFile(filePath);
|
|
1583
|
+
const content = raw.toString("utf-8");
|
|
1584
|
+
const tree = parser.parse(content);
|
|
1585
|
+
if (!tree) {
|
|
1586
|
+
throw new Error(`extractTypeScript: parser produced no tree for ${filePath}`);
|
|
1587
|
+
}
|
|
1588
|
+
const root = tree.rootNode;
|
|
1589
|
+
const sourceFile = toPosix(filePath);
|
|
1590
|
+
const builder = new ExtractionBuilder();
|
|
1591
|
+
const fileId = sourceFile;
|
|
1592
|
+
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
1593
|
+
const nameIndex = /* @__PURE__ */ new Map();
|
|
1594
|
+
const functionNodeMap = /* @__PURE__ */ new Map();
|
|
1595
|
+
const classMethods = /* @__PURE__ */ new Map();
|
|
1596
|
+
const enclosingClassOf = /* @__PURE__ */ new Map();
|
|
1597
|
+
const importIndex = /* @__PURE__ */ new Map();
|
|
1598
|
+
function addModuleNode(ref) {
|
|
1599
|
+
if (builder.hasNode(ref.id)) return;
|
|
1600
|
+
builder.addNode({
|
|
1601
|
+
id: ref.id,
|
|
1602
|
+
label: ref.label,
|
|
1603
|
+
sourceFile: ref.external ? "<external>" : ref.id,
|
|
1604
|
+
sourceLocation: "L1"
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
function importTargetId(binding, property) {
|
|
1608
|
+
if (!binding.ref.external) {
|
|
1609
|
+
if (binding.kind === "named" && binding.importedName) {
|
|
1610
|
+
return entityId(binding.ref.id, binding.importedName);
|
|
1611
|
+
}
|
|
1612
|
+
if (binding.kind === "namespace" && property) {
|
|
1613
|
+
return entityId(binding.ref.id, property);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return binding.ref.id;
|
|
1617
|
+
}
|
|
1618
|
+
function resolveHeritageTarget(name) {
|
|
1619
|
+
const resolved = nameIndex.get(name);
|
|
1620
|
+
if (resolved) return resolved;
|
|
1621
|
+
const extId = externalId(name);
|
|
1622
|
+
if (!builder.hasNode(extId)) {
|
|
1623
|
+
builder.addNode({ id: extId, label: name, sourceFile: "<external>", sourceLocation: "L0" });
|
|
1624
|
+
}
|
|
1625
|
+
return extId;
|
|
1626
|
+
}
|
|
1627
|
+
function handleFunctionDeclaration(node) {
|
|
1628
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1629
|
+
const id = entityId(sourceFile, name);
|
|
1630
|
+
builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
1631
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1632
|
+
nameIndex.set(name, id);
|
|
1633
|
+
functionNodeMap.set(node.id, id);
|
|
1634
|
+
}
|
|
1635
|
+
function registerRequireBinding(nameNode, ref) {
|
|
1636
|
+
if (nameNode.type === "identifier") {
|
|
1637
|
+
importIndex.set(nameNode.text, { ref, kind: "namespace" });
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (nameNode.type === "object_pattern") {
|
|
1641
|
+
for (const prop of namedChildren(nameNode)) {
|
|
1642
|
+
if (prop.type === "shorthand_property_identifier_pattern") {
|
|
1643
|
+
importIndex.set(prop.text, { ref, importedName: prop.text, kind: "named" });
|
|
1644
|
+
} else if (prop.type === "pair_pattern") {
|
|
1645
|
+
const key = prop.childForFieldName("key")?.text;
|
|
1646
|
+
const value = prop.childForFieldName("value")?.text;
|
|
1647
|
+
if (key && value) importIndex.set(value, { ref, importedName: key, kind: "named" });
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
function handleTopLevelVariableFunctions(node) {
|
|
1653
|
+
for (const declarator of namedChildren(node)) {
|
|
1654
|
+
if (declarator.type !== "variable_declarator") continue;
|
|
1655
|
+
const value = declarator.childForFieldName("value");
|
|
1656
|
+
if (!value) continue;
|
|
1657
|
+
if (value.type === "arrow_function" || value.type === "function_expression") {
|
|
1658
|
+
const name = declarator.childForFieldName("name")?.text;
|
|
1659
|
+
if (!name) continue;
|
|
1660
|
+
const id = entityId(sourceFile, name);
|
|
1661
|
+
builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
|
|
1662
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1663
|
+
nameIndex.set(name, id);
|
|
1664
|
+
functionNodeMap.set(value.id, id);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
if (value.type === "call_expression") {
|
|
1668
|
+
const callee = value.childForFieldName("function");
|
|
1669
|
+
const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(value) : null;
|
|
1670
|
+
if (specifier) {
|
|
1671
|
+
const nameNode = declarator.childForFieldName("name");
|
|
1672
|
+
if (nameNode) {
|
|
1673
|
+
const ref = resolveModuleRef(sourceFile, specifier);
|
|
1674
|
+
addModuleNode(ref);
|
|
1675
|
+
registerRequireBinding(nameNode, ref);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
function handleInterfaceDeclaration(node) {
|
|
1682
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1683
|
+
const id = entityId(sourceFile, name);
|
|
1684
|
+
builder.addNode({ id, label: `interface ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
1685
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1686
|
+
nameIndex.set(name, id);
|
|
1687
|
+
}
|
|
1688
|
+
function handleClassDeclaration(node) {
|
|
1689
|
+
const name = node.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1690
|
+
const classId = entityId(sourceFile, name);
|
|
1691
|
+
builder.addNode({ id: classId, label: `class ${name}`, sourceFile, sourceLocation: lineOf(node) });
|
|
1692
|
+
builder.addEdge({ source: fileId, target: classId, relation: "contains", confidence: "EXTRACTED" });
|
|
1693
|
+
nameIndex.set(name, classId);
|
|
1694
|
+
functionNodeMap.set(node.id, classId);
|
|
1695
|
+
const heritage = namedChildren(node).find((c) => c.type === "class_heritage");
|
|
1696
|
+
if (heritage) {
|
|
1697
|
+
for (const clause of namedChildren(heritage)) {
|
|
1698
|
+
if (clause.type === "extends_clause") {
|
|
1699
|
+
const base = clause.childForFieldName("value")?.text;
|
|
1700
|
+
if (base) {
|
|
1701
|
+
builder.addEdge({
|
|
1702
|
+
source: classId,
|
|
1703
|
+
target: resolveHeritageTarget(base),
|
|
1704
|
+
relation: "inherits",
|
|
1705
|
+
confidence: "EXTRACTED"
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
} else if (clause.type === "implements_clause") {
|
|
1709
|
+
for (const iface of namedChildren(clause)) {
|
|
1710
|
+
builder.addEdge({
|
|
1711
|
+
source: classId,
|
|
1712
|
+
target: resolveHeritageTarget(iface.text),
|
|
1713
|
+
relation: "implements",
|
|
1714
|
+
confidence: "EXTRACTED"
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const methods = /* @__PURE__ */ new Map();
|
|
1721
|
+
classMethods.set(node.id, methods);
|
|
1722
|
+
const body = node.childForFieldName("body");
|
|
1723
|
+
if (body) {
|
|
1724
|
+
for (const member of namedChildren(body)) {
|
|
1725
|
+
if (member.type !== "method_definition") continue;
|
|
1726
|
+
const methodName = member.childForFieldName("name")?.text ?? "(anonymous)";
|
|
1727
|
+
const methodId = entityId(sourceFile, `${name}.${methodName}`);
|
|
1728
|
+
builder.addNode({
|
|
1729
|
+
id: methodId,
|
|
1730
|
+
label: `method ${name}.${methodName}`,
|
|
1731
|
+
sourceFile,
|
|
1732
|
+
sourceLocation: lineOf(member)
|
|
1733
|
+
});
|
|
1734
|
+
builder.addEdge({ source: classId, target: methodId, relation: "method", confidence: "EXTRACTED" });
|
|
1735
|
+
methods.set(methodName, methodId);
|
|
1736
|
+
functionNodeMap.set(member.id, methodId);
|
|
1737
|
+
enclosingClassOf.set(member.id, node.id);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
function handleImport(node) {
|
|
1742
|
+
const sourceNode = node.childForFieldName("source");
|
|
1743
|
+
if (!sourceNode) return;
|
|
1744
|
+
const ref = resolveModuleRef(sourceFile, stripQuotes2(sourceNode.text));
|
|
1745
|
+
addModuleNode(ref);
|
|
1746
|
+
const clause = namedChildren(node).find((c) => c.type === "import_clause");
|
|
1747
|
+
if (!clause) return;
|
|
1748
|
+
let sawNamed = false;
|
|
1749
|
+
let sawWhole = false;
|
|
1750
|
+
for (const child of namedChildren(clause)) {
|
|
1751
|
+
if (child.type === "named_imports") {
|
|
1752
|
+
sawNamed = true;
|
|
1753
|
+
for (const specifierNode of namedChildren(child)) {
|
|
1754
|
+
const importedName = specifierNode.childForFieldName("name")?.text;
|
|
1755
|
+
const localName = specifierNode.childForFieldName("alias")?.text ?? importedName;
|
|
1756
|
+
if (localName) importIndex.set(localName, { ref, importedName, kind: "named" });
|
|
1757
|
+
}
|
|
1758
|
+
} else if (child.type === "namespace_import") {
|
|
1759
|
+
sawWhole = true;
|
|
1760
|
+
const localName = namedChildren(child)[0]?.text;
|
|
1761
|
+
if (localName) importIndex.set(localName, { ref, kind: "namespace" });
|
|
1762
|
+
} else if (child.type === "identifier") {
|
|
1763
|
+
sawWhole = true;
|
|
1764
|
+
importIndex.set(child.text, { ref, kind: "default" });
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (sawNamed) {
|
|
1768
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports_from", confidence: "EXTRACTED" });
|
|
1769
|
+
}
|
|
1770
|
+
if (sawWhole) {
|
|
1771
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "imports", confidence: "EXTRACTED" });
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function handleReExport(node) {
|
|
1775
|
+
const sourceNode = node.childForFieldName("source");
|
|
1776
|
+
if (!sourceNode) return;
|
|
1777
|
+
const ref = resolveModuleRef(sourceFile, stripQuotes2(sourceNode.text));
|
|
1778
|
+
addModuleNode(ref);
|
|
1779
|
+
builder.addEdge({ source: fileId, target: ref.id, relation: "re_exports", confidence: "EXTRACTED" });
|
|
1780
|
+
}
|
|
1781
|
+
function unwrapExport(node) {
|
|
1782
|
+
if (node.type !== "export_statement") return node;
|
|
1783
|
+
return node.childForFieldName("declaration");
|
|
1784
|
+
}
|
|
1785
|
+
for (const child of namedChildren(root)) {
|
|
1786
|
+
if (child.type === "import_statement") {
|
|
1787
|
+
handleImport(child);
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
if (child.type === "export_statement" && child.childForFieldName("source")) {
|
|
1791
|
+
handleReExport(child);
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
const effective = unwrapExport(child);
|
|
1795
|
+
if (!effective) continue;
|
|
1796
|
+
switch (effective.type) {
|
|
1797
|
+
case "function_declaration":
|
|
1798
|
+
case "generator_function_declaration":
|
|
1799
|
+
handleFunctionDeclaration(effective);
|
|
1800
|
+
break;
|
|
1801
|
+
case "class_declaration":
|
|
1802
|
+
handleClassDeclaration(effective);
|
|
1803
|
+
break;
|
|
1804
|
+
case "interface_declaration":
|
|
1805
|
+
handleInterfaceDeclaration(effective);
|
|
1806
|
+
break;
|
|
1807
|
+
case "lexical_declaration":
|
|
1808
|
+
case "variable_declaration":
|
|
1809
|
+
handleTopLevelVariableFunctions(effective);
|
|
1810
|
+
break;
|
|
1811
|
+
default:
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function closestTrackedCaller(node) {
|
|
1816
|
+
let current = node.parent;
|
|
1817
|
+
while (current) {
|
|
1818
|
+
const tracked = functionNodeMap.get(current.id);
|
|
1819
|
+
if (tracked) return tracked;
|
|
1820
|
+
current = current.parent;
|
|
1821
|
+
}
|
|
1822
|
+
return fileId;
|
|
1823
|
+
}
|
|
1824
|
+
function closestEnclosingClassMethods(node) {
|
|
1825
|
+
let current = node.parent;
|
|
1826
|
+
while (current) {
|
|
1827
|
+
if (current.type === "method_definition") {
|
|
1828
|
+
const classNodeId = enclosingClassOf.get(current.id);
|
|
1829
|
+
if (classNodeId !== void 0) return classMethods.get(classNodeId);
|
|
1830
|
+
}
|
|
1831
|
+
current = current.parent;
|
|
1832
|
+
}
|
|
1833
|
+
return void 0;
|
|
1834
|
+
}
|
|
1835
|
+
for (const call of descendantsOfType(root, ["call_expression"])) {
|
|
1836
|
+
const fn = call.childForFieldName("function");
|
|
1837
|
+
if (!fn) continue;
|
|
1838
|
+
if (fn.type === "identifier" && fn.text === "require" || fn.type === "import") {
|
|
1839
|
+
const specifier = firstStringArgument(call);
|
|
1840
|
+
if (specifier) {
|
|
1841
|
+
const ref = resolveModuleRef(sourceFile, specifier);
|
|
1842
|
+
addModuleNode(ref);
|
|
1843
|
+
builder.addEdge({
|
|
1844
|
+
source: closestTrackedCaller(call),
|
|
1845
|
+
target: ref.id,
|
|
1846
|
+
relation: "imports",
|
|
1847
|
+
confidence: "EXTRACTED"
|
|
1848
|
+
});
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
let targetId = null;
|
|
1853
|
+
let confidence = "EXTRACTED";
|
|
1854
|
+
if (fn.type === "identifier") {
|
|
1855
|
+
const name = fn.text;
|
|
1856
|
+
const sameFile = nameIndex.get(name);
|
|
1857
|
+
if (sameFile) {
|
|
1858
|
+
targetId = sameFile;
|
|
1859
|
+
} else {
|
|
1860
|
+
const binding = importIndex.get(name);
|
|
1861
|
+
if (binding) {
|
|
1862
|
+
targetId = importTargetId(binding);
|
|
1863
|
+
confidence = "INFERRED";
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
} else if (fn.type === "member_expression") {
|
|
1867
|
+
const object = fn.childForFieldName("object");
|
|
1868
|
+
const property = fn.childForFieldName("property")?.text;
|
|
1869
|
+
if (object && property) {
|
|
1870
|
+
if (object.type === "this") {
|
|
1871
|
+
const methodId = closestEnclosingClassMethods(call)?.get(property);
|
|
1872
|
+
if (methodId) targetId = methodId;
|
|
1873
|
+
} else if (object.type === "identifier") {
|
|
1874
|
+
const binding = importIndex.get(object.text);
|
|
1875
|
+
if (binding) {
|
|
1876
|
+
targetId = importTargetId(binding, property);
|
|
1877
|
+
confidence = "INFERRED";
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (!targetId) continue;
|
|
1883
|
+
const callerId = closestTrackedCaller(call);
|
|
1884
|
+
builder.addEdge({ source: callerId, target: targetId, relation: "calls", confidence });
|
|
1885
|
+
}
|
|
1886
|
+
return builder.build();
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/extract.ts
|
|
1890
|
+
var EXTRACTOR_REGISTRY = {
|
|
1891
|
+
".ts": extractTypeScript,
|
|
1892
|
+
".tsx": extractTypeScript,
|
|
1893
|
+
".js": extractTypeScript,
|
|
1894
|
+
".jsx": extractTypeScript,
|
|
1895
|
+
".mjs": extractTypeScript,
|
|
1896
|
+
".cjs": extractTypeScript,
|
|
1897
|
+
".mts": extractTypeScript,
|
|
1898
|
+
".cts": extractTypeScript,
|
|
1899
|
+
".py": extractPython,
|
|
1900
|
+
".go": extractGo,
|
|
1901
|
+
".rs": extractRust,
|
|
1902
|
+
".java": extractJava,
|
|
1903
|
+
".cs": extractCSharp,
|
|
1904
|
+
".rb": extractRuby
|
|
1905
|
+
};
|
|
1906
|
+
async function extract(filePath) {
|
|
1907
|
+
const ext = path4.extname(filePath);
|
|
1908
|
+
const extractor = EXTRACTOR_REGISTRY[ext];
|
|
1909
|
+
if (!extractor) {
|
|
1910
|
+
return { nodes: [], edges: [] };
|
|
1911
|
+
}
|
|
1912
|
+
return extractor(filePath);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// src/schema.ts
|
|
1916
|
+
import { z } from "zod";
|
|
1917
|
+
var ConfidenceSchema = z.enum(["EXTRACTED", "INFERRED", "AMBIGUOUS"]);
|
|
1918
|
+
var RelationSchema = z.enum([
|
|
1919
|
+
"calls",
|
|
1920
|
+
"imports",
|
|
1921
|
+
"imports_from",
|
|
1922
|
+
"inherits",
|
|
1923
|
+
"implements",
|
|
1924
|
+
"mixes_in",
|
|
1925
|
+
"embeds",
|
|
1926
|
+
"references",
|
|
1927
|
+
"contains",
|
|
1928
|
+
"method",
|
|
1929
|
+
"re_exports"
|
|
1930
|
+
]);
|
|
1931
|
+
var GraphNodeSchema = z.object({
|
|
1932
|
+
id: z.string().min(1, "node id must be non-empty"),
|
|
1933
|
+
label: z.string(),
|
|
1934
|
+
sourceFile: z.string(),
|
|
1935
|
+
sourceLocation: z.string()
|
|
1936
|
+
});
|
|
1937
|
+
var GraphEdgeSchema = z.object({
|
|
1938
|
+
source: z.string().min(1, "edge source must be non-empty"),
|
|
1939
|
+
target: z.string().min(1, "edge target must be non-empty"),
|
|
1940
|
+
relation: RelationSchema,
|
|
1941
|
+
confidence: ConfidenceSchema
|
|
1942
|
+
});
|
|
1943
|
+
var ExtractionResultSchema = z.object({
|
|
1944
|
+
nodes: z.array(GraphNodeSchema),
|
|
1945
|
+
edges: z.array(GraphEdgeSchema)
|
|
1946
|
+
});
|
|
1947
|
+
var ExtractionValidationError = class extends Error {
|
|
1948
|
+
issues;
|
|
1949
|
+
constructor(message, issues) {
|
|
1950
|
+
super(message);
|
|
1951
|
+
this.name = "ExtractionValidationError";
|
|
1952
|
+
this.issues = issues;
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
function validateExtraction(value, context) {
|
|
1956
|
+
const result = ExtractionResultSchema.safeParse(value);
|
|
1957
|
+
if (!result.success) {
|
|
1958
|
+
const label = context ? ` (${context})` : "";
|
|
1959
|
+
const issues = result.error.issues.map((issue) => ({
|
|
1960
|
+
path: issue.path,
|
|
1961
|
+
message: issue.message
|
|
1962
|
+
}));
|
|
1963
|
+
const details = issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
1964
|
+
throw new ExtractionValidationError(`Invalid ExtractionResult${label}: ${details}`, issues);
|
|
1965
|
+
}
|
|
1966
|
+
return result.data;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// src/build.ts
|
|
1970
|
+
import Graph from "graphology";
|
|
1971
|
+
var CONFIDENCE_RANK = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
1972
|
+
function strongerConfidence(a, b) {
|
|
1973
|
+
return CONFIDENCE_RANK[a] >= CONFIDENCE_RANK[b] ? a : b;
|
|
1974
|
+
}
|
|
1975
|
+
function ensureNode(graph, id) {
|
|
1976
|
+
if (graph.hasNode(id)) return;
|
|
1977
|
+
graph.addNode(id, { label: id, sourceFile: "<unknown>", sourceLocation: "L0" });
|
|
1978
|
+
}
|
|
1979
|
+
function buildGraph(extractions) {
|
|
1980
|
+
const graph = new Graph({ type: "directed", multi: true, allowSelfLoops: true });
|
|
1981
|
+
const validated = extractions.map(
|
|
1982
|
+
(extraction, index) => validateExtraction(extraction, `extraction #${index}`)
|
|
1983
|
+
);
|
|
1984
|
+
const allNodes = validated.flatMap((extraction) => extraction.nodes);
|
|
1985
|
+
const allEdges = validated.flatMap((extraction) => extraction.edges);
|
|
1986
|
+
const sortedNodes = [...allNodes].sort((a, b) => a.id.localeCompare(b.id));
|
|
1987
|
+
for (const node of sortedNodes) {
|
|
1988
|
+
if (graph.hasNode(node.id)) continue;
|
|
1989
|
+
graph.addNode(node.id, {
|
|
1990
|
+
label: node.label,
|
|
1991
|
+
sourceFile: node.sourceFile,
|
|
1992
|
+
sourceLocation: node.sourceLocation
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
const sortedEdges = [...allEdges].sort(
|
|
1996
|
+
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
|
|
1997
|
+
);
|
|
1998
|
+
for (const edge of sortedEdges) {
|
|
1999
|
+
ensureNode(graph, edge.source);
|
|
2000
|
+
ensureNode(graph, edge.target);
|
|
2001
|
+
const key = `${edge.source}|${edge.relation}|${edge.target}`;
|
|
2002
|
+
if (graph.hasEdge(key)) {
|
|
2003
|
+
const existing = graph.getEdgeAttribute(key, "confidence");
|
|
2004
|
+
graph.setEdgeAttribute(key, "confidence", strongerConfidence(existing, edge.confidence));
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
graph.addEdgeWithKey(key, edge.source, edge.target, {
|
|
2008
|
+
relation: edge.relation,
|
|
2009
|
+
confidence: edge.confidence
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
return graph;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// src/cluster.ts
|
|
2016
|
+
import { createHash } from "crypto";
|
|
2017
|
+
import Graph2 from "graphology";
|
|
2018
|
+
import leiden from "@aflsolutions/graphology-communities-leiden";
|
|
2019
|
+
import louvain from "graphology-communities-louvain";
|
|
2020
|
+
var FIXED_SEED = 1592594996;
|
|
2021
|
+
function mulberry32(seed) {
|
|
2022
|
+
let a = seed >>> 0;
|
|
2023
|
+
return function next() {
|
|
2024
|
+
a = a + 1831565813 | 0;
|
|
2025
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
2026
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
2027
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
function sortedWorkingCopy(graph) {
|
|
2031
|
+
const copy = new Graph2({ type: graph.type, multi: graph.multi, allowSelfLoops: graph.allowSelfLoops });
|
|
2032
|
+
const nodeIds = graph.nodes().sort((a, b) => a.localeCompare(b));
|
|
2033
|
+
for (const id of nodeIds) {
|
|
2034
|
+
copy.addNode(id, { ...graph.getNodeAttributes(id) });
|
|
2035
|
+
}
|
|
2036
|
+
const edgeEntries = [];
|
|
2037
|
+
graph.forEachEdge((edge, attributes, source, target) => {
|
|
2038
|
+
edgeEntries.push({ key: edge, source, target, attributes });
|
|
2039
|
+
});
|
|
2040
|
+
edgeEntries.sort(
|
|
2041
|
+
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || String(a.attributes.relation).localeCompare(String(b.attributes.relation))
|
|
2042
|
+
);
|
|
2043
|
+
for (const entry of edgeEntries) {
|
|
2044
|
+
copy.addEdgeWithKey(entry.key, entry.source, entry.target, { ...entry.attributes });
|
|
2045
|
+
}
|
|
2046
|
+
return copy;
|
|
2047
|
+
}
|
|
2048
|
+
function toUndirectedCopy(graph) {
|
|
2049
|
+
const copy = new Graph2({ type: "undirected", multi: true, allowSelfLoops: graph.allowSelfLoops });
|
|
2050
|
+
graph.forEachNode((nodeId, attributes) => copy.addNode(nodeId, { ...attributes }));
|
|
2051
|
+
graph.forEachEdge((edgeKey, attributes, source, target) => {
|
|
2052
|
+
copy.addEdgeWithKey(edgeKey, source, target, { ...attributes });
|
|
2053
|
+
});
|
|
2054
|
+
return copy;
|
|
2055
|
+
}
|
|
2056
|
+
function runAlgorithm(working, options) {
|
|
2057
|
+
const rng = mulberry32(FIXED_SEED);
|
|
2058
|
+
if (options.algorithm === "leiden") {
|
|
2059
|
+
const undirected = toUndirectedCopy(working);
|
|
2060
|
+
leiden.assign(undirected, { rng, maxIterations: options.maxIterations ?? 0 });
|
|
2061
|
+
undirected.forEachNode((nodeId, attributes) => {
|
|
2062
|
+
working.setNodeAttribute(nodeId, "community", attributes.community);
|
|
2063
|
+
});
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
louvain.assign(working, { rng });
|
|
2067
|
+
}
|
|
2068
|
+
function cluster(graph, options = {}) {
|
|
2069
|
+
if (graph.order === 0) return graph;
|
|
2070
|
+
const working = sortedWorkingCopy(graph);
|
|
2071
|
+
runAlgorithm(working, options);
|
|
2072
|
+
const hubs = /* @__PURE__ */ new Map();
|
|
2073
|
+
working.forEachNode((nodeId) => {
|
|
2074
|
+
const community = working.getNodeAttribute(nodeId, "community");
|
|
2075
|
+
const degree = working.degree(nodeId);
|
|
2076
|
+
const current = hubs.get(community);
|
|
2077
|
+
if (!current || degree > current.degree) {
|
|
2078
|
+
hubs.set(community, { id: nodeId, degree });
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
const membersByCommunity = /* @__PURE__ */ new Map();
|
|
2082
|
+
working.forEachNode((nodeId) => {
|
|
2083
|
+
const community = working.getNodeAttribute(nodeId, "community");
|
|
2084
|
+
const list = membersByCommunity.get(community);
|
|
2085
|
+
if (list) list.push(nodeId);
|
|
2086
|
+
else membersByCommunity.set(community, [nodeId]);
|
|
2087
|
+
});
|
|
2088
|
+
const labelByCommunity = /* @__PURE__ */ new Map();
|
|
2089
|
+
const hashByCommunity = /* @__PURE__ */ new Map();
|
|
2090
|
+
for (const [community, members] of membersByCommunity) {
|
|
2091
|
+
members.sort((a, b) => a.localeCompare(b));
|
|
2092
|
+
hashByCommunity.set(community, createHash("sha256").update(members.join("\n")).digest("hex"));
|
|
2093
|
+
const hub = hubs.get(community);
|
|
2094
|
+
const hubLabel = hub ? working.getNodeAttribute(hub.id, "label") : void 0;
|
|
2095
|
+
labelByCommunity.set(community, hubLabel && hubLabel.length > 0 ? hubLabel : `community-${community}`);
|
|
2096
|
+
}
|
|
2097
|
+
working.forEachNode((nodeId) => {
|
|
2098
|
+
const community = working.getNodeAttribute(nodeId, "community");
|
|
2099
|
+
graph.mergeNodeAttributes(nodeId, {
|
|
2100
|
+
community,
|
|
2101
|
+
communityLabel: labelByCommunity.get(community),
|
|
2102
|
+
communityHash: hashByCommunity.get(community)
|
|
2103
|
+
});
|
|
2104
|
+
});
|
|
2105
|
+
return graph;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/analyze.ts
|
|
2109
|
+
var ABSOLUTE_DEGREE_FLOOR = 10;
|
|
2110
|
+
var MEAN_DEGREE_MULTIPLIER = 3;
|
|
2111
|
+
var MAX_LISTED = 10;
|
|
2112
|
+
function truncatedList(items) {
|
|
2113
|
+
const shown = items.slice(0, MAX_LISTED).join(", ");
|
|
2114
|
+
return items.length > MAX_LISTED ? `${shown}, ...` : shown;
|
|
2115
|
+
}
|
|
2116
|
+
function connectedComponents(graph) {
|
|
2117
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2118
|
+
const components = [];
|
|
2119
|
+
const sortedNodes = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
|
|
2120
|
+
for (const start of sortedNodes) {
|
|
2121
|
+
if (visited.has(start)) continue;
|
|
2122
|
+
const component = [];
|
|
2123
|
+
const queue = [start];
|
|
2124
|
+
visited.add(start);
|
|
2125
|
+
while (queue.length > 0) {
|
|
2126
|
+
const current = queue.shift();
|
|
2127
|
+
component.push(current);
|
|
2128
|
+
const neighbors = [...graph.neighbors(current)].sort((a, b) => a.localeCompare(b));
|
|
2129
|
+
for (const neighbor of neighbors) {
|
|
2130
|
+
if (!visited.has(neighbor)) {
|
|
2131
|
+
visited.add(neighbor);
|
|
2132
|
+
queue.push(neighbor);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
component.sort((a, b) => a.localeCompare(b));
|
|
2137
|
+
components.push(component);
|
|
2138
|
+
}
|
|
2139
|
+
components.sort((a, b) => b.length - a.length || (a[0] ?? "").localeCompare(b[0] ?? ""));
|
|
2140
|
+
return components;
|
|
2141
|
+
}
|
|
2142
|
+
function labelOf(graph, id) {
|
|
2143
|
+
return graph.getNodeAttribute(id, "label") ?? id;
|
|
2144
|
+
}
|
|
2145
|
+
function analyze(graph) {
|
|
2146
|
+
const godNodes = [];
|
|
2147
|
+
const surprises = [];
|
|
2148
|
+
const openQuestions = [];
|
|
2149
|
+
if (graph.order === 0) {
|
|
2150
|
+
openQuestions.push(
|
|
2151
|
+
"The graph is empty \u2014 no nodes were extracted. Check detect()/extract() coverage for this codebase."
|
|
2152
|
+
);
|
|
2153
|
+
return { godNodes, surprises, openQuestions };
|
|
2154
|
+
}
|
|
2155
|
+
const degreeEntries = [...graph.nodes()].map((id) => ({ id, degree: graph.degree(id) })).sort((a, b) => a.id.localeCompare(b.id));
|
|
2156
|
+
const meanDegree = degreeEntries.reduce((sum, e) => sum + e.degree, 0) / degreeEntries.length;
|
|
2157
|
+
const threshold = Math.max(ABSOLUTE_DEGREE_FLOOR, meanDegree * MEAN_DEGREE_MULTIPLIER);
|
|
2158
|
+
for (const { id, degree } of degreeEntries) {
|
|
2159
|
+
if (degree > 0 && degree >= threshold) {
|
|
2160
|
+
godNodes.push(id);
|
|
2161
|
+
surprises.push(
|
|
2162
|
+
`${labelOf(graph, id)} has ${degree} connections (fan-in + fan-out) \u2014 well above the graph average of ${meanDegree.toFixed(1)}. Likely a god node / hotspot worth reviewing for excessive coupling.`
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
graph.forEachEdge((_edge, attributes, source, target) => {
|
|
2167
|
+
if (source === target) {
|
|
2168
|
+
surprises.push(`${labelOf(graph, source)} has a self-referential "${String(attributes.relation)}" edge.`);
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
const ambiguousEdges = [];
|
|
2172
|
+
graph.forEachEdge((_edge, attributes, source, target) => {
|
|
2173
|
+
if (attributes.confidence === "AMBIGUOUS") {
|
|
2174
|
+
ambiguousEdges.push(
|
|
2175
|
+
`${labelOf(graph, source)} --${String(attributes.relation)}--> ${labelOf(graph, target)}`
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
if (ambiguousEdges.length > 0) {
|
|
2180
|
+
ambiguousEdges.sort((a, b) => a.localeCompare(b));
|
|
2181
|
+
openQuestions.push(
|
|
2182
|
+
`${ambiguousEdges.length} edge(s) are marked AMBIGUOUS and need human review: ` + truncatedList(ambiguousEdges)
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
const isolated = degreeEntries.filter((e) => e.degree === 0).map((e) => e.id);
|
|
2186
|
+
if (isolated.length > 0) {
|
|
2187
|
+
openQuestions.push(
|
|
2188
|
+
`${isolated.length} node(s) have no incoming or outgoing edges (${truncatedList(isolated)}) \u2014 dead code, an entry point, or a gap in extraction?`
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
const components = connectedComponents(graph);
|
|
2192
|
+
if (components.length > 1) {
|
|
2193
|
+
const largest = components[0];
|
|
2194
|
+
openQuestions.push(
|
|
2195
|
+
`The graph has ${components.length} disconnected components \u2014 the largest has ${largest.length} node(s). Confirm this reflects real module boundaries rather than missed cross-file edges.`
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
return { godNodes, surprises, openQuestions };
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/report.ts
|
|
2202
|
+
function labelOf2(graph, id) {
|
|
2203
|
+
return graph.getNodeAttribute(id, "label") ?? id;
|
|
2204
|
+
}
|
|
2205
|
+
function bulletList(items) {
|
|
2206
|
+
if (items.length === 0) return "_None found._";
|
|
2207
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
2208
|
+
}
|
|
2209
|
+
function summarizeCommunities(graph) {
|
|
2210
|
+
const byCommunity = /* @__PURE__ */ new Map();
|
|
2211
|
+
graph.forEachNode((nodeId, attributes) => {
|
|
2212
|
+
const community = attributes.community;
|
|
2213
|
+
if (community === void 0) return;
|
|
2214
|
+
const label = attributes.communityLabel ?? `community-${community}`;
|
|
2215
|
+
const existing = byCommunity.get(community);
|
|
2216
|
+
if (existing) {
|
|
2217
|
+
existing.members.push(nodeId);
|
|
2218
|
+
} else {
|
|
2219
|
+
byCommunity.set(community, { label, members: [nodeId] });
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
const summaries = [...byCommunity.values()];
|
|
2223
|
+
for (const summary of summaries) {
|
|
2224
|
+
summary.members.sort((a, b) => a.localeCompare(b));
|
|
2225
|
+
}
|
|
2226
|
+
summaries.sort((a, b) => b.members.length - a.members.length || a.label.localeCompare(b.label));
|
|
2227
|
+
return summaries;
|
|
2228
|
+
}
|
|
2229
|
+
function renderReport(graph, analysis, now = /* @__PURE__ */ new Date()) {
|
|
2230
|
+
const generatedAt = now.toISOString();
|
|
2231
|
+
const communities = summarizeCommunities(graph);
|
|
2232
|
+
const lines = [];
|
|
2233
|
+
lines.push("# Graph Report");
|
|
2234
|
+
lines.push("");
|
|
2235
|
+
lines.push(`Generated: ${generatedAt}`);
|
|
2236
|
+
lines.push("");
|
|
2237
|
+
lines.push("## Summary");
|
|
2238
|
+
lines.push("");
|
|
2239
|
+
lines.push(`- **Nodes:** ${graph.order}`);
|
|
2240
|
+
lines.push(`- **Edges:** ${graph.size}`);
|
|
2241
|
+
lines.push(`- **Communities:** ${communities.length}`);
|
|
2242
|
+
lines.push(`- **God nodes:** ${analysis.godNodes.length}`);
|
|
2243
|
+
lines.push("");
|
|
2244
|
+
lines.push("## Communities");
|
|
2245
|
+
lines.push("");
|
|
2246
|
+
if (communities.length === 0) {
|
|
2247
|
+
lines.push("_No communities detected (graph may be empty, or cluster() has not run yet)._");
|
|
2248
|
+
} else {
|
|
2249
|
+
for (const community of communities) {
|
|
2250
|
+
lines.push(`### ${community.label} (${community.members.length} member(s))`);
|
|
2251
|
+
lines.push("");
|
|
2252
|
+
const shown = community.members.slice(0, 25).map((id) => labelOf2(graph, id));
|
|
2253
|
+
lines.push(bulletList(shown));
|
|
2254
|
+
if (community.members.length > 25) {
|
|
2255
|
+
lines.push(`- ...and ${community.members.length - 25} more`);
|
|
2256
|
+
}
|
|
2257
|
+
lines.push("");
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
lines.push("## God Nodes");
|
|
2261
|
+
lines.push("");
|
|
2262
|
+
lines.push(
|
|
2263
|
+
"Nodes with unusually high fan-in + fan-out relative to the rest of this graph \u2014 often a sign of a shared utility, a bottleneck, or a module that has taken on too many responsibilities."
|
|
2264
|
+
);
|
|
2265
|
+
lines.push("");
|
|
2266
|
+
lines.push(bulletList(analysis.godNodes.map((id) => labelOf2(graph, id))));
|
|
2267
|
+
lines.push("");
|
|
2268
|
+
lines.push("## Structural Surprises");
|
|
2269
|
+
lines.push("");
|
|
2270
|
+
lines.push(bulletList(analysis.surprises));
|
|
2271
|
+
lines.push("");
|
|
2272
|
+
lines.push("## Open Questions");
|
|
2273
|
+
lines.push("");
|
|
2274
|
+
lines.push(bulletList(analysis.openQuestions));
|
|
2275
|
+
lines.push("");
|
|
2276
|
+
return lines.join("\n");
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/export.ts
|
|
2280
|
+
import { createRequire as createRequire2 } from "module";
|
|
2281
|
+
import * as fs9 from "fs/promises";
|
|
2282
|
+
import * as path5 from "path";
|
|
2283
|
+
var require3 = createRequire2(import.meta.url);
|
|
2284
|
+
function resolveVisNetworkAssets() {
|
|
2285
|
+
const packageJsonPath = require3.resolve("vis-network/package.json");
|
|
2286
|
+
const root = path5.dirname(packageJsonPath);
|
|
2287
|
+
return {
|
|
2288
|
+
jsPath: path5.join(root, "standalone", "umd", "vis-network.min.js"),
|
|
2289
|
+
cssPath: path5.join(root, "styles", "vis-network.min.css")
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
var COMMUNITY_PALETTE = [
|
|
2293
|
+
"#4e79a7",
|
|
2294
|
+
"#f28e2b",
|
|
2295
|
+
"#e15759",
|
|
2296
|
+
"#76b7b2",
|
|
2297
|
+
"#59a14f",
|
|
2298
|
+
"#edc948",
|
|
2299
|
+
"#b07aa1",
|
|
2300
|
+
"#ff9da7",
|
|
2301
|
+
"#9c755f",
|
|
2302
|
+
"#bab0ac"
|
|
2303
|
+
];
|
|
2304
|
+
function colorForCommunity(community) {
|
|
2305
|
+
if (community === void 0) return "#8ab4f8";
|
|
2306
|
+
const index = (community % COMMUNITY_PALETTE.length + COMMUNITY_PALETTE.length) % COMMUNITY_PALETTE.length;
|
|
2307
|
+
return COMMUNITY_PALETTE[index];
|
|
2308
|
+
}
|
|
2309
|
+
function safeInlineJson(value) {
|
|
2310
|
+
return JSON.stringify(value, null, 2).replace(/<\/(script)/gi, "<\\/$1");
|
|
2311
|
+
}
|
|
2312
|
+
function buildVisNodesAndEdges(graph) {
|
|
2313
|
+
const nodes = graph.nodes().map((id) => {
|
|
2314
|
+
const attributes = graph.getNodeAttributes(id);
|
|
2315
|
+
const rawLabel = attributes.label ?? id;
|
|
2316
|
+
const label = escapeHtml(sanitizeLabel(rawLabel));
|
|
2317
|
+
const sourceFile = escapeHtml(sanitizeLabel(attributes.sourceFile ?? ""));
|
|
2318
|
+
const sourceLocation = escapeHtml(sanitizeLabel(attributes.sourceLocation ?? ""));
|
|
2319
|
+
const communityLabel = escapeHtml(sanitizeLabel(attributes.communityLabel ?? ""));
|
|
2320
|
+
return {
|
|
2321
|
+
id,
|
|
2322
|
+
label,
|
|
2323
|
+
title: `${label}
|
|
2324
|
+
${sourceFile}${sourceLocation ? `:${sourceLocation}` : ""}${communityLabel ? `
|
|
2325
|
+
community: ${communityLabel}` : ""}`,
|
|
2326
|
+
color: colorForCommunity(attributes.community)
|
|
2327
|
+
};
|
|
2328
|
+
});
|
|
2329
|
+
const edges = [];
|
|
2330
|
+
graph.forEachEdge((edgeKey, attributes, source, target) => {
|
|
2331
|
+
const relation = escapeHtml(sanitizeLabel(String(attributes.relation ?? "")));
|
|
2332
|
+
const confidence = escapeHtml(sanitizeLabel(String(attributes.confidence ?? "")));
|
|
2333
|
+
edges.push({
|
|
2334
|
+
id: edgeKey,
|
|
2335
|
+
from: source,
|
|
2336
|
+
to: target,
|
|
2337
|
+
label: relation,
|
|
2338
|
+
title: `${relation} (${confidence})`,
|
|
2339
|
+
dashes: confidence === "INFERRED" || confidence === "AMBIGUOUS",
|
|
2340
|
+
arrows: "to"
|
|
2341
|
+
});
|
|
2342
|
+
});
|
|
2343
|
+
return { nodes, edges };
|
|
2344
|
+
}
|
|
2345
|
+
async function renderHtml(graph) {
|
|
2346
|
+
const { jsPath, cssPath } = resolveVisNetworkAssets();
|
|
2347
|
+
const [visJs, visCss] = await Promise.all([
|
|
2348
|
+
fs9.readFile(jsPath, "utf-8"),
|
|
2349
|
+
fs9.readFile(cssPath, "utf-8")
|
|
2350
|
+
]);
|
|
2351
|
+
const { nodes, edges } = buildVisNodesAndEdges(graph);
|
|
2352
|
+
const dataJson = safeInlineJson({ nodes, edges });
|
|
2353
|
+
return `<!doctype html>
|
|
2354
|
+
<html lang="en">
|
|
2355
|
+
<head>
|
|
2356
|
+
<meta charset="utf-8">
|
|
2357
|
+
<title>graphify \u2014 graph.html</title>
|
|
2358
|
+
<style>
|
|
2359
|
+
html, body { margin: 0; padding: 0; height: 100%; font-family: system-ui, sans-serif; }
|
|
2360
|
+
#graph { width: 100%; height: 100vh; }
|
|
2361
|
+
${visCss}
|
|
2362
|
+
</style>
|
|
2363
|
+
</head>
|
|
2364
|
+
<body>
|
|
2365
|
+
<div id="graph"></div>
|
|
2366
|
+
<script>
|
|
2367
|
+
${visJs}
|
|
2368
|
+
</script>
|
|
2369
|
+
<script>
|
|
2370
|
+
(function () {
|
|
2371
|
+
var data = ${dataJson};
|
|
2372
|
+
var nodes = new vis.DataSet(data.nodes);
|
|
2373
|
+
var edges = new vis.DataSet(data.edges);
|
|
2374
|
+
var container = document.getElementById('graph');
|
|
2375
|
+
var network = new vis.Network(container, { nodes: nodes, edges: edges }, {
|
|
2376
|
+
nodes: { shape: 'dot', size: 12, font: { size: 14 } },
|
|
2377
|
+
edges: { smooth: { type: 'continuous' }, font: { size: 10, align: 'middle' } },
|
|
2378
|
+
physics: { stabilization: true },
|
|
2379
|
+
interaction: { hover: true, tooltipDelay: 150 },
|
|
2380
|
+
});
|
|
2381
|
+
window.__graphifyNetwork = network;
|
|
2382
|
+
})();
|
|
2383
|
+
</script>
|
|
2384
|
+
</body>
|
|
2385
|
+
</html>
|
|
2386
|
+
`;
|
|
2387
|
+
}
|
|
2388
|
+
async function exportGraph(graph, options, report) {
|
|
2389
|
+
const outDir = path5.resolve(options.outDir);
|
|
2390
|
+
await fs9.mkdir(outDir, { recursive: true });
|
|
2391
|
+
const jsonPath = validateGraphPath("graph.json", outDir);
|
|
2392
|
+
await fs9.writeFile(jsonPath, JSON.stringify(graph.toJSON(), null, 2), "utf-8");
|
|
2393
|
+
if (options.html !== false) {
|
|
2394
|
+
const htmlPath = validateGraphPath("graph.html", outDir);
|
|
2395
|
+
await fs9.writeFile(htmlPath, await renderHtml(graph), "utf-8");
|
|
2396
|
+
}
|
|
2397
|
+
if (report !== void 0) {
|
|
2398
|
+
const reportPath = validateGraphPath("GRAPH_REPORT.md", outDir);
|
|
2399
|
+
await fs9.writeFile(reportPath, report, "utf-8");
|
|
2400
|
+
}
|
|
2401
|
+
const notImplemented = [];
|
|
2402
|
+
if (options.svg) notImplemented.push("--svg");
|
|
2403
|
+
if (options.graphml) notImplemented.push("--graphml");
|
|
2404
|
+
if (options.neo4j) notImplemented.push("--neo4j");
|
|
2405
|
+
if (options.obsidian) notImplemented.push("--obsidian");
|
|
2406
|
+
for (const flag of notImplemented) {
|
|
2407
|
+
console.warn(`graphify: ${flag} export is not implemented yet in v1 \u2014 skipping.`);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
// src/pipeline.ts
|
|
2412
|
+
import * as path6 from "path";
|
|
2413
|
+
async function runPipeline(root, options = {}) {
|
|
2414
|
+
const progress = options.onProgress ?? (() => {
|
|
2415
|
+
});
|
|
2416
|
+
const resolvedRoot = path6.resolve(root);
|
|
2417
|
+
progress(`Scanning ${resolvedRoot} ...`);
|
|
2418
|
+
const manifest = collectFiles(resolvedRoot);
|
|
2419
|
+
progress(
|
|
2420
|
+
`Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
|
|
2421
|
+
);
|
|
2422
|
+
progress("Extracting...");
|
|
2423
|
+
const extractions = [];
|
|
2424
|
+
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
2425
|
+
const filePath = path6.relative(process.cwd(), path6.join(resolvedRoot, relFile)) || relFile;
|
|
2426
|
+
try {
|
|
2427
|
+
const extraction = await extract(filePath);
|
|
2428
|
+
extractions.push(extraction);
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
progress(` extraction failed for ${relFile}: ${error.message}`);
|
|
2431
|
+
throw error;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
progress("Building graph...");
|
|
2435
|
+
const graph = buildGraph(extractions);
|
|
2436
|
+
progress(`Clustering (${options.algorithm ?? "louvain"})...`);
|
|
2437
|
+
cluster(graph, { algorithm: options.algorithm, maxIterations: options.maxIterations });
|
|
2438
|
+
progress("Analyzing...");
|
|
2439
|
+
const analysis = analyze(graph);
|
|
2440
|
+
progress("Rendering report...");
|
|
2441
|
+
const report = renderReport(graph, analysis);
|
|
2442
|
+
const outDir = options.outDir ?? path6.join(resolvedRoot, "graphify-out");
|
|
2443
|
+
progress(`Exporting to ${outDir} ...`);
|
|
2444
|
+
await exportGraph(
|
|
2445
|
+
graph,
|
|
2446
|
+
{
|
|
2447
|
+
outDir,
|
|
2448
|
+
html: options.html,
|
|
2449
|
+
svg: options.svg,
|
|
2450
|
+
graphml: options.graphml,
|
|
2451
|
+
neo4j: options.neo4j,
|
|
2452
|
+
obsidian: options.obsidian
|
|
2453
|
+
},
|
|
2454
|
+
report
|
|
2455
|
+
);
|
|
2456
|
+
progress("Done.");
|
|
2457
|
+
return { manifest, graph, analysis, report };
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
export {
|
|
2461
|
+
collectFiles,
|
|
2462
|
+
EXTRACTOR_REGISTRY,
|
|
2463
|
+
extract,
|
|
2464
|
+
ExtractionResultSchema,
|
|
2465
|
+
ExtractionValidationError,
|
|
2466
|
+
validateExtraction,
|
|
2467
|
+
buildGraph,
|
|
2468
|
+
cluster,
|
|
2469
|
+
analyze,
|
|
2470
|
+
renderReport,
|
|
2471
|
+
exportGraph,
|
|
2472
|
+
runPipeline
|
|
2473
|
+
};
|
|
2474
|
+
//# sourceMappingURL=chunk-DG5FECXV.js.map
|