@optave/codegraph 3.11.1 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +7 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts +2 -0
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +24 -2
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +73 -0
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
- package/dist/domain/graph/builder/call-resolver.js +292 -0
- package/dist/domain/graph/builder/call-resolver.js.map +1 -0
- package/dist/domain/graph/builder/cha.d.ts +61 -0
- package/dist/domain/graph/builder/cha.d.ts.map +1 -0
- package/dist/domain/graph/builder/cha.js +143 -0
- package/dist/domain/graph/builder/cha.js.map +1 -0
- package/dist/domain/graph/builder/context.d.ts +3 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +2 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +17 -1
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +159 -5
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +147 -54
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts +2 -0
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +932 -110
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.js +501 -14
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +9 -0
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/journal.js +1 -1
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/resolver/points-to.d.ts +53 -0
- package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
- package/dist/domain/graph/resolver/points-to.js +213 -0
- package/dist/domain/graph/resolver/points-to.js.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.js +476 -0
- package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +5 -2
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +10 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +39 -7
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +25 -0
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
- package/dist/domain/wasm-worker-pool.js +32 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +14 -1
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
- package/dist/extractors/c.js +3 -3
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/clojure.js +1 -1
- package/dist/extractors/clojure.js.map +1 -1
- package/dist/extractors/cpp.js +3 -3
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/csharp.d.ts.map +1 -1
- package/dist/extractors/csharp.js +37 -8
- package/dist/extractors/csharp.js.map +1 -1
- package/dist/extractors/cuda.js +3 -3
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +6 -6
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/fsharp.js +1 -1
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/go.js +5 -5
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/haskell.js +1 -1
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/java.js +2 -2
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts +2 -0
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +1674 -64
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/kotlin.js +5 -5
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/lua.js +1 -1
- package/dist/extractors/lua.js.map +1 -1
- package/dist/extractors/objc.js +3 -3
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/ocaml.js +1 -1
- package/dist/extractors/ocaml.js.map +1 -1
- package/dist/extractors/php.js +2 -2
- package/dist/extractors/php.js.map +1 -1
- package/dist/extractors/python.js +7 -7
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/ruby.js +2 -2
- package/dist/extractors/ruby.js.map +1 -1
- package/dist/extractors/scala.js +1 -1
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.js +1 -1
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/swift.js +4 -4
- package/dist/extractors/swift.js.map +1 -1
- package/dist/extractors/zig.js +4 -4
- package/dist/extractors/zig.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +121 -16
- package/dist/features/structure.js.map +1 -1
- package/dist/infrastructure/config.d.ts +10 -0
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +15 -0
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/native.d.ts +11 -0
- package/dist/infrastructure/native.d.ts.map +1 -1
- package/dist/infrastructure/native.js +78 -5
- package/dist/infrastructure/native.js.map +1 -1
- package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
- package/dist/presentation/queries-cli/overview.js +5 -0
- package/dist/presentation/queries-cli/overview.js.map +1 -1
- package/dist/types.d.ts +184 -0
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/package.json +9 -9
- package/src/db/migrations.ts +7 -0
- package/src/domain/analysis/module-map.ts +29 -1
- package/src/domain/graph/builder/call-resolver.ts +351 -0
- package/src/domain/graph/builder/cha.ts +175 -0
- package/src/domain/graph/builder/context.ts +3 -0
- package/src/domain/graph/builder/helpers.ts +175 -5
- package/src/domain/graph/builder/incremental.ts +186 -66
- package/src/domain/graph/builder/stages/build-edges.ts +1146 -146
- package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
- package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
- package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
- package/src/domain/graph/journal.ts +1 -1
- package/src/domain/graph/resolver/points-to.ts +254 -0
- package/src/domain/graph/resolver/ts-resolver.ts +536 -0
- package/src/domain/graph/watcher.ts +4 -2
- package/src/domain/parser.ts +43 -5
- package/src/domain/wasm-worker-entry.ts +25 -0
- package/src/domain/wasm-worker-pool.ts +21 -0
- package/src/domain/wasm-worker-protocol.ts +14 -0
- package/src/extractors/c.ts +3 -3
- package/src/extractors/clojure.ts +1 -1
- package/src/extractors/cpp.ts +3 -3
- package/src/extractors/csharp.ts +33 -9
- package/src/extractors/cuda.ts +3 -3
- package/src/extractors/elixir.ts +6 -6
- package/src/extractors/fsharp.ts +1 -1
- package/src/extractors/go.ts +5 -5
- package/src/extractors/haskell.ts +1 -1
- package/src/extractors/java.ts +2 -2
- package/src/extractors/javascript.ts +1802 -66
- package/src/extractors/kotlin.ts +5 -5
- package/src/extractors/lua.ts +1 -1
- package/src/extractors/objc.ts +3 -3
- package/src/extractors/ocaml.ts +1 -1
- package/src/extractors/php.ts +2 -2
- package/src/extractors/python.ts +7 -7
- package/src/extractors/ruby.ts +2 -2
- package/src/extractors/scala.ts +1 -1
- package/src/extractors/solidity.ts +1 -1
- package/src/extractors/swift.ts +4 -4
- package/src/extractors/zig.ts +4 -4
- package/src/features/structure.ts +143 -23
- package/src/infrastructure/config.ts +15 -0
- package/src/infrastructure/native.ts +87 -5
- package/src/presentation/queries-cli/overview.ts +15 -1
- package/src/types.ts +194 -0
|
@@ -7,7 +7,7 @@ import { createHash } from 'node:crypto';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { purgeFilesData } from '../../../db/index.js';
|
|
10
|
-
import { warn } from '../../../infrastructure/logger.js';
|
|
10
|
+
import { debug, warn } from '../../../infrastructure/logger.js';
|
|
11
11
|
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../../shared/constants.js';
|
|
12
12
|
import { compileGlobs, matchesAny } from '../../../shared/globs.js';
|
|
13
13
|
import type {
|
|
@@ -313,9 +313,9 @@ function getEdgeStmt(db: BetterSqlite3Database, chunkSize: number): SqliteStatem
|
|
|
313
313
|
}
|
|
314
314
|
let stmt = cache.get(chunkSize);
|
|
315
315
|
if (!stmt) {
|
|
316
|
-
const ph = '(
|
|
316
|
+
const ph = '(?,?,?,?,?,?)';
|
|
317
317
|
stmt = db.prepare(
|
|
318
|
-
'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic) VALUES ' +
|
|
318
|
+
'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic,technique) VALUES ' +
|
|
319
319
|
Array.from({ length: chunkSize }, () => ph).join(','),
|
|
320
320
|
);
|
|
321
321
|
cache.set(chunkSize, stmt);
|
|
@@ -344,7 +344,7 @@ export function batchInsertNodes(db: BetterSqlite3Database, rows: unknown[][]):
|
|
|
344
344
|
|
|
345
345
|
/**
|
|
346
346
|
* Batch-insert edge rows via multi-value INSERT statements.
|
|
347
|
-
* Each row: [source_id, target_id, kind, confidence, dynamic]
|
|
347
|
+
* Each row: [source_id, target_id, kind, confidence, dynamic, technique]
|
|
348
348
|
*/
|
|
349
349
|
export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]): void {
|
|
350
350
|
if (!rows.length) return;
|
|
@@ -355,8 +355,178 @@ export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]):
|
|
|
355
355
|
const vals: unknown[] = [];
|
|
356
356
|
for (let j = i; j < end; j++) {
|
|
357
357
|
const r = rows[j] as unknown[];
|
|
358
|
-
vals.push(r[0], r[1], r[2], r[3], r[4]);
|
|
358
|
+
vals.push(r[0], r[1], r[2], r[3], r[4], r[5] ?? null);
|
|
359
359
|
}
|
|
360
360
|
stmt.run(...vals);
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* CHA (Class Hierarchy Analysis) post-pass.
|
|
366
|
+
*
|
|
367
|
+
* Expands virtual-dispatch call edges for class hierarchies and interface
|
|
368
|
+
* implementations already present in the DB:
|
|
369
|
+
*
|
|
370
|
+
* 1. Build implementors map: parent/interface → [child/implementing class] from
|
|
371
|
+
* `extends` and `implements` edges.
|
|
372
|
+
* 2. Collect RTA evidence: class nodes that appear as `calls` targets (new X()).
|
|
373
|
+
* 3. Find all `calls` edges to qualified method nodes (name contains '.').
|
|
374
|
+
* 4. For each such call, expand to concrete overrides via the implementors map,
|
|
375
|
+
* filtered by RTA when evidence exists.
|
|
376
|
+
*
|
|
377
|
+
* Used by both the native orchestrator post-pass and the WASM build-edges pass.
|
|
378
|
+
*/
|
|
379
|
+
export function runChaPostPass(db: BetterSqlite3Database): number {
|
|
380
|
+
const hasHierarchy = db
|
|
381
|
+
.prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
|
|
382
|
+
.get();
|
|
383
|
+
if (!hasHierarchy) return 0;
|
|
384
|
+
|
|
385
|
+
const hierarchyRows = db
|
|
386
|
+
.prepare(
|
|
387
|
+
`SELECT src.name AS child_name, tgt.name AS parent_name
|
|
388
|
+
FROM edges e
|
|
389
|
+
JOIN nodes src ON e.source_id = src.id
|
|
390
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
391
|
+
WHERE e.kind IN ('extends', 'implements')`,
|
|
392
|
+
)
|
|
393
|
+
.all() as Array<{ child_name: string; parent_name: string }>;
|
|
394
|
+
|
|
395
|
+
const implementorSets = new Map<string, Set<string>>();
|
|
396
|
+
for (const row of hierarchyRows) {
|
|
397
|
+
let set = implementorSets.get(row.parent_name);
|
|
398
|
+
if (!set) {
|
|
399
|
+
set = new Set<string>();
|
|
400
|
+
implementorSets.set(row.parent_name, set);
|
|
401
|
+
}
|
|
402
|
+
set.add(row.child_name);
|
|
403
|
+
}
|
|
404
|
+
if (implementorSets.size === 0) return 0;
|
|
405
|
+
// Convert to arrays for iteration compatibility with the rest of the function
|
|
406
|
+
const implementors = new Map([...implementorSets.entries()].map(([k, v]) => [k, [...v]]));
|
|
407
|
+
|
|
408
|
+
// RTA: collect class names instantiated via constructor calls (`new X()`).
|
|
409
|
+
let rtaRows = db
|
|
410
|
+
.prepare(
|
|
411
|
+
`SELECT DISTINCT tgt.name
|
|
412
|
+
FROM edges e
|
|
413
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
414
|
+
WHERE e.kind = 'calls' AND tgt.kind = 'class'`,
|
|
415
|
+
)
|
|
416
|
+
.all() as Array<{ name: string }>;
|
|
417
|
+
if (rtaRows.length === 0) {
|
|
418
|
+
// Fallback: some languages (e.g. TypeScript via WASM) record constructor calls as
|
|
419
|
+
// 'function' or 'constructor' kind rather than 'class'. Restrict to names that are
|
|
420
|
+
// actually known class names to avoid treating unrelated function calls like `logger()`
|
|
421
|
+
// as class-instantiation evidence.
|
|
422
|
+
// Include both parent/interface names AND implementor (child) names so that
|
|
423
|
+
// `new UserRepository()` (a child class) is correctly detected as RTA evidence.
|
|
424
|
+
const knownClassNames = [
|
|
425
|
+
...new Set([
|
|
426
|
+
...implementorSets.keys(),
|
|
427
|
+
...[...implementorSets.values()].flatMap((s) => [...s]),
|
|
428
|
+
]),
|
|
429
|
+
];
|
|
430
|
+
if (knownClassNames.length > 0) {
|
|
431
|
+
// Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
|
|
432
|
+
const CHUNK = 999;
|
|
433
|
+
for (let i = 0; i < knownClassNames.length; i += CHUNK) {
|
|
434
|
+
const chunk = knownClassNames.slice(i, i + CHUNK);
|
|
435
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
436
|
+
const chunkRows = db
|
|
437
|
+
.prepare(
|
|
438
|
+
`SELECT DISTINCT tgt.name
|
|
439
|
+
FROM edges e
|
|
440
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
441
|
+
WHERE e.kind = 'calls' AND tgt.kind IN ('constructor', 'function')
|
|
442
|
+
AND tgt.name IN (${placeholders})`,
|
|
443
|
+
)
|
|
444
|
+
.all(...chunk) as Array<{ name: string }>;
|
|
445
|
+
rtaRows = rtaRows.concat(chunkRows);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const instantiated = new Set(rtaRows.map((r) => r.name));
|
|
450
|
+
const noRtaEvidence = instantiated.size === 0;
|
|
451
|
+
if (noRtaEvidence) {
|
|
452
|
+
debug('runChaPostPass: no constructor-call evidence — proceeding without RTA filter');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const callToMethods = db
|
|
456
|
+
.prepare(
|
|
457
|
+
`SELECT e.source_id, tgt.name AS method_name
|
|
458
|
+
FROM edges e
|
|
459
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
460
|
+
WHERE e.kind = 'calls' AND tgt.kind = 'method'
|
|
461
|
+
AND INSTR(tgt.name, '.') > 0`,
|
|
462
|
+
)
|
|
463
|
+
.all() as Array<{ source_id: number; method_name: string }>;
|
|
464
|
+
|
|
465
|
+
const seen = new Set<string>();
|
|
466
|
+
// Scope deduplication to only the source_ids we are about to expand, avoiding
|
|
467
|
+
// a full-table scan. CHA only inserts edges FROM callers that already call a
|
|
468
|
+
// qualified method (the source_ids in callToMethods), so we only need to
|
|
469
|
+
// check existing edges for those specific callers.
|
|
470
|
+
const callerIds = [...new Set(callToMethods.map((r) => r.source_id))];
|
|
471
|
+
if (callerIds.length > 0) {
|
|
472
|
+
// Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
|
|
473
|
+
const CHUNK = 999;
|
|
474
|
+
for (let i = 0; i < callerIds.length; i += CHUNK) {
|
|
475
|
+
const chunk = callerIds.slice(i, i + CHUNK);
|
|
476
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
477
|
+
const existingPairs = db
|
|
478
|
+
.prepare(
|
|
479
|
+
`SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND source_id IN (${placeholders})`,
|
|
480
|
+
)
|
|
481
|
+
.all(...chunk) as Array<{ source_id: number; target_id: number }>;
|
|
482
|
+
for (const e of existingPairs) seen.add(`${e.source_id}|${e.target_id}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// No LIMIT: multiple files can define the same qualified name in a monorepo.
|
|
487
|
+
const findMethodStmt = db.prepare(`SELECT id FROM nodes WHERE name = ? AND kind = 'method'`);
|
|
488
|
+
const newEdges: Array<[number, number, string, number, number, string]> = [];
|
|
489
|
+
|
|
490
|
+
for (const { source_id, method_name } of callToMethods) {
|
|
491
|
+
const dotIdx = method_name.indexOf('.');
|
|
492
|
+
if (dotIdx === -1) continue;
|
|
493
|
+
const typeName = method_name.slice(0, dotIdx);
|
|
494
|
+
const methodSuffix = method_name.slice(dotIdx + 1);
|
|
495
|
+
|
|
496
|
+
// BFS over the implementors map — handles multi-level hierarchies where
|
|
497
|
+
// abstract/non-instantiated classes sit between the call-site type and
|
|
498
|
+
// the concrete leaf implementations (matches runPostNativeCha, issue #1311).
|
|
499
|
+
const bfsQueue: string[] = [typeName];
|
|
500
|
+
const bfsVisited = new Set<string>([typeName]);
|
|
501
|
+
while (bfsQueue.length > 0) {
|
|
502
|
+
const current = bfsQueue.shift()!;
|
|
503
|
+
const children = implementors.get(current);
|
|
504
|
+
if (!children?.length) continue;
|
|
505
|
+
|
|
506
|
+
for (const cls of children) {
|
|
507
|
+
if (bfsVisited.has(cls)) continue;
|
|
508
|
+
bfsVisited.add(cls);
|
|
509
|
+
|
|
510
|
+
if (noRtaEvidence || instantiated.has(cls)) {
|
|
511
|
+
const qualifiedName = `${cls}.${methodSuffix}`;
|
|
512
|
+
const methodNodes = findMethodStmt.all(qualifiedName) as Array<{ id: number }>;
|
|
513
|
+
for (const methodNode of methodNodes) {
|
|
514
|
+
const key = `${source_id}|${methodNode.id}`;
|
|
515
|
+
if (seen.has(key)) continue;
|
|
516
|
+
seen.add(key);
|
|
517
|
+
newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Always traverse children — non-instantiated classes may have instantiated subclasses.
|
|
522
|
+
bfsQueue.push(cls);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (newEdges.length > 0) {
|
|
528
|
+
db.transaction(() => batchInsertEdges(db, newEdges))();
|
|
529
|
+
debug(`runChaPostPass: inserted ${newEdges.length} CHA dispatch edge(s)`);
|
|
530
|
+
}
|
|
531
|
+
return newEdges.length;
|
|
532
|
+
}
|
|
@@ -21,6 +21,12 @@ import type {
|
|
|
21
21
|
} from '../../../types.js';
|
|
22
22
|
import { parseFileIncremental } from '../../parser.js';
|
|
23
23
|
import { computeConfidence, resolveImportPath } from '../resolve.js';
|
|
24
|
+
import {
|
|
25
|
+
type CallNodeLookup,
|
|
26
|
+
findCaller,
|
|
27
|
+
resolveCallTargets,
|
|
28
|
+
resolveReceiverEdge,
|
|
29
|
+
} from './call-resolver.js';
|
|
24
30
|
import { BUILTIN_RECEIVERS, readFileSafe } from './helpers.js';
|
|
25
31
|
|
|
26
32
|
// ── Local types ─────────────────────────────────────────────────────────
|
|
@@ -185,8 +191,9 @@ function rebuildReverseDepEdges(
|
|
|
185
191
|
aliases,
|
|
186
192
|
skipBarrel ? null : db,
|
|
187
193
|
);
|
|
188
|
-
const importedNames = buildImportedNamesMap(symbols, rootDir, depRelPath, aliases);
|
|
189
|
-
edgesAdded += buildCallEdges(stmts, depRelPath, symbols, fileNodeRow, importedNames);
|
|
194
|
+
const importedNames = buildImportedNamesMap(symbols, rootDir, depRelPath, aliases, db);
|
|
195
|
+
edgesAdded += buildCallEdges(db, stmts, depRelPath, symbols, fileNodeRow, importedNames);
|
|
196
|
+
edgesAdded += buildClassHierarchyEdges(stmts, depRelPath, symbols);
|
|
190
197
|
return edgesAdded;
|
|
191
198
|
}
|
|
192
199
|
|
|
@@ -353,7 +360,13 @@ function emitEdgesForImport(
|
|
|
353
360
|
const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
|
|
354
361
|
if (!targetRow) return 0;
|
|
355
362
|
|
|
356
|
-
const edgeKind = imp.reexport
|
|
363
|
+
const edgeKind = imp.reexport
|
|
364
|
+
? 'reexports'
|
|
365
|
+
: imp.typeOnly
|
|
366
|
+
? 'imports-type'
|
|
367
|
+
: imp.dynamicImport
|
|
368
|
+
? 'dynamic-imports'
|
|
369
|
+
: 'imports';
|
|
357
370
|
stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
|
|
358
371
|
let edgesAdded = 1;
|
|
359
372
|
|
|
@@ -387,6 +400,7 @@ function buildImportedNamesMap(
|
|
|
387
400
|
rootDir: string,
|
|
388
401
|
relPath: string,
|
|
389
402
|
aliases: PathAliases,
|
|
403
|
+
db: BetterSqlite3Database,
|
|
390
404
|
): Map<string, string> {
|
|
391
405
|
const importedNames = new Map<string, string>();
|
|
392
406
|
for (const imp of symbols.imports) {
|
|
@@ -397,78 +411,79 @@ function buildImportedNamesMap(
|
|
|
397
411
|
aliases,
|
|
398
412
|
);
|
|
399
413
|
for (const name of imp.names) {
|
|
400
|
-
|
|
414
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
415
|
+
// Mirror full-build's `buildImportedNamesMap`: follow barrel re-exports so
|
|
416
|
+
// `importedNames` maps to the *defining* file, not the barrel. This ensures
|
|
417
|
+
// `computeConfidence` gets `importedFrom === targetFile` and returns 1.0
|
|
418
|
+
// instead of the cross-directory fallback (0.3).
|
|
419
|
+
let targetFile = resolvedPath;
|
|
420
|
+
if (isBarrelFile(db, resolvedPath)) {
|
|
421
|
+
const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
|
|
422
|
+
if (actual) targetFile = actual;
|
|
423
|
+
}
|
|
424
|
+
importedNames.set(cleanName, targetFile);
|
|
401
425
|
}
|
|
402
426
|
}
|
|
403
427
|
return importedNames;
|
|
404
428
|
}
|
|
405
429
|
|
|
406
|
-
// ──
|
|
430
|
+
// ── Class hierarchy edges ───────────────────────────────────────────────
|
|
407
431
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
): { id: number } | null {
|
|
414
|
-
let caller: { id: number } | null = null;
|
|
415
|
-
let callerSpan = Infinity;
|
|
416
|
-
for (const def of definitions) {
|
|
417
|
-
if (def.line <= call.line) {
|
|
418
|
-
const end = def.endLine || Infinity;
|
|
419
|
-
if (call.line <= end) {
|
|
420
|
-
const span = end - def.line;
|
|
421
|
-
if (span < callerSpan) {
|
|
422
|
-
const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
423
|
-
if (row) {
|
|
424
|
-
caller = row;
|
|
425
|
-
callerSpan = span;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return caller;
|
|
432
|
-
}
|
|
432
|
+
type NodeWithKind = { id: number; kind: string; file: string };
|
|
433
|
+
|
|
434
|
+
const HIERARCHY_SOURCE_KINDS = new Set(['class', 'struct', 'record', 'enum']);
|
|
435
|
+
const EXTENDS_TARGET_KINDS = new Set(['class', 'struct', 'trait', 'record']);
|
|
436
|
+
const IMPLEMENTS_TARGET_KINDS = new Set(['interface', 'trait', 'class']);
|
|
433
437
|
|
|
434
|
-
function
|
|
438
|
+
function buildClassHierarchyEdges(
|
|
435
439
|
stmts: IncrementalStmts,
|
|
436
|
-
call: ExtractorOutput['calls'][number],
|
|
437
440
|
relPath: string,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
441
|
+
symbols: ExtractorOutput,
|
|
442
|
+
): number {
|
|
443
|
+
let edgesAdded = 0;
|
|
444
|
+
for (const cls of symbols.classes) {
|
|
445
|
+
const sourceRow = (stmts.findNodeInFile.all(cls.name, relPath) as NodeWithKind[]).find((n) =>
|
|
446
|
+
HIERARCHY_SOURCE_KINDS.has(n.kind),
|
|
447
|
+
);
|
|
448
|
+
if (!sourceRow) continue;
|
|
449
|
+
|
|
450
|
+
if (cls.extends) {
|
|
451
|
+
for (const t of (stmts.findNodeByName.all(cls.extends) as NodeWithKind[]).filter((n) =>
|
|
452
|
+
EXTENDS_TARGET_KINDS.has(n.kind),
|
|
453
|
+
)) {
|
|
454
|
+
stmts.insertEdge.run(sourceRow.id, t.id, 'extends', 1.0, 0);
|
|
455
|
+
edgesAdded++;
|
|
456
|
+
}
|
|
453
457
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
: (typeEntry as { type?: string }).type
|
|
462
|
-
: null;
|
|
463
|
-
if (typeName) {
|
|
464
|
-
const qualified = `${typeName}.${call.name}`;
|
|
465
|
-
targets = stmts.findNodeByName.all(qualified) as Array<{ id: number; file: string }>;
|
|
458
|
+
if (cls.implements) {
|
|
459
|
+
for (const t of (stmts.findNodeByName.all(cls.implements) as NodeWithKind[]).filter((n) =>
|
|
460
|
+
IMPLEMENTS_TARGET_KINDS.has(n.kind),
|
|
461
|
+
)) {
|
|
462
|
+
stmts.insertEdge.run(sourceRow.id, t.id, 'implements', 1.0, 0);
|
|
463
|
+
edgesAdded++;
|
|
464
|
+
}
|
|
466
465
|
}
|
|
467
466
|
}
|
|
468
|
-
return
|
|
467
|
+
return edgesAdded;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── Call edge building ──────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
function makeIncrementalLookup(db: BetterSqlite3Database, stmts: IncrementalStmts): CallNodeLookup {
|
|
473
|
+
return {
|
|
474
|
+
byNameAndFile: (name, file) =>
|
|
475
|
+
stmts.findNodeInFile.all(name, file) as Array<{ id: number; file: string; kind?: string }>,
|
|
476
|
+
byName: (name) =>
|
|
477
|
+
stmts.findNodeByName.all(name) as Array<{ id: number; file: string; kind?: string }>,
|
|
478
|
+
isBarrel: (file) => isBarrelFile(db, file),
|
|
479
|
+
resolveBarrel: (barrelFile, symbolName) => resolveBarrelTarget(db, barrelFile, symbolName),
|
|
480
|
+
nodeId: (name, kind, file, line) =>
|
|
481
|
+
stmts.getNodeId.get(name, kind, file, line) as { id: number } | undefined,
|
|
482
|
+
};
|
|
469
483
|
}
|
|
470
484
|
|
|
471
485
|
function buildCallEdges(
|
|
486
|
+
db: BetterSqlite3Database,
|
|
472
487
|
stmts: IncrementalStmts,
|
|
473
488
|
relPath: string,
|
|
474
489
|
symbols: ExtractorOutput,
|
|
@@ -487,26 +502,130 @@ function buildCallEdges(
|
|
|
487
502
|
]),
|
|
488
503
|
)
|
|
489
504
|
: new Map();
|
|
505
|
+
|
|
506
|
+
// Phase 8.3f: seed typeMap[callee::restName] = { type: argName } from
|
|
507
|
+
// objectRestParamBindings × paramBindings, mirroring buildObjectRestParamPostPass.
|
|
508
|
+
// Scoped keys prevent same-name rest-param collisions when two functions in
|
|
509
|
+
// the same file both use `...rest` (#1358). The unscoped key is also seeded
|
|
510
|
+
// when only one callee uses a given rest name, preserving resolution when
|
|
511
|
+
// callerName is null (findCaller couldn't identify the enclosing function).
|
|
512
|
+
if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) {
|
|
513
|
+
const restNameCallees = new Map<string, Set<string>>();
|
|
514
|
+
for (const orpb of symbols.objectRestParamBindings) {
|
|
515
|
+
if (!restNameCallees.has(orpb.restName)) restNameCallees.set(orpb.restName, new Set());
|
|
516
|
+
restNameCallees.get(orpb.restName)!.add(orpb.callee);
|
|
517
|
+
}
|
|
518
|
+
for (const orpb of symbols.objectRestParamBindings) {
|
|
519
|
+
for (const pb of symbols.paramBindings) {
|
|
520
|
+
if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
|
|
521
|
+
const scopedKey = `${orpb.callee}::${orpb.restName}`;
|
|
522
|
+
if (!typeMap.has(scopedKey)) {
|
|
523
|
+
typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
|
|
524
|
+
if (restNameCallees.get(orpb.restName)!.size === 1 && !typeMap.has(orpb.restName)) {
|
|
525
|
+
typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const seenCallEdges = new Set<string>();
|
|
534
|
+
const lookup = makeIncrementalLookup(db, stmts);
|
|
490
535
|
let edgesAdded = 0;
|
|
536
|
+
|
|
491
537
|
for (const call of symbols.calls) {
|
|
492
538
|
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
|
|
493
539
|
|
|
494
|
-
const caller = findCaller(call, symbols.definitions, relPath,
|
|
495
|
-
const { targets, importedFrom } = resolveCallTargets(
|
|
496
|
-
|
|
540
|
+
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
|
|
541
|
+
const { targets: initialTargets, importedFrom } = resolveCallTargets(
|
|
542
|
+
lookup,
|
|
497
543
|
call,
|
|
498
544
|
relPath,
|
|
499
545
|
importedNames,
|
|
500
546
|
typeMap,
|
|
547
|
+
caller.callerName,
|
|
501
548
|
);
|
|
549
|
+
let targets = initialTargets;
|
|
550
|
+
|
|
551
|
+
if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
|
|
552
|
+
const dotIdx = caller.callerName.indexOf('.');
|
|
553
|
+
if (dotIdx > 0) {
|
|
554
|
+
const className = caller.callerName.slice(0, dotIdx);
|
|
555
|
+
const qualifiedName = `${className}.${call.name}`;
|
|
556
|
+
const qualified = lookup
|
|
557
|
+
.byNameAndFile(qualifiedName, relPath)
|
|
558
|
+
.filter((n) => n.kind === 'method');
|
|
559
|
+
if (qualified.length > 0) {
|
|
560
|
+
targets = qualified;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (
|
|
566
|
+
targets.length === 0 &&
|
|
567
|
+
call.receiver === 'this' &&
|
|
568
|
+
caller.callerName != null &&
|
|
569
|
+
symbols.definePropertyReceivers
|
|
570
|
+
) {
|
|
571
|
+
const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName);
|
|
572
|
+
if (receiverVarName) {
|
|
573
|
+
const typeEntry = typeMap.get(receiverVarName);
|
|
574
|
+
const typeName = typeEntry
|
|
575
|
+
? typeof typeEntry === 'string'
|
|
576
|
+
? typeEntry
|
|
577
|
+
: (typeEntry as { type?: string }).type
|
|
578
|
+
: null;
|
|
579
|
+
if (typeName) {
|
|
580
|
+
const qualifiedName = `${typeName}.${call.name}`;
|
|
581
|
+
const qualified = lookup.byNameAndFile(qualifiedName, relPath);
|
|
582
|
+
if (qualified.length > 0) {
|
|
583
|
+
targets = [...qualified];
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (targets.length === 0) {
|
|
587
|
+
// Narrow to function/method kinds only to avoid matching unrelated
|
|
588
|
+
// variables or classes that share a name in the same file.
|
|
589
|
+
const sameFile = lookup
|
|
590
|
+
.byNameAndFile(call.name, relPath)
|
|
591
|
+
.filter((n) => n.kind === 'function' || n.kind === 'method');
|
|
592
|
+
if (sameFile.length > 0) {
|
|
593
|
+
targets = [...sameFile];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
502
598
|
|
|
503
599
|
for (const t of targets) {
|
|
504
|
-
|
|
600
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
601
|
+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
|
|
602
|
+
seenCallEdges.add(edgeKey);
|
|
505
603
|
const confidence = computeConfidence(relPath, t.file, importedFrom ?? null);
|
|
506
604
|
stmts.insertEdge.run(caller.id, t.id, 'calls', confidence, call.dynamic ? 1 : 0);
|
|
507
605
|
edgesAdded++;
|
|
508
606
|
}
|
|
509
607
|
}
|
|
608
|
+
|
|
609
|
+
if (
|
|
610
|
+
call.receiver &&
|
|
611
|
+
!BUILTIN_RECEIVERS.has(call.receiver) &&
|
|
612
|
+
call.receiver !== 'this' &&
|
|
613
|
+
call.receiver !== 'self' &&
|
|
614
|
+
call.receiver !== 'super'
|
|
615
|
+
) {
|
|
616
|
+
const recv = resolveReceiverEdge(
|
|
617
|
+
lookup,
|
|
618
|
+
{ name: call.name, receiver: call.receiver },
|
|
619
|
+
caller,
|
|
620
|
+
relPath,
|
|
621
|
+
typeMap,
|
|
622
|
+
seenCallEdges,
|
|
623
|
+
);
|
|
624
|
+
if (recv) {
|
|
625
|
+
stmts.insertEdge.run(recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0);
|
|
626
|
+
edgesAdded++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
510
629
|
}
|
|
511
630
|
return edgesAdded;
|
|
512
631
|
}
|
|
@@ -549,8 +668,9 @@ function rebuildEdgesForTargetFile(
|
|
|
549
668
|
let edgesAdded = buildContainmentEdges(db, stmts, relPath, symbols);
|
|
550
669
|
edgesAdded += rebuildDirContainment(db, stmts, relPath);
|
|
551
670
|
edgesAdded += buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeRow.id, aliases, db);
|
|
552
|
-
const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases);
|
|
553
|
-
edgesAdded += buildCallEdges(stmts, relPath, symbols, fileNodeRow, importedNames);
|
|
671
|
+
const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases, db);
|
|
672
|
+
edgesAdded += buildCallEdges(db, stmts, relPath, symbols, fileNodeRow, importedNames);
|
|
673
|
+
edgesAdded += buildClassHierarchyEdges(stmts, relPath, symbols);
|
|
554
674
|
return edgesAdded;
|
|
555
675
|
}
|
|
556
676
|
|