openlore 2.0.3 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli/commands/analyze.js +36 -23
- package/dist/cli/commands/analyze.js.map +1 -1
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
- package/dist/core/analyzer/artifact-generator.js +37 -3
- package/dist/core/analyzer/artifact-generator.js.map +1 -1
- package/dist/core/analyzer/call-graph.d.ts +9 -2
- package/dist/core/analyzer/call-graph.d.ts.map +1 -1
- package/dist/core/analyzer/call-graph.js +534 -1
- package/dist/core/analyzer/call-graph.js.map +1 -1
- package/dist/core/analyzer/embedding-service.d.ts +2 -0
- package/dist/core/analyzer/embedding-service.d.ts.map +1 -1
- package/dist/core/analyzer/embedding-service.js +4 -0
- package/dist/core/analyzer/embedding-service.js.map +1 -1
- package/dist/core/analyzer/fixtures/regression/sample.d.ts +9 -0
- package/dist/core/analyzer/fixtures/regression/sample.d.ts.map +1 -0
- package/dist/core/analyzer/fixtures/regression/sample.js +26 -0
- package/dist/core/analyzer/fixtures/regression/sample.js.map +1 -0
- package/dist/core/analyzer/iac/ansible.d.ts +18 -0
- package/dist/core/analyzer/iac/ansible.d.ts.map +1 -0
- package/dist/core/analyzer/iac/ansible.js +219 -0
- package/dist/core/analyzer/iac/ansible.js.map +1 -0
- package/dist/core/analyzer/iac/cdk.d.ts +26 -0
- package/dist/core/analyzer/iac/cdk.d.ts.map +1 -0
- package/dist/core/analyzer/iac/cdk.js +204 -0
- package/dist/core/analyzer/iac/cdk.js.map +1 -0
- package/dist/core/analyzer/iac/classify-yaml.d.ts +20 -0
- package/dist/core/analyzer/iac/classify-yaml.d.ts.map +1 -0
- package/dist/core/analyzer/iac/classify-yaml.js +81 -0
- package/dist/core/analyzer/iac/classify-yaml.js.map +1 -0
- package/dist/core/analyzer/iac/cloudformation.d.ts +15 -0
- package/dist/core/analyzer/iac/cloudformation.d.ts.map +1 -0
- package/dist/core/analyzer/iac/cloudformation.js +189 -0
- package/dist/core/analyzer/iac/cloudformation.js.map +1 -0
- package/dist/core/analyzer/iac/helm.d.ts +20 -0
- package/dist/core/analyzer/iac/helm.d.ts.map +1 -0
- package/dist/core/analyzer/iac/helm.js +229 -0
- package/dist/core/analyzer/iac/helm.js.map +1 -0
- package/dist/core/analyzer/iac/index.d.ts +23 -0
- package/dist/core/analyzer/iac/index.d.ts.map +1 -0
- package/dist/core/analyzer/iac/index.js +40 -0
- package/dist/core/analyzer/iac/index.js.map +1 -0
- package/dist/core/analyzer/iac/kubernetes.d.ts +14 -0
- package/dist/core/analyzer/iac/kubernetes.d.ts.map +1 -0
- package/dist/core/analyzer/iac/kubernetes.js +226 -0
- package/dist/core/analyzer/iac/kubernetes.js.map +1 -0
- package/dist/core/analyzer/iac/project.d.ts +16 -0
- package/dist/core/analyzer/iac/project.d.ts.map +1 -0
- package/dist/core/analyzer/iac/project.js +97 -0
- package/dist/core/analyzer/iac/project.js.map +1 -0
- package/dist/core/analyzer/iac/pulumi.d.ts +21 -0
- package/dist/core/analyzer/iac/pulumi.d.ts.map +1 -0
- package/dist/core/analyzer/iac/pulumi.js +190 -0
- package/dist/core/analyzer/iac/pulumi.js.map +1 -0
- package/dist/core/analyzer/iac/terraform.d.ts +19 -0
- package/dist/core/analyzer/iac/terraform.d.ts.map +1 -0
- package/dist/core/analyzer/iac/terraform.js +546 -0
- package/dist/core/analyzer/iac/terraform.js.map +1 -0
- package/dist/core/analyzer/iac/types.d.ts +67 -0
- package/dist/core/analyzer/iac/types.d.ts.map +1 -0
- package/dist/core/analyzer/iac/types.js +39 -0
- package/dist/core/analyzer/iac/types.js.map +1 -0
- package/dist/core/analyzer/signature-extractor.d.ts +6 -0
- package/dist/core/analyzer/signature-extractor.d.ts.map +1 -1
- package/dist/core/analyzer/signature-extractor.js +112 -2
- package/dist/core/analyzer/signature-extractor.js.map +1 -1
- package/dist/core/analyzer/spec-vector-index.d.ts +9 -2
- package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -1
- package/dist/core/analyzer/spec-vector-index.js +111 -10
- package/dist/core/analyzer/spec-vector-index.js.map +1 -1
- package/dist/core/analyzer/vector-index.d.ts +33 -1
- package/dist/core/analyzer/vector-index.d.ts.map +1 -1
- package/dist/core/analyzer/vector-index.js +99 -10
- package/dist/core/analyzer/vector-index.js.map +1 -1
- package/dist/core/services/edge-store.d.ts +6 -0
- package/dist/core/services/edge-store.d.ts.map +1 -1
- package/dist/core/services/edge-store.js +8 -0
- package/dist/core/services/edge-store.js.map +1 -1
- package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/graph.js +12 -0
- package/dist/core/services/mcp-handlers/graph.js.map +1 -1
- package/dist/core/services/mcp-handlers/orient.js +2 -2
- package/dist/core/services/mcp-handlers/orient.js.map +1 -1
- package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/semantic.js +23 -30
- package/dist/core/services/mcp-handlers/semantic.js.map +1 -1
- package/dist/core/services/mcp-watcher.d.ts.map +1 -1
- package/dist/core/services/mcp-watcher.js +14 -10
- package/dist/core/services/mcp-watcher.js.map +1 -1
- package/package.json +10 -1
|
@@ -16,6 +16,8 @@ import Parser from 'tree-sitter';
|
|
|
16
16
|
import { FunctionRegistryTrie } from './function-registry-trie.js';
|
|
17
17
|
import { inferTypesFromSource, resolveViaTypeInference } from './type-inference-engine.js';
|
|
18
18
|
import { extractAllHttpEdges } from './http-route-parser.js';
|
|
19
|
+
import { buildProjectedIac } from './iac/index.js';
|
|
20
|
+
import { logger } from '../../utils/logger.js';
|
|
19
21
|
// ============================================================================
|
|
20
22
|
// CONSTANTS
|
|
21
23
|
// ============================================================================
|
|
@@ -1167,6 +1169,494 @@ async function extractSwiftGraph(filePath, content) {
|
|
|
1167
1169
|
return { nodes, rawEdges };
|
|
1168
1170
|
}
|
|
1169
1171
|
// ============================================================================
|
|
1172
|
+
// ADDITIONAL GENERAL-PURPOSE LANGUAGES (spec-08)
|
|
1173
|
+
// ============================================================================
|
|
1174
|
+
//
|
|
1175
|
+
// C#, Kotlin, PHP, C, Scala, Dart, Lua, Elixir, Bash. Each follows the existing
|
|
1176
|
+
// extractor pattern (lazy soft-loaded grammar + FN/CALL queries + dispatch).
|
|
1177
|
+
// Grammars are native modules; loaders fail SOFT (graceful degradation): a
|
|
1178
|
+
// missing/ABI-incompatible grammar logs one warning and skips graphing for that
|
|
1179
|
+
// language without aborting analyze or any other language.
|
|
1180
|
+
const _warnedUnavailable = new Set();
|
|
1181
|
+
const _grammarHandleCache = new Map();
|
|
1182
|
+
function warnUnavailable(language, err) {
|
|
1183
|
+
if (!_warnedUnavailable.has(language)) {
|
|
1184
|
+
_warnedUnavailable.add(language);
|
|
1185
|
+
logger.warning(`language ${language} grammar unavailable — files will be indexed for search but not graphed (${err.message})`);
|
|
1186
|
+
}
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
/** Native tree-sitter loader. Returns a uniform handle, or null when unavailable. */
|
|
1190
|
+
async function loadGrammarSoft(language, importer, pick) {
|
|
1191
|
+
if (_grammarHandleCache.has(language))
|
|
1192
|
+
return _grammarHandleCache.get(language);
|
|
1193
|
+
try {
|
|
1194
|
+
const mod = (await importer());
|
|
1195
|
+
const lang = pick(mod);
|
|
1196
|
+
if (!lang)
|
|
1197
|
+
throw new Error('grammar export resolved to undefined');
|
|
1198
|
+
const parser = new Parser();
|
|
1199
|
+
parser.setLanguage(lang);
|
|
1200
|
+
const handle = {
|
|
1201
|
+
withTree: (content, fn) => {
|
|
1202
|
+
const tree = parser.parse(content);
|
|
1203
|
+
const root = tree.rootNode;
|
|
1204
|
+
const runQuery = (src) => {
|
|
1205
|
+
try {
|
|
1206
|
+
const q = new Parser.Query(lang, src);
|
|
1207
|
+
return q.matches(tree.rootNode);
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
return [];
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
return fn(root, runQuery);
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
_grammarHandleCache.set(language, handle);
|
|
1217
|
+
return handle;
|
|
1218
|
+
}
|
|
1219
|
+
catch (err) {
|
|
1220
|
+
_grammarHandleCache.set(language, warnUnavailable(language, err));
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* WASM grammar loader via web-tree-sitter (ABI-agnostic, portable). Used for
|
|
1226
|
+
* grammars with no host-ABI-compatible native build (Dart, Lua). Soft-fails.
|
|
1227
|
+
*/
|
|
1228
|
+
async function loadWasmGrammarSoft(language, wasmSpecifier) {
|
|
1229
|
+
const cacheKey = `wasm:${language}`;
|
|
1230
|
+
if (_grammarHandleCache.has(cacheKey))
|
|
1231
|
+
return _grammarHandleCache.get(cacheKey);
|
|
1232
|
+
try {
|
|
1233
|
+
const { createRequire } = await import('node:module');
|
|
1234
|
+
const { readFile } = await import('node:fs/promises');
|
|
1235
|
+
const req = createRequire(import.meta.url);
|
|
1236
|
+
const wasmPath = req.resolve(wasmSpecifier);
|
|
1237
|
+
// Load the wasm bytes ourselves and hand web-tree-sitter a Uint8Array, so it
|
|
1238
|
+
// never does its own `require("fs/promises")` (which breaks under ESM/vitest).
|
|
1239
|
+
const wasmBytes = new Uint8Array(await readFile(wasmPath));
|
|
1240
|
+
// CRITICAL: each WASM grammar gets its OWN web-tree-sitter module instance.
|
|
1241
|
+
// web-tree-sitter is a singleton emscripten module with a shared heap; loading
|
|
1242
|
+
// two different grammars into one instance corrupts parsing (a Dart parse
|
|
1243
|
+
// silently breaks subsequent Lua parses). Busting the require cache before each
|
|
1244
|
+
// grammar yields an isolated runtime + heap per grammar, so they never interfere.
|
|
1245
|
+
for (const k of Object.keys(req.cache)) {
|
|
1246
|
+
if (k.includes('web-tree-sitter'))
|
|
1247
|
+
delete req.cache[k];
|
|
1248
|
+
}
|
|
1249
|
+
const TS = req('web-tree-sitter');
|
|
1250
|
+
const ParserCtor = (TS.default ?? TS.Parser ?? TS);
|
|
1251
|
+
if (typeof ParserCtor.init === 'function')
|
|
1252
|
+
await ParserCtor.init();
|
|
1253
|
+
const LanguageNs = (TS.Language ?? ParserCtor.Language);
|
|
1254
|
+
const lang = await LanguageNs.load(wasmBytes);
|
|
1255
|
+
const handle = {
|
|
1256
|
+
withTree: (content, fn) => {
|
|
1257
|
+
// Fresh parser + explicit tree/query disposal: web-tree-sitter holds the
|
|
1258
|
+
// parse tree in WASM heap, which corrupts the next parse if not freed.
|
|
1259
|
+
const p = new ParserCtor();
|
|
1260
|
+
p.setLanguage(lang);
|
|
1261
|
+
const tree = p.parse(content);
|
|
1262
|
+
const queries = [];
|
|
1263
|
+
const runQuery = (src) => {
|
|
1264
|
+
try {
|
|
1265
|
+
const q = lang.query(src);
|
|
1266
|
+
queries.push(q);
|
|
1267
|
+
return q.matches(tree.rootNode);
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
return [];
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
try {
|
|
1274
|
+
return fn(tree.rootNode, runQuery);
|
|
1275
|
+
}
|
|
1276
|
+
finally {
|
|
1277
|
+
for (const q of queries)
|
|
1278
|
+
q.delete?.();
|
|
1279
|
+
tree.delete?.();
|
|
1280
|
+
p.delete?.();
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
};
|
|
1284
|
+
_grammarHandleCache.set(cacheKey, handle);
|
|
1285
|
+
return handle;
|
|
1286
|
+
}
|
|
1287
|
+
catch (err) {
|
|
1288
|
+
_grammarHandleCache.set(cacheKey, warnUnavailable(language, err));
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/** Reset loader caches — test-only hook for the graceful-degradation test. */
|
|
1293
|
+
export function __resetGrammarCacheForTests() {
|
|
1294
|
+
_grammarHandleCache.clear();
|
|
1295
|
+
_warnedUnavailable.clear();
|
|
1296
|
+
}
|
|
1297
|
+
const NAME_CHILD_TYPES = new Set(['identifier', 'name', 'type_identifier', 'simple_identifier', 'word']);
|
|
1298
|
+
/** Walk up from a node to the nearest grouping construct; return its declared name. */
|
|
1299
|
+
function enclosingGroupName(node, classTypes) {
|
|
1300
|
+
let cursor = node.parent;
|
|
1301
|
+
while (cursor) {
|
|
1302
|
+
if (classTypes.has(cursor.type)) {
|
|
1303
|
+
const nameNode = cursor.namedChildren.find(c => NAME_CHILD_TYPES.has(c.type))
|
|
1304
|
+
?? cursor.childForFieldName('name') ?? undefined;
|
|
1305
|
+
if (nameNode)
|
|
1306
|
+
return nameNode.text;
|
|
1307
|
+
}
|
|
1308
|
+
cursor = cursor.parent;
|
|
1309
|
+
}
|
|
1310
|
+
return undefined;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Generic query-driven extractor shared by the structurally-similar languages.
|
|
1314
|
+
* Mirrors the Java extractor's shape; per-language differences are expressed
|
|
1315
|
+
* via the QueryLangSpec rather than copy-pasted bodies.
|
|
1316
|
+
*/
|
|
1317
|
+
async function extractByQueries(spec, filePath, content) {
|
|
1318
|
+
const handle = await spec.loader();
|
|
1319
|
+
if (!handle)
|
|
1320
|
+
return { nodes: [], rawEdges: [] };
|
|
1321
|
+
return handle.withTree(content, (_root, runQuery) => {
|
|
1322
|
+
const nodes = [];
|
|
1323
|
+
for (const match of runQuery(spec.fnQuery)) {
|
|
1324
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
1325
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
1326
|
+
if (!nameCapture || !nodeCapture)
|
|
1327
|
+
continue;
|
|
1328
|
+
const name = nameCapture.node.text;
|
|
1329
|
+
const fnNode = nodeCapture.node;
|
|
1330
|
+
const className = (spec.classTypes.size ? enclosingGroupName(fnNode, spec.classTypes) : undefined)
|
|
1331
|
+
?? spec.extraClassName?.(fnNode);
|
|
1332
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
1333
|
+
if (nodes.some(n => n.id === id))
|
|
1334
|
+
continue; // collapse multi-clause/overloads to one node
|
|
1335
|
+
nodes.push({
|
|
1336
|
+
id, name, filePath, className,
|
|
1337
|
+
isAsync: false,
|
|
1338
|
+
language: spec.language,
|
|
1339
|
+
startIndex: fnNode.startIndex,
|
|
1340
|
+
endIndex: fnNode.endIndex,
|
|
1341
|
+
fanIn: 0, fanOut: 0,
|
|
1342
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, spec.language),
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
const definedNames = new Set(nodes.map(n => n.name));
|
|
1346
|
+
const rawEdges = [];
|
|
1347
|
+
const seen = new Set();
|
|
1348
|
+
for (const match of runQuery(spec.callQuery)) {
|
|
1349
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
1350
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
1351
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
1352
|
+
if (!nameCapture || !nodeCapture)
|
|
1353
|
+
continue;
|
|
1354
|
+
const calleeName = nameCapture.node.text;
|
|
1355
|
+
if (isIgnoredCallee(calleeName))
|
|
1356
|
+
continue;
|
|
1357
|
+
if (spec.callFilter && !spec.callFilter(calleeName, definedNames))
|
|
1358
|
+
continue;
|
|
1359
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
1360
|
+
if (!caller)
|
|
1361
|
+
continue;
|
|
1362
|
+
const calleeObject = objectCapture?.node.text;
|
|
1363
|
+
const key = `${caller.id}\0${calleeName}\0${calleeObject ?? ''}\0${nodeCapture.node.startIndex}`;
|
|
1364
|
+
if (seen.has(key))
|
|
1365
|
+
continue;
|
|
1366
|
+
seen.add(key);
|
|
1367
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject });
|
|
1368
|
+
}
|
|
1369
|
+
return { nodes, rawEdges };
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
// ── C# ──────────────────────────────────────────────────────────────────────
|
|
1373
|
+
const CSHARP_SPEC = {
|
|
1374
|
+
language: 'C#',
|
|
1375
|
+
loader: () => loadGrammarSoft('C#', () => import('tree-sitter-c-sharp'), m => m.default),
|
|
1376
|
+
classTypes: new Set(['class_declaration', 'struct_declaration', 'record_declaration', 'interface_declaration', 'enum_declaration']),
|
|
1377
|
+
fnQuery: `
|
|
1378
|
+
(method_declaration name: (identifier) @fn.name) @fn.node
|
|
1379
|
+
(constructor_declaration name: (identifier) @fn.name) @fn.node
|
|
1380
|
+
(local_function_statement name: (identifier) @fn.name) @fn.node
|
|
1381
|
+
`,
|
|
1382
|
+
// Name-based resolution (matches the codebase's best-effort approach): capture
|
|
1383
|
+
// the callee name only — the object isn't used for resolution in these langs.
|
|
1384
|
+
callQuery: `
|
|
1385
|
+
(invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call.node
|
|
1386
|
+
(invocation_expression function: (identifier) @call.name) @call.node
|
|
1387
|
+
`,
|
|
1388
|
+
};
|
|
1389
|
+
// ── Kotlin ──────────────────────────────────────────────────────────────────
|
|
1390
|
+
const KOTLIN_SPEC = {
|
|
1391
|
+
language: 'Kotlin',
|
|
1392
|
+
loader: () => loadGrammarSoft('Kotlin', () => import('tree-sitter-kotlin'), m => m.default),
|
|
1393
|
+
classTypes: new Set(['class_declaration', 'object_declaration', 'interface_declaration', 'companion_object']),
|
|
1394
|
+
// Extension functions: `fun Foo.bar()` — receiver user_type becomes the className.
|
|
1395
|
+
extraClassName: (fnNode) => {
|
|
1396
|
+
const receiver = fnNode.namedChildren.find(c => c.type === 'user_type');
|
|
1397
|
+
return receiver?.text;
|
|
1398
|
+
},
|
|
1399
|
+
fnQuery: `
|
|
1400
|
+
(function_declaration (simple_identifier) @fn.name) @fn.node
|
|
1401
|
+
`,
|
|
1402
|
+
callQuery: `
|
|
1403
|
+
(call_expression (simple_identifier) @call.name) @call.node
|
|
1404
|
+
(call_expression (navigation_expression (navigation_suffix (simple_identifier) @call.name))) @call.node
|
|
1405
|
+
`,
|
|
1406
|
+
};
|
|
1407
|
+
// ── PHP ─────────────────────────────────────────────────────────────────────
|
|
1408
|
+
const PHP_SPEC = {
|
|
1409
|
+
language: 'PHP',
|
|
1410
|
+
loader: () => loadGrammarSoft('PHP', () => import('tree-sitter-php'), m => m.default.php),
|
|
1411
|
+
classTypes: new Set(['class_declaration', 'trait_declaration', 'interface_declaration', 'enum_declaration']),
|
|
1412
|
+
fnQuery: `
|
|
1413
|
+
(function_definition name: (name) @fn.name) @fn.node
|
|
1414
|
+
(method_declaration name: (name) @fn.name) @fn.node
|
|
1415
|
+
`,
|
|
1416
|
+
callQuery: `
|
|
1417
|
+
(function_call_expression function: (name) @call.name) @call.node
|
|
1418
|
+
(member_call_expression name: (name) @call.name) @call.node
|
|
1419
|
+
(scoped_call_expression name: (name) @call.name) @call.node
|
|
1420
|
+
`,
|
|
1421
|
+
};
|
|
1422
|
+
// ── C ───────────────────────────────────────────────────────────────────────
|
|
1423
|
+
const C_SPEC = {
|
|
1424
|
+
language: 'C',
|
|
1425
|
+
loader: () => loadGrammarSoft('C', () => import('tree-sitter-c'), m => m.default),
|
|
1426
|
+
classTypes: new Set(), // C has no classes — file scope is the implicit grouping
|
|
1427
|
+
fnQuery: `
|
|
1428
|
+
(function_definition declarator: (function_declarator declarator: (identifier) @fn.name)) @fn.node
|
|
1429
|
+
`,
|
|
1430
|
+
callQuery: `
|
|
1431
|
+
(call_expression function: (identifier) @call.name) @call.node
|
|
1432
|
+
`,
|
|
1433
|
+
};
|
|
1434
|
+
// ── Scala ───────────────────────────────────────────────────────────────────
|
|
1435
|
+
const SCALA_SPEC = {
|
|
1436
|
+
language: 'Scala',
|
|
1437
|
+
loader: () => loadGrammarSoft('Scala', () => import('tree-sitter-scala'), m => m.default),
|
|
1438
|
+
classTypes: new Set(['object_definition', 'class_definition', 'trait_definition']),
|
|
1439
|
+
fnQuery: `
|
|
1440
|
+
(function_definition name: (identifier) @fn.name) @fn.node
|
|
1441
|
+
`,
|
|
1442
|
+
callQuery: `
|
|
1443
|
+
(call_expression function: (identifier) @call.name) @call.node
|
|
1444
|
+
(call_expression function: (field_expression field: (identifier) @call.name)) @call.node
|
|
1445
|
+
`,
|
|
1446
|
+
};
|
|
1447
|
+
// ── Lua (via bundled WASM — no ABI-compatible native build for the host) ─────
|
|
1448
|
+
const LUA_SPEC = {
|
|
1449
|
+
language: 'Lua',
|
|
1450
|
+
loader: () => loadWasmGrammarSoft('Lua', 'tree-sitter-wasms/out/tree-sitter-lua.wasm'),
|
|
1451
|
+
classTypes: new Set(),
|
|
1452
|
+
// `function t.f()` / `function t:m()` record the table name in className.
|
|
1453
|
+
extraClassName: (fnNode) => {
|
|
1454
|
+
const nameVar = fnNode.childForFieldName('name');
|
|
1455
|
+
if (nameVar?.type === 'variable')
|
|
1456
|
+
return nameVar.childForFieldName('table')?.text;
|
|
1457
|
+
return undefined;
|
|
1458
|
+
},
|
|
1459
|
+
fnQuery: `
|
|
1460
|
+
(local_function_definition_statement name: (identifier) @fn.name) @fn.node
|
|
1461
|
+
(function_definition_statement name: (identifier) @fn.name) @fn.node
|
|
1462
|
+
(function_definition_statement name: (variable field: (identifier) @fn.name)) @fn.node
|
|
1463
|
+
(function_definition_statement name: (variable method: (identifier) @fn.name)) @fn.node
|
|
1464
|
+
`,
|
|
1465
|
+
callQuery: `
|
|
1466
|
+
(call function: (variable name: (identifier) @call.name)) @call.node
|
|
1467
|
+
(call function: (variable field: (identifier) @call.name)) @call.node
|
|
1468
|
+
(call function: (variable method: (identifier) @call.name)) @call.node
|
|
1469
|
+
`,
|
|
1470
|
+
};
|
|
1471
|
+
// ── Bash ────────────────────────────────────────────────────────────────────
|
|
1472
|
+
const BASH_SPEC = {
|
|
1473
|
+
language: 'Bash',
|
|
1474
|
+
loader: () => loadGrammarSoft('Bash', () => import('tree-sitter-bash'), m => m.default),
|
|
1475
|
+
classTypes: new Set(),
|
|
1476
|
+
// Only edge to project-defined functions, never external binaries (grep/ls/…).
|
|
1477
|
+
callFilter: (calleeName, definedNames) => definedNames.has(calleeName),
|
|
1478
|
+
fnQuery: `
|
|
1479
|
+
(function_definition name: (word) @fn.name) @fn.node
|
|
1480
|
+
`,
|
|
1481
|
+
callQuery: `
|
|
1482
|
+
(command name: (command_name (word) @call.name)) @call.node
|
|
1483
|
+
`,
|
|
1484
|
+
};
|
|
1485
|
+
const QUERY_LANG_SPECS = {
|
|
1486
|
+
'C#': CSHARP_SPEC, 'Kotlin': KOTLIN_SPEC, 'PHP': PHP_SPEC, 'C': C_SPEC,
|
|
1487
|
+
'Scala': SCALA_SPEC, 'Lua': LUA_SPEC, 'Bash': BASH_SPEC,
|
|
1488
|
+
};
|
|
1489
|
+
// ── Dart (via portable WASM + web-tree-sitter) ───────────────────────────────
|
|
1490
|
+
//
|
|
1491
|
+
// No ABI-compatible native Dart grammar exists for the pinned host binding, so
|
|
1492
|
+
// Dart loads the portable `tree-sitter-wasms` WASM through web-tree-sitter
|
|
1493
|
+
// (ABI-agnostic, pure JS/WASM, builds on every platform) — each WASM grammar in
|
|
1494
|
+
// its own module instance (see loadWasmGrammarSoft). Dart's grammar places the
|
|
1495
|
+
// `function_body` as a SIBLING of `function_signature` (not a child), so a
|
|
1496
|
+
// generic query extractor would attribute no calls — hence a custom walk that
|
|
1497
|
+
// spans signature+body.
|
|
1498
|
+
const DART_CLASS_TYPES = new Set(['class_definition', 'mixin_declaration', 'extension_declaration', 'enum_declaration']);
|
|
1499
|
+
async function extractDartGraph(filePath, content) {
|
|
1500
|
+
const handle = await loadWasmGrammarSoft('Dart', 'tree-sitter-wasms/out/tree-sitter-dart.wasm');
|
|
1501
|
+
if (!handle)
|
|
1502
|
+
return { nodes: [], rawEdges: [] };
|
|
1503
|
+
return handle.withTree(content, (root) => {
|
|
1504
|
+
const enclosingClass = (node) => {
|
|
1505
|
+
let c = node.parent;
|
|
1506
|
+
while (c) {
|
|
1507
|
+
if (DART_CLASS_TYPES.has(c.type))
|
|
1508
|
+
return c.childForFieldName('name')?.text;
|
|
1509
|
+
c = c.parent;
|
|
1510
|
+
}
|
|
1511
|
+
return undefined;
|
|
1512
|
+
};
|
|
1513
|
+
const nodes = [];
|
|
1514
|
+
const collectFns = (n) => {
|
|
1515
|
+
if (n.type === 'function_signature') {
|
|
1516
|
+
const nameNode = n.childForFieldName('name');
|
|
1517
|
+
if (nameNode) {
|
|
1518
|
+
// Body is a sibling of the signature (or of its method_signature parent).
|
|
1519
|
+
const unit = n.parent && n.parent.type === 'method_signature' ? n.parent : n;
|
|
1520
|
+
const sib = unit.nextNamedSibling;
|
|
1521
|
+
const endIndex = sib && sib.type === 'function_body' ? sib.endIndex : n.endIndex;
|
|
1522
|
+
const className = enclosingClass(n);
|
|
1523
|
+
const id = className ? `${filePath}::${className}.${nameNode.text}` : `${filePath}::${nameNode.text}`;
|
|
1524
|
+
if (!nodes.some(x => x.id === id)) {
|
|
1525
|
+
nodes.push({
|
|
1526
|
+
id, name: nameNode.text, filePath, className, isAsync: false, language: 'Dart',
|
|
1527
|
+
startIndex: n.startIndex, endIndex, fanIn: 0, fanOut: 0,
|
|
1528
|
+
signature: extractDeclaration(content, n.startIndex, n.endIndex, 'Dart'),
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
for (const c of n.namedChildren)
|
|
1534
|
+
collectFns(c);
|
|
1535
|
+
};
|
|
1536
|
+
collectFns(root);
|
|
1537
|
+
const rawEdges = [];
|
|
1538
|
+
const seen = new Set();
|
|
1539
|
+
const collectCalls = (n) => {
|
|
1540
|
+
if (n.type === 'selector' && n.namedChildren.some(c => c.type === 'argument_part')) {
|
|
1541
|
+
const prev = n.previousNamedSibling;
|
|
1542
|
+
let name;
|
|
1543
|
+
if (prev?.type === 'identifier')
|
|
1544
|
+
name = prev.text;
|
|
1545
|
+
else if (prev?.type === 'selector') {
|
|
1546
|
+
const uas = prev.namedChildren.find(c => c.type === 'unconditional_assignable_selector');
|
|
1547
|
+
name = uas?.namedChildren.find(c => c.type === 'identifier')?.text;
|
|
1548
|
+
}
|
|
1549
|
+
if (name && !isIgnoredCallee(name)) {
|
|
1550
|
+
const caller = findEnclosingFunction(nodes, n.startIndex);
|
|
1551
|
+
if (caller) {
|
|
1552
|
+
const key = `${caller.id}\0${name}\0${n.startIndex}`;
|
|
1553
|
+
if (!seen.has(key)) {
|
|
1554
|
+
seen.add(key);
|
|
1555
|
+
rawEdges.push({ callerId: caller.id, calleeName: name, line: n.startPosition.row + 1 });
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
for (const c of n.namedChildren)
|
|
1561
|
+
collectCalls(c);
|
|
1562
|
+
};
|
|
1563
|
+
collectCalls(root);
|
|
1564
|
+
return { nodes, rawEdges };
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
// ── Elixir (custom walk — everything is a `call` node) ───────────────────────
|
|
1568
|
+
const ELIXIR_DEF_KEYWORDS = new Set(['def', 'defp', 'defmacro', 'defmacrop']);
|
|
1569
|
+
async function extractElixirGraph(filePath, content) {
|
|
1570
|
+
const loaded = await loadGrammarSoft('Elixir', () => import('tree-sitter-elixir'), m => m.default);
|
|
1571
|
+
if (!loaded)
|
|
1572
|
+
return { nodes: [], rawEdges: [] };
|
|
1573
|
+
return loaded.withTree(content, (root) => {
|
|
1574
|
+
const nodes = [];
|
|
1575
|
+
const calls = [];
|
|
1576
|
+
const targetIdent = (call) => {
|
|
1577
|
+
const t = call.childForFieldName('target');
|
|
1578
|
+
return t ?? call.namedChildren[0];
|
|
1579
|
+
};
|
|
1580
|
+
const walk = (node, moduleName) => {
|
|
1581
|
+
if (node.type === 'call') {
|
|
1582
|
+
const target = targetIdent(node);
|
|
1583
|
+
const kw = target?.type === 'identifier' ? target.text : undefined;
|
|
1584
|
+
const args = node.childForFieldName('arguments') ?? node.namedChildren.find(c => c.type === 'arguments');
|
|
1585
|
+
if (kw === 'defmodule') {
|
|
1586
|
+
const aliasNode = args?.namedChildren.find(c => c.type === 'alias');
|
|
1587
|
+
const newModule = aliasNode?.text ?? moduleName;
|
|
1588
|
+
for (const child of node.namedChildren)
|
|
1589
|
+
walk(child, newModule);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
if (kw && ELIXIR_DEF_KEYWORDS.has(kw)) {
|
|
1593
|
+
// First argument is the function head: an identifier (no args) or a call (with args).
|
|
1594
|
+
const head = args?.namedChildren[0];
|
|
1595
|
+
let fnName;
|
|
1596
|
+
let arity = 0;
|
|
1597
|
+
if (head?.type === 'identifier') {
|
|
1598
|
+
fnName = head.text;
|
|
1599
|
+
}
|
|
1600
|
+
else if (head?.type === 'call') {
|
|
1601
|
+
const ht = head.childForFieldName('target') ?? head.namedChildren[0];
|
|
1602
|
+
fnName = ht?.text;
|
|
1603
|
+
const hargs = head.childForFieldName('arguments') ?? head.namedChildren.find(c => c.type === 'arguments');
|
|
1604
|
+
arity = hargs?.namedChildren.length ?? 0;
|
|
1605
|
+
}
|
|
1606
|
+
if (fnName) {
|
|
1607
|
+
const id = moduleName ? `${filePath}::${moduleName}.${fnName}` : `${filePath}::${fnName}`;
|
|
1608
|
+
const existing = nodes.find(n => n.id === id);
|
|
1609
|
+
if (existing) {
|
|
1610
|
+
existing.signature = `${existing.signature} (+clause)`;
|
|
1611
|
+
}
|
|
1612
|
+
else {
|
|
1613
|
+
nodes.push({
|
|
1614
|
+
id, name: fnName, filePath, className: moduleName, isAsync: false,
|
|
1615
|
+
language: 'Elixir', startIndex: node.startIndex, endIndex: node.endIndex,
|
|
1616
|
+
fanIn: 0, fanOut: 0, signature: `${kw} ${fnName}/${arity}`,
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
// Recurse into the body for nested calls.
|
|
1621
|
+
for (const child of node.namedChildren)
|
|
1622
|
+
walk(child, moduleName);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
// Otherwise it's a call site: local `fun(...)` or remote `Mod.fun(...)`.
|
|
1626
|
+
if (target?.type === 'identifier' && !ELIXIR_DEF_KEYWORDS.has(target.text)) {
|
|
1627
|
+
calls.push({ name: target.text, pos: node.startIndex, row: node.startPosition.row });
|
|
1628
|
+
}
|
|
1629
|
+
else if (target?.type === 'dot') {
|
|
1630
|
+
// Remote `Mod.fun(...)`: emit the function name only (no receiver), so
|
|
1631
|
+
// name-based resolution can match an in-project function (matching how
|
|
1632
|
+
// the other spec-08 languages resolve member/static calls).
|
|
1633
|
+
const right = target.childForFieldName('right') ?? target.namedChildren[target.namedChildren.length - 1];
|
|
1634
|
+
if (right)
|
|
1635
|
+
calls.push({ name: right.text, pos: node.startIndex, row: node.startPosition.row });
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
for (const child of node.namedChildren)
|
|
1639
|
+
walk(child, moduleName);
|
|
1640
|
+
};
|
|
1641
|
+
walk(root, undefined);
|
|
1642
|
+
const rawEdges = [];
|
|
1643
|
+
const seen = new Set();
|
|
1644
|
+
for (const c of calls) {
|
|
1645
|
+
if (isIgnoredCallee(c.name))
|
|
1646
|
+
continue;
|
|
1647
|
+
const caller = findEnclosingFunction(nodes, c.pos);
|
|
1648
|
+
if (!caller)
|
|
1649
|
+
continue;
|
|
1650
|
+
const key = `${caller.id}\0${c.name}\0${c.object ?? ''}\0${c.pos}`;
|
|
1651
|
+
if (seen.has(key))
|
|
1652
|
+
continue;
|
|
1653
|
+
seen.add(key);
|
|
1654
|
+
rawEdges.push({ callerId: caller.id, calleeName: c.name, line: c.row + 1, calleeObject: c.object });
|
|
1655
|
+
}
|
|
1656
|
+
return { nodes, rawEdges };
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
// ============================================================================
|
|
1170
1660
|
// CLASS HIERARCHY EXTRACTION
|
|
1171
1661
|
// ============================================================================
|
|
1172
1662
|
/**
|
|
@@ -1537,8 +2027,13 @@ export class CallGraphBuilder {
|
|
|
1537
2027
|
* @param files Source files with path, content, and language
|
|
1538
2028
|
* @param layers Optional layer map { layerName: [path prefix, ...] }
|
|
1539
2029
|
* @param importMap Optional per-file import map (from ImportResolverBridge)
|
|
2030
|
+
* @param resolutionNodes Optional pre-existing nodes used only to seed the
|
|
2031
|
+
* call-resolution trie (not added to the returned nodes/edges). An
|
|
2032
|
+
* incremental subset rebuild passes the full set of known nodes so calls
|
|
2033
|
+
* into files outside the re-parsed subset resolve to their real node
|
|
2034
|
+
* instead of degrading to a synthetic `external::` leaf.
|
|
1540
2035
|
*/
|
|
1541
|
-
async build(files, layers, importMap) {
|
|
2036
|
+
async build(files, layers, importMap, resolutionNodes) {
|
|
1542
2037
|
const allNodes = new Map();
|
|
1543
2038
|
const allRawEdges = [];
|
|
1544
2039
|
// Pass 1: Extract nodes and raw edges from each file
|
|
@@ -1569,6 +2064,16 @@ export class CallGraphBuilder {
|
|
|
1569
2064
|
else if (file.language === 'Swift') {
|
|
1570
2065
|
result = await extractSwiftGraph(file.path, file.content);
|
|
1571
2066
|
}
|
|
2067
|
+
else if (file.language === 'Elixir') {
|
|
2068
|
+
result = await extractElixirGraph(file.path, file.content);
|
|
2069
|
+
}
|
|
2070
|
+
else if (file.language === 'Dart') {
|
|
2071
|
+
result = await extractDartGraph(file.path, file.content);
|
|
2072
|
+
}
|
|
2073
|
+
else if (QUERY_LANG_SPECS[file.language]) {
|
|
2074
|
+
// spec-08 additional languages (C#, Kotlin, PHP, C, Scala, Dart, Lua, Bash).
|
|
2075
|
+
result = await extractByQueries(QUERY_LANG_SPECS[file.language], file.path, file.content);
|
|
2076
|
+
}
|
|
1572
2077
|
else {
|
|
1573
2078
|
continue;
|
|
1574
2079
|
}
|
|
@@ -1607,6 +2112,15 @@ export class CallGraphBuilder {
|
|
|
1607
2112
|
const trie = new FunctionRegistryTrie();
|
|
1608
2113
|
for (const node of allNodes.values())
|
|
1609
2114
|
trie.insert(node);
|
|
2115
|
+
// Seed resolution with pre-existing nodes (incremental subset rebuilds) so
|
|
2116
|
+
// cross-file calls outside the re-parsed subset still resolve internally.
|
|
2117
|
+
// These are NOT added to allNodes, so they never appear in the output.
|
|
2118
|
+
if (resolutionNodes) {
|
|
2119
|
+
for (const node of resolutionNodes) {
|
|
2120
|
+
if (!allNodes.has(node.id) && !node.isExternal)
|
|
2121
|
+
trie.insert(node);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
1610
2124
|
// Build per-function-body content slices for type inference (keyed by functionId)
|
|
1611
2125
|
const fileContents = new Map();
|
|
1612
2126
|
for (const file of files)
|
|
@@ -1750,6 +2264,20 @@ export class CallGraphBuilder {
|
|
|
1750
2264
|
catch {
|
|
1751
2265
|
// HTTP edge extraction is best-effort; don't fail the whole build
|
|
1752
2266
|
}
|
|
2267
|
+
// Pass 2c: Infrastructure-as-Code projection (spec-07).
|
|
2268
|
+
// IaC resources/references project onto the existing node/edge primitives.
|
|
2269
|
+
let iacClasses = [];
|
|
2270
|
+
try {
|
|
2271
|
+
const iac = buildProjectedIac(files);
|
|
2272
|
+
for (const n of iac.nodes)
|
|
2273
|
+
if (!allNodes.has(n.id))
|
|
2274
|
+
allNodes.set(n.id, n);
|
|
2275
|
+
edges.push(...iac.edges);
|
|
2276
|
+
iacClasses = iac.classes;
|
|
2277
|
+
}
|
|
2278
|
+
catch {
|
|
2279
|
+
// IaC extraction is best-effort; never fail the whole build
|
|
2280
|
+
}
|
|
1753
2281
|
// Pass 3: Calculate fanIn / fanOut (count unique caller→callee pairs, not call sites)
|
|
1754
2282
|
const seenPairs = new Set();
|
|
1755
2283
|
for (const edge of edges) {
|
|
@@ -1968,6 +2496,11 @@ export class CallGraphBuilder {
|
|
|
1968
2496
|
// Pass 7: Build class hierarchy (inheritance + grouping)
|
|
1969
2497
|
const relationships = await extractClassRelationships(files);
|
|
1970
2498
|
const { classes, inheritanceEdges } = buildClassNodes(allNodes, relationships);
|
|
2499
|
+
// Merge IaC module groupings (deduped by id) into the class set.
|
|
2500
|
+
const classIds = new Set(classes.map(c => c.id));
|
|
2501
|
+
for (const c of iacClasses)
|
|
2502
|
+
if (!classIds.has(c.id))
|
|
2503
|
+
classes.push(c);
|
|
1971
2504
|
return {
|
|
1972
2505
|
nodes: allNodes,
|
|
1973
2506
|
edges,
|