@optave/codegraph 3.11.2 → 3.13.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 +73 -37
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +2 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +1 -0
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +272 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/triage.js +1 -1
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/shared/options.d.ts +2 -1
- package/dist/cli/shared/options.d.ts.map +1 -1
- package/dist/cli/shared/options.js +11 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +8 -1
- 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 +16 -10
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
- package/dist/domain/graph/builder/call-resolver.js +251 -34
- package/dist/domain/graph/builder/call-resolver.js.map +1 -1
- package/dist/domain/graph/builder/cha.d.ts +69 -0
- package/dist/domain/graph/builder/cha.d.ts.map +1 -0
- package/dist/domain/graph/builder/cha.js +158 -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 +25 -1
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +178 -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 +74 -2
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +37 -2
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +704 -34
- 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 +3 -2
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +4 -0
- package/dist/domain/graph/builder/stages/finalize.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 +783 -37
- 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 +10 -1
- 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/parser.d.ts +12 -4
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +83 -20
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +35 -2
- 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 +34 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +15 -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.d.ts.map +1 -1
- package/dist/extractors/cpp.js +45 -4
- 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.d.ts.map +1 -1
- package/dist/extractors/cuda.js +45 -4
- 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/helpers.d.ts +11 -0
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +40 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +10 -9
- 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 +1812 -71
- 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-query.d.ts +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +6 -6
- package/dist/features/structure-query.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/config.d.ts +85 -2
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +408 -19
- 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/infrastructure/registry.d.ts +27 -0
- package/dist/infrastructure/registry.d.ts.map +1 -1
- package/dist/infrastructure/registry.js +59 -1
- package/dist/infrastructure/registry.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/presentation/structure.d.ts +1 -1
- package/dist/presentation/structure.d.ts.map +1 -1
- package/dist/presentation/structure.js +2 -2
- package/dist/presentation/structure.js.map +1 -1
- package/dist/types.d.ts +221 -0
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +7 -8
- package/src/cli/commands/audit.ts +2 -1
- package/src/cli/commands/batch.ts +1 -0
- package/src/cli/commands/build.ts +6 -1
- package/src/cli/commands/config.ts +353 -0
- package/src/cli/commands/triage.ts +1 -1
- package/src/cli/index.ts +10 -0
- package/src/cli/shared/options.ts +11 -1
- package/src/cli/types.ts +2 -0
- package/src/db/migrations.ts +8 -1
- package/src/domain/analysis/module-map.ts +29 -1
- package/src/domain/graph/builder/call-resolver.ts +263 -35
- package/src/domain/graph/builder/cha.ts +192 -0
- package/src/domain/graph/builder/context.ts +3 -0
- package/src/domain/graph/builder/helpers.ts +195 -5
- package/src/domain/graph/builder/incremental.ts +80 -1
- package/src/domain/graph/builder/pipeline.ts +49 -2
- package/src/domain/graph/builder/stages/build-edges.ts +867 -32
- package/src/domain/graph/builder/stages/detect-changes.ts +4 -2
- package/src/domain/graph/builder/stages/finalize.ts +4 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +910 -43
- package/src/domain/graph/builder/stages/resolve-imports.ts +15 -1
- 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/parser.ts +86 -17
- package/src/domain/wasm-worker-entry.ts +35 -2
- package/src/domain/wasm-worker-pool.ts +22 -0
- package/src/domain/wasm-worker-protocol.ts +15 -0
- package/src/extractors/c.ts +3 -3
- package/src/extractors/clojure.ts +1 -1
- package/src/extractors/cpp.ts +47 -4
- package/src/extractors/csharp.ts +33 -9
- package/src/extractors/cuda.ts +47 -4
- 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/helpers.ts +43 -0
- package/src/extractors/java.ts +10 -9
- package/src/extractors/javascript.ts +1929 -72
- 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-query.ts +7 -7
- package/src/index.ts +5 -1
- package/src/infrastructure/config.ts +494 -20
- package/src/infrastructure/native.ts +87 -5
- package/src/infrastructure/registry.ts +82 -1
- package/src/presentation/queries-cli/overview.ts +15 -1
- package/src/presentation/structure.ts +3 -3
- package/src/types.ts +235 -0
- package/grammars/tree-sitter-erlang.wasm +0 -0
|
@@ -41,9 +41,16 @@ import {
|
|
|
41
41
|
NATIVE_SUPPORTED_EXTENSIONS,
|
|
42
42
|
parseFilesWasmForBackfill,
|
|
43
43
|
} from '../../../parser.js';
|
|
44
|
+
import { computeConfidence } from '../../resolve.js';
|
|
45
|
+
import type { CallNodeLookup } from '../call-resolver.js';
|
|
46
|
+
import type { ChaContext } from '../cha.js';
|
|
47
|
+
import { resolveThisDispatch } from '../cha.js';
|
|
44
48
|
import type { PipelineContext } from '../context.js';
|
|
45
49
|
import {
|
|
50
|
+
batchInsertEdges,
|
|
46
51
|
batchInsertNodes,
|
|
52
|
+
CHA_DISPATCH_PENALTY,
|
|
53
|
+
CHA_TYPED_DISPATCH_CONFIDENCE,
|
|
47
54
|
collectFiles as collectFilesUtil,
|
|
48
55
|
fileHash,
|
|
49
56
|
fileStat,
|
|
@@ -382,11 +389,630 @@ async function runPostNativeAnalysis(
|
|
|
382
389
|
return timing;
|
|
383
390
|
}
|
|
384
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Phase 8.6: CHA expansion post-pass for the native orchestrator path.
|
|
394
|
+
*
|
|
395
|
+
* The Rust build pipeline resolves typed receiver calls (e.g. `worker.doWork()`
|
|
396
|
+
* where `worker: IWorker`) to the interface method declaration only. This
|
|
397
|
+
* post-pass reads the class hierarchy (via `implements`/`extends` edges) and
|
|
398
|
+
* instantiated types (via `calls` edges to class nodes) from the DB and expands
|
|
399
|
+
* each call to an interface/abstract method to ALL RTA-filtered concrete
|
|
400
|
+
* implementations.
|
|
401
|
+
*
|
|
402
|
+
* Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
|
|
403
|
+
* which WASM-re-parses JS/TS files to obtain raw call site receiver info.
|
|
404
|
+
*
|
|
405
|
+
* `changedFiles` controls candidate scoping on incremental builds:
|
|
406
|
+
* - null → full build; scan all call→method edges (existing behaviour).
|
|
407
|
+
* - array → incremental; two cheap gate queries decide scope:
|
|
408
|
+
* Gate A: any class/interface/trait/struct/record nodes in changed files?
|
|
409
|
+
* If yes, a new implementor may have appeared — full scan required.
|
|
410
|
+
* Gate B: any `calls` edges from changed-file sources targeting
|
|
411
|
+
* class/constructor/function-kind nodes? If yes, the RTA set may
|
|
412
|
+
* have grown (also covers the older-schema fallback where
|
|
413
|
+
* constructor calls target `constructor`/`function` nodes instead
|
|
414
|
+
* of `class` nodes) — full scan required.
|
|
415
|
+
* If neither gate fires: scope `callToMethods` to `src.file IN changedFiles`
|
|
416
|
+
* (safe because no hierarchy or RTA evidence changed).
|
|
417
|
+
*
|
|
418
|
+
* Returns the count of newly inserted CHA edges plus the set of files containing
|
|
419
|
+
* the new edges' endpoints, so the caller can scope role re-classification to the
|
|
420
|
+
* nodes whose fan-in/out actually changed. A zero count means no edges were added
|
|
421
|
+
* and role re-classification is unnecessary.
|
|
422
|
+
*/
|
|
423
|
+
function runPostNativeCha(
|
|
424
|
+
db: BetterSqlite3Database,
|
|
425
|
+
changedFiles: string[] | null,
|
|
426
|
+
): {
|
|
427
|
+
newEdgeCount: number;
|
|
428
|
+
affectedFiles: Set<string>;
|
|
429
|
+
} {
|
|
430
|
+
const affectedFiles = new Set<string>();
|
|
431
|
+
const empty = { newEdgeCount: 0, affectedFiles };
|
|
432
|
+
// Fast guard: no hierarchy edges → no CHA work
|
|
433
|
+
const hasHierarchy = db
|
|
434
|
+
.prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
|
|
435
|
+
.get();
|
|
436
|
+
if (!hasHierarchy) return empty;
|
|
437
|
+
|
|
438
|
+
// Build implementors map: parent/interface name → [child/implementing class names]
|
|
439
|
+
const hierarchyRows = db
|
|
440
|
+
.prepare(`
|
|
441
|
+
SELECT src.name AS child_name, tgt.name AS parent_name
|
|
442
|
+
FROM edges e
|
|
443
|
+
JOIN nodes src ON e.source_id = src.id
|
|
444
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
445
|
+
WHERE e.kind IN ('extends', 'implements')
|
|
446
|
+
`)
|
|
447
|
+
.all() as Array<{ child_name: string; parent_name: string }>;
|
|
448
|
+
|
|
449
|
+
const implementors = new Map<string, string[]>();
|
|
450
|
+
for (const row of hierarchyRows) {
|
|
451
|
+
let list = implementors.get(row.parent_name);
|
|
452
|
+
if (!list) {
|
|
453
|
+
list = [];
|
|
454
|
+
implementors.set(row.parent_name, list);
|
|
455
|
+
}
|
|
456
|
+
if (!list.includes(row.child_name)) list.push(row.child_name);
|
|
457
|
+
}
|
|
458
|
+
if (implementors.size === 0) return empty;
|
|
459
|
+
|
|
460
|
+
// RTA: collect class names that are actually instantiated via `new X()`.
|
|
461
|
+
// Primary query targets `class`-kind nodes (the canonical schema).
|
|
462
|
+
// Fallback also matches `constructor`/`function`-kind nodes because some native
|
|
463
|
+
// engine versions record constructor calls against those kinds instead of `class`.
|
|
464
|
+
let rtaRows = db
|
|
465
|
+
.prepare(`
|
|
466
|
+
SELECT DISTINCT tgt.name
|
|
467
|
+
FROM edges e
|
|
468
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
469
|
+
WHERE e.kind = 'calls' AND tgt.kind = 'class'
|
|
470
|
+
`)
|
|
471
|
+
.all() as Array<{ name: string }>;
|
|
472
|
+
if (rtaRows.length === 0) {
|
|
473
|
+
// Fallback: try constructor/function-kind nodes for older native engine schemas
|
|
474
|
+
rtaRows = db
|
|
475
|
+
.prepare(`
|
|
476
|
+
SELECT DISTINCT tgt.name
|
|
477
|
+
FROM edges e
|
|
478
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
479
|
+
WHERE e.kind = 'calls' AND tgt.kind IN ('constructor', 'function')
|
|
480
|
+
AND INSTR(tgt.name, '.') = 0
|
|
481
|
+
`)
|
|
482
|
+
.all() as Array<{ name: string }>;
|
|
483
|
+
}
|
|
484
|
+
const instantiated = new Set(rtaRows.map((r) => r.name));
|
|
485
|
+
// noRtaEvidence: true when no constructor-call evidence exists in the DB (e.g. graph
|
|
486
|
+
// built by an older native engine that doesn't emit constructor call edges at all).
|
|
487
|
+
// In that case we skip RTA filtering so interface dispatch still produces edges —
|
|
488
|
+
// all instantiated implementors are admitted rather than silently dropping everything.
|
|
489
|
+
const noRtaEvidence = instantiated.size === 0;
|
|
490
|
+
if (noRtaEvidence) {
|
|
491
|
+
debug('runPostNativeCha: no constructor-call evidence found — proceeding without RTA filter');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ── Incremental candidate scoping ──────────────────────────────────────────
|
|
495
|
+
// On incremental builds, two gate queries decide whether to restrict the
|
|
496
|
+
// candidate scan to changed-file call sites or run the full graph scan.
|
|
497
|
+
//
|
|
498
|
+
// Gate A: did a changed file add/change a class hierarchy node?
|
|
499
|
+
// A new `extends`/`implements` edge means a previously-untracked implementor
|
|
500
|
+
// is now in the hierarchy — unchanged call sites in OTHER files may gain new
|
|
501
|
+
// valid expansions, so the full scan is required.
|
|
502
|
+
// Note: *removed* class nodes are safe — Rust's `purge_changed_files` runs
|
|
503
|
+
// before this post-pass and deletes stale nodes and their hierarchy edges, so
|
|
504
|
+
// Gate A queries the post-purge DB. A deleted class returns no row here, which
|
|
505
|
+
// is correct: its stale CHA edges were already cleaned up by the Rust purge.
|
|
506
|
+
//
|
|
507
|
+
// Gate B: did a changed file add new RTA evidence (`new ConcreteX()`)?
|
|
508
|
+
// A new `calls` edge to a class/constructor/function-kind target means the
|
|
509
|
+
// instantiated set grew — previously RTA-filtered expansions in unchanged
|
|
510
|
+
// caller files become admissible, so the full scan is required.
|
|
511
|
+
// (`constructor`/`function` cover the older native engine fallback schema.)
|
|
512
|
+
//
|
|
513
|
+
// If neither gate fires, the hierarchy and RTA set are unchanged for all files
|
|
514
|
+
// outside changedFiles, so restricting to changed-file sources is safe.
|
|
515
|
+
let scopeToChangedFiles = false; // true → add WHERE src.file IN changedFiles
|
|
516
|
+
if (changedFiles !== null && changedFiles.length > 0) {
|
|
517
|
+
// Gate A: class/interface/trait/struct/record nodes in changed files?
|
|
518
|
+
const CHUNK_SIZE = 500;
|
|
519
|
+
let gateAFired = false;
|
|
520
|
+
for (let i = 0; i < changedFiles.length && !gateAFired; i += CHUNK_SIZE) {
|
|
521
|
+
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
522
|
+
const ph = chunk.map(() => '?').join(',');
|
|
523
|
+
const row = db
|
|
524
|
+
.prepare(
|
|
525
|
+
`SELECT 1 FROM nodes
|
|
526
|
+
WHERE file IN (${ph})
|
|
527
|
+
AND kind IN ('class', 'interface', 'trait', 'struct', 'record')
|
|
528
|
+
LIMIT 1`,
|
|
529
|
+
)
|
|
530
|
+
.get(...chunk);
|
|
531
|
+
if (row) gateAFired = true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Gate B: calls from changed-file sources to class/instantiable-kind targets
|
|
535
|
+
// (also covers older-schema fallback and future CHA extensions to struct/record).
|
|
536
|
+
// Includes class/interface/trait/struct/record (future CHA extension safety) and
|
|
537
|
+
// constructor/function (older native engine schema fallback).
|
|
538
|
+
let gateBFired = false;
|
|
539
|
+
if (!gateAFired) {
|
|
540
|
+
for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) {
|
|
541
|
+
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
542
|
+
const ph = chunk.map(() => '?').join(',');
|
|
543
|
+
const row = db
|
|
544
|
+
.prepare(
|
|
545
|
+
`SELECT 1 FROM edges e
|
|
546
|
+
JOIN nodes src ON e.source_id = src.id
|
|
547
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
548
|
+
WHERE e.kind = 'calls'
|
|
549
|
+
AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record', 'constructor', 'function')
|
|
550
|
+
AND src.file IN (${ph})
|
|
551
|
+
LIMIT 1`,
|
|
552
|
+
)
|
|
553
|
+
.get(...chunk);
|
|
554
|
+
if (row) gateBFired = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!gateAFired && !gateBFired) {
|
|
559
|
+
scopeToChangedFiles = true;
|
|
560
|
+
debug(
|
|
561
|
+
`runPostNativeCha: neither gate fired — scoping candidate scan to ${changedFiles.length} changed file(s)`,
|
|
562
|
+
);
|
|
563
|
+
} else {
|
|
564
|
+
debug(
|
|
565
|
+
`runPostNativeCha: ${gateAFired ? 'Gate A (hierarchy)' : 'Gate B (RTA)'} fired — running full scan`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
|
|
571
|
+
// Include caller_file and method_file so affectedFiles can be populated for
|
|
572
|
+
// incremental role reclassification; confidence uses CHA_TYPED_DISPATCH_CONFIDENCE matching runChaPostPass.
|
|
573
|
+
// When scopeToChangedFiles is true, restrict to call sites in the changed files
|
|
574
|
+
// (safe because no hierarchy or RTA evidence changed outside those files).
|
|
575
|
+
let callToMethods: Array<{
|
|
576
|
+
source_id: number;
|
|
577
|
+
caller_name: string;
|
|
578
|
+
method_name: string;
|
|
579
|
+
caller_file: string | null;
|
|
580
|
+
}>;
|
|
581
|
+
if (scopeToChangedFiles && changedFiles && changedFiles.length > 0) {
|
|
582
|
+
const CHUNK_SIZE = 500;
|
|
583
|
+
const rows: Array<{
|
|
584
|
+
source_id: number;
|
|
585
|
+
caller_name: string;
|
|
586
|
+
method_name: string;
|
|
587
|
+
caller_file: string | null;
|
|
588
|
+
}> = [];
|
|
589
|
+
for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
|
|
590
|
+
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
591
|
+
const ph = chunk.map(() => '?').join(',');
|
|
592
|
+
const chunkRows = db
|
|
593
|
+
.prepare(
|
|
594
|
+
`SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
|
|
595
|
+
FROM edges e
|
|
596
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
597
|
+
JOIN nodes src ON e.source_id = src.id
|
|
598
|
+
WHERE e.kind = 'calls' AND tgt.kind = 'method'
|
|
599
|
+
AND INSTR(tgt.name, '.') > 0
|
|
600
|
+
AND (e.technique IS NULL OR e.technique != 'cha-expanded')
|
|
601
|
+
AND src.file IN (${ph})`,
|
|
602
|
+
)
|
|
603
|
+
.all(...chunk) as Array<{
|
|
604
|
+
source_id: number;
|
|
605
|
+
caller_name: string;
|
|
606
|
+
method_name: string;
|
|
607
|
+
caller_file: string | null;
|
|
608
|
+
}>;
|
|
609
|
+
rows.push(...chunkRows);
|
|
610
|
+
}
|
|
611
|
+
callToMethods = rows;
|
|
612
|
+
} else {
|
|
613
|
+
callToMethods = db
|
|
614
|
+
.prepare(`
|
|
615
|
+
SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
|
|
616
|
+
FROM edges e
|
|
617
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
618
|
+
JOIN nodes src ON e.source_id = src.id
|
|
619
|
+
WHERE e.kind = 'calls' AND tgt.kind = 'method'
|
|
620
|
+
AND INSTR(tgt.name, '.') > 0
|
|
621
|
+
AND (e.technique IS NULL OR e.technique != 'cha-expanded')
|
|
622
|
+
`)
|
|
623
|
+
.all() as Array<{
|
|
624
|
+
source_id: number;
|
|
625
|
+
caller_name: string;
|
|
626
|
+
method_name: string;
|
|
627
|
+
caller_file: string | null;
|
|
628
|
+
}>;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Seed seen-pairs only from the source_ids we'll be expanding — avoids loading every
|
|
632
|
+
// call edge in the DB (which would be O(all edges)) for large codebases.
|
|
633
|
+
const seen = new Set<string>();
|
|
634
|
+
if (callToMethods.length > 0) {
|
|
635
|
+
const sourceIds = [...new Set(callToMethods.map((r) => r.source_id))];
|
|
636
|
+
const CHUNK_SIZE = 500;
|
|
637
|
+
for (let i = 0; i < sourceIds.length; i += CHUNK_SIZE) {
|
|
638
|
+
const chunk = sourceIds.slice(i, i + CHUNK_SIZE);
|
|
639
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
640
|
+
const existingPairs = db
|
|
641
|
+
.prepare(
|
|
642
|
+
`SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND source_id IN (${placeholders})`,
|
|
643
|
+
)
|
|
644
|
+
.all(...chunk) as Array<{ source_id: number; target_id: number }>;
|
|
645
|
+
for (const e of existingPairs) seen.add(`${e.source_id}|${e.target_id}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// No LIMIT: multiple files can define the same qualified name in a monorepo.
|
|
650
|
+
const findMethodStmt = db.prepare(
|
|
651
|
+
`SELECT id, file AS method_file FROM nodes WHERE name = ? AND kind = 'method'`,
|
|
652
|
+
);
|
|
653
|
+
const newEdges: Array<[number, number, string, number, number, string]> = [];
|
|
654
|
+
let newEdgeCount = 0;
|
|
655
|
+
|
|
656
|
+
for (const { source_id, method_name, caller_file } of callToMethods) {
|
|
657
|
+
const dotIdx = method_name.indexOf('.');
|
|
658
|
+
if (dotIdx === -1) continue;
|
|
659
|
+
const typeName = method_name.slice(0, dotIdx);
|
|
660
|
+
const methodSuffix = method_name.slice(dotIdx + 1);
|
|
661
|
+
|
|
662
|
+
// BFS over the implementors map — handles multi-level hierarchies where
|
|
663
|
+
// abstract/non-instantiated classes sit between the call-site type and
|
|
664
|
+
// the concrete leaf implementations (issue #1311).
|
|
665
|
+
const bfsQueue: string[] = [typeName];
|
|
666
|
+
const bfsVisited = new Set<string>([typeName]);
|
|
667
|
+
while (bfsQueue.length > 0) {
|
|
668
|
+
const current = bfsQueue.shift()!;
|
|
669
|
+
const children = implementors.get(current);
|
|
670
|
+
if (!children?.length) continue;
|
|
671
|
+
|
|
672
|
+
for (const cls of children) {
|
|
673
|
+
if (bfsVisited.has(cls)) continue;
|
|
674
|
+
bfsVisited.add(cls);
|
|
675
|
+
|
|
676
|
+
if (noRtaEvidence || instantiated.has(cls)) {
|
|
677
|
+
const qualifiedName = `${cls}.${methodSuffix}`;
|
|
678
|
+
const methodNodes = findMethodStmt.all(qualifiedName) as Array<{
|
|
679
|
+
id: number;
|
|
680
|
+
method_file: string | null;
|
|
681
|
+
}>;
|
|
682
|
+
for (const methodNode of methodNodes) {
|
|
683
|
+
if (methodNode.id === source_id) continue; // skip self-loops
|
|
684
|
+
const key = `${source_id}|${methodNode.id}`;
|
|
685
|
+
if (seen.has(key)) continue;
|
|
686
|
+
seen.add(key);
|
|
687
|
+
const conf = CHA_TYPED_DISPATCH_CONFIDENCE;
|
|
688
|
+
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha-expanded']);
|
|
689
|
+
newEdgeCount++;
|
|
690
|
+
if (caller_file) affectedFiles.add(caller_file);
|
|
691
|
+
if (methodNode.method_file) affectedFiles.add(methodNode.method_file);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Always traverse children — non-instantiated classes may have instantiated subclasses.
|
|
696
|
+
bfsQueue.push(cls);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (newEdges.length > 0) {
|
|
702
|
+
db.transaction(() => batchInsertEdges(db, newEdges))();
|
|
703
|
+
// Account for post-pass edges excluded from the build summary line (#1452),
|
|
704
|
+
// mirroring the this/super dispatch post-pass insertion log.
|
|
705
|
+
debug(`CHA expansion post-pass: inserted ${newEdgeCount} edge(s)`);
|
|
706
|
+
}
|
|
707
|
+
return { newEdgeCount, affectedFiles };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Extensions where `this`/`super` dispatch can occur (JS/TS family)
|
|
711
|
+
const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Phase 8.5: this/super dispatch post-pass for the native orchestrator path.
|
|
715
|
+
*
|
|
716
|
+
* The Rust build pipeline resolves typed receiver calls but does NOT persist raw
|
|
717
|
+
* unresolved call site receiver info (e.g. `this`, `super`) to the DB. This
|
|
718
|
+
* hybrid post-pass re-parses JS/TS/TSX files via WASM to collect call sites with
|
|
719
|
+
* `this`/`super` receivers, then resolves them through the class hierarchy stored
|
|
720
|
+
* in DB `extends` edges — mirroring what `buildChaPostPass` does on the WASM path.
|
|
721
|
+
*
|
|
722
|
+
* Also handles function-as-object-property methods (`f.h = function() { this.g() }`):
|
|
723
|
+
* these use `this` to reference sibling properties on the same object (`f`), so
|
|
724
|
+
* `resolveThisDispatch` resolves them by treating the dot-prefix of the caller name
|
|
725
|
+
* (`f` from `f.h`) as the class and looking up `f.g` directly — no `extends` edge needed.
|
|
726
|
+
*
|
|
727
|
+
* Runs when either `extends` edges exist (class inheritance) OR dot-named `method`
|
|
728
|
+
* nodes exist (func-prop assignments); skips only when neither is present.
|
|
729
|
+
*/
|
|
730
|
+
async function runPostNativeThisDispatch(
|
|
731
|
+
db: BetterSqlite3Database,
|
|
732
|
+
rootDir: string,
|
|
733
|
+
changedFiles: string[] | undefined,
|
|
734
|
+
isFullBuild: boolean,
|
|
735
|
+
): Promise<{ elapsedMs: number; targetIds: Set<number>; affectedFiles: Set<string> }> {
|
|
736
|
+
const t0 = performance.now();
|
|
737
|
+
const targetIds = new Set<number>();
|
|
738
|
+
// Files containing endpoints of newly inserted edges — lets the caller scope
|
|
739
|
+
// role re-classification to the nodes whose fan-in/out actually changed.
|
|
740
|
+
const affectedFiles = new Set<string>();
|
|
741
|
+
|
|
742
|
+
// Fast guard: need at least one extends edge (class inheritance) OR a dot-named
|
|
743
|
+
// method node (func-prop assignment: `f.h = function() { this.g() }`) for
|
|
744
|
+
// this/super dispatch to produce any edges.
|
|
745
|
+
const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
|
|
746
|
+
const hasFuncPropMethod = db
|
|
747
|
+
.prepare(`SELECT 1 FROM nodes WHERE kind = 'method' AND INSTR(name, '.') > 0 LIMIT 1`)
|
|
748
|
+
.get();
|
|
749
|
+
if (!hasExtends && !hasFuncPropMethod) return { elapsedMs: 0, targetIds, affectedFiles };
|
|
750
|
+
|
|
751
|
+
// Build parents map: child class → direct parent class (from `extends` edges).
|
|
752
|
+
// May be empty when only func-prop methods exist (no class inheritance) —
|
|
753
|
+
// resolveThisDispatch handles that case via direct class-prefix lookup.
|
|
754
|
+
const parentRows = hasExtends
|
|
755
|
+
? (db
|
|
756
|
+
.prepare(`
|
|
757
|
+
SELECT src.name AS child_name, tgt.name AS parent_name
|
|
758
|
+
FROM edges e
|
|
759
|
+
JOIN nodes src ON e.source_id = src.id
|
|
760
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
761
|
+
WHERE e.kind = 'extends'
|
|
762
|
+
`)
|
|
763
|
+
.all() as Array<{ child_name: string; parent_name: string }>)
|
|
764
|
+
: [];
|
|
765
|
+
|
|
766
|
+
const parents = new Map<string, string>();
|
|
767
|
+
for (const row of parentRows) {
|
|
768
|
+
if (!parents.has(row.child_name)) parents.set(row.child_name, row.parent_name);
|
|
769
|
+
}
|
|
770
|
+
// Note: parents may be empty when hasFuncPropMethod but !hasExtends — that is
|
|
771
|
+
// intentional. resolveThisDispatch still resolves `this.g()` inside `f.h` by
|
|
772
|
+
// treating `f` (the dot-prefix of callerName `f.h`) as the class and looking
|
|
773
|
+
// up `f.g` directly via lookup.byName(), without traversing the parents chain.
|
|
774
|
+
|
|
775
|
+
const chaCtx: ChaContext = {
|
|
776
|
+
implementors: new Map(), // not needed for this/super resolution
|
|
777
|
+
parents,
|
|
778
|
+
instantiatedTypes: new Set(), // not needed for this/super resolution
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// Determine which files to re-parse.
|
|
782
|
+
//
|
|
783
|
+
// On a full build we do NOT re-parse every JS/TS file — that would WASM-parse
|
|
784
|
+
// the entire project on top of the native pass, causing a massive regression
|
|
785
|
+
// (measured: +358% ms/file on codegraph itself). Instead we restrict to files
|
|
786
|
+
// that are part of the class inheritance hierarchy (both subclass files with
|
|
787
|
+
// `super.X()` calls and parent-class files with `this.X()` calls) OR that
|
|
788
|
+
// contain dot-named method nodes (func-prop assignments whose bodies may call
|
|
789
|
+
// `this.sibling()`). Any file not in either set has no class or object context
|
|
790
|
+
// where `this`/`super` dispatch would produce new edges.
|
|
791
|
+
let relFiles: string[];
|
|
792
|
+
if (isFullBuild || !changedFiles) {
|
|
793
|
+
const rows = db
|
|
794
|
+
.prepare(`
|
|
795
|
+
SELECT DISTINCT file FROM (
|
|
796
|
+
SELECT src.file AS file
|
|
797
|
+
FROM edges e
|
|
798
|
+
JOIN nodes src ON e.source_id = src.id
|
|
799
|
+
WHERE e.kind = 'extends' AND src.file IS NOT NULL
|
|
800
|
+
UNION
|
|
801
|
+
SELECT tgt.file AS file
|
|
802
|
+
FROM edges e
|
|
803
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
804
|
+
WHERE e.kind = 'extends' AND tgt.file IS NOT NULL
|
|
805
|
+
UNION
|
|
806
|
+
-- Files with func-prop method definitions (e.g. f.h = function(){this.g()}).
|
|
807
|
+
-- Only include files where the method's owner prefix is NOT a known class name —
|
|
808
|
+
-- this keeps the re-parse set small (func-prop files only, not all class-method files).
|
|
809
|
+
-- AND name IS NOT NULL guards the NOT IN sub-select: if any class node had a NULL
|
|
810
|
+
-- name the entire NOT IN clause would silently return no rows (SQL NULL semantics).
|
|
811
|
+
SELECT n.file AS file
|
|
812
|
+
FROM nodes n
|
|
813
|
+
WHERE n.kind = 'method'
|
|
814
|
+
AND INSTR(n.name, '.') > 0
|
|
815
|
+
AND n.file IS NOT NULL
|
|
816
|
+
AND SUBSTR(n.name, 1, INSTR(n.name, '.') - 1) NOT IN (
|
|
817
|
+
SELECT name FROM nodes WHERE kind IN ('class', 'struct', 'interface', 'type')
|
|
818
|
+
AND name IS NOT NULL
|
|
819
|
+
)
|
|
820
|
+
)
|
|
821
|
+
`)
|
|
822
|
+
.all() as Array<{ file: string }>;
|
|
823
|
+
relFiles = rows
|
|
824
|
+
.map((r) => r.file)
|
|
825
|
+
.filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
|
|
826
|
+
} else {
|
|
827
|
+
// NOTE: Only files explicitly listed in changedFiles are re-parsed.
|
|
828
|
+
// If a parent-class method is replaced (new node ID) but the child file is
|
|
829
|
+
// unchanged, the stale super.method() edge is not refreshed here. A full
|
|
830
|
+
// rebuild (isFullBuild=true) is required to recover in that scenario.
|
|
831
|
+
relFiles = changedFiles.filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
|
|
832
|
+
}
|
|
833
|
+
if (relFiles.length === 0) return { elapsedMs: 0, targetIds, affectedFiles };
|
|
834
|
+
|
|
835
|
+
// DB-backed CallNodeLookup — resolveThisDispatch only calls byName()
|
|
836
|
+
const findByNameStmt = db.prepare(`SELECT id, file, kind FROM nodes WHERE name = ?`);
|
|
837
|
+
const lookup: CallNodeLookup = {
|
|
838
|
+
byName: (name) => findByNameStmt.all(name) as Array<{ id: number; file: string; kind: string }>,
|
|
839
|
+
byNameAndFile: (name, file) =>
|
|
840
|
+
(findByNameStmt.all(name) as Array<{ id: number; file: string; kind: string }>).filter(
|
|
841
|
+
(n) => n.file === file,
|
|
842
|
+
),
|
|
843
|
+
isBarrel: () => false,
|
|
844
|
+
resolveBarrel: () => null,
|
|
845
|
+
nodeId: () => undefined,
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// Seed seen-pairs from existing call edges on source nodes in our file set
|
|
849
|
+
const seen = new Set<string>();
|
|
850
|
+
const CHUNK = 500;
|
|
851
|
+
for (let i = 0; i < relFiles.length; i += CHUNK) {
|
|
852
|
+
const chunk = relFiles.slice(i, i + CHUNK);
|
|
853
|
+
const ph = chunk.map(() => '?').join(',');
|
|
854
|
+
const rows = db
|
|
855
|
+
.prepare(
|
|
856
|
+
`SELECT e.source_id, e.target_id
|
|
857
|
+
FROM edges e
|
|
858
|
+
JOIN nodes n ON e.source_id = n.id
|
|
859
|
+
WHERE e.kind = 'calls' AND n.file IN (${ph})`,
|
|
860
|
+
)
|
|
861
|
+
.all(...chunk) as Array<{ source_id: number; target_id: number }>;
|
|
862
|
+
for (const r of rows) seen.add(`${r.source_id}|${r.target_id}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Find the innermost containing method/function for a call at `line` in `file`.
|
|
866
|
+
// COALESCE maps NULL end_line to a large sentinel so unbounded nodes sort last
|
|
867
|
+
// (SQLite ASC orders NULLs first, so a raw `end_line - line` would pick them first).
|
|
868
|
+
const findCallerByLineStmt = db.prepare(`
|
|
869
|
+
SELECT id, name FROM nodes
|
|
870
|
+
WHERE file = ? AND kind IN ('method', 'function')
|
|
871
|
+
AND line <= ? AND (end_line IS NULL OR end_line >= ?)
|
|
872
|
+
ORDER BY COALESCE(end_line - line, 999999999) ASC
|
|
873
|
+
LIMIT 1
|
|
874
|
+
`);
|
|
875
|
+
|
|
876
|
+
// Re-parse the files to obtain raw call sites with receiver info. Only
|
|
877
|
+
// `calls` (with receivers) are consumed here.
|
|
878
|
+
//
|
|
879
|
+
// The native engine is preferred: this pass only runs after a native
|
|
880
|
+
// orchestrator build, so the addon is already loaded and re-parses the
|
|
881
|
+
// hierarchy file set in single-digit milliseconds with the same
|
|
882
|
+
// receiver-annotated call sites as the WASM extractor. Booting the WASM
|
|
883
|
+
// runtime here instead cost ~40–110ms per full build (in-process
|
|
884
|
+
// web-tree-sitter + grammar init dominated) — part of the v3.12.0
|
|
885
|
+
// publish-gate regression. Files the native engine cannot parse (extension
|
|
886
|
+
// outside NATIVE_SUPPORTED_EXTENSIONS, e.g. .mts/.cts) and native parse
|
|
887
|
+
// failures fall back to the WASM backfill path so the sweep stays complete.
|
|
888
|
+
const absFiles = relFiles.map((f) => path.join(rootDir, f));
|
|
889
|
+
const nativeAbs = absFiles.filter((f) =>
|
|
890
|
+
NATIVE_SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()),
|
|
891
|
+
);
|
|
892
|
+
const callsByRel = new Map<string, { name: string; receiver?: string; line: number }[]>();
|
|
893
|
+
// Track native-supported files that returned null (per-file parse error) so
|
|
894
|
+
// they can be included in the WASM fallback set below, ensuring no file's
|
|
895
|
+
// this/super call sites are silently discarded.
|
|
896
|
+
const nativeNullFiles = new Set<string>();
|
|
897
|
+
let nativeParsed = false;
|
|
898
|
+
if (nativeAbs.length > 0) {
|
|
899
|
+
const native = loadNative();
|
|
900
|
+
if (native) {
|
|
901
|
+
try {
|
|
902
|
+
const results = native.parseFiles(nativeAbs, rootDir, false, false) as Array<{
|
|
903
|
+
file: string;
|
|
904
|
+
calls?: { name: string; receiver?: string; line: number }[];
|
|
905
|
+
} | null>;
|
|
906
|
+
for (let i = 0; i < results.length; i++) {
|
|
907
|
+
const r = results[i];
|
|
908
|
+
if (!r) {
|
|
909
|
+
// Per-file parse failure — fall back to WASM for this file.
|
|
910
|
+
const abs = nativeAbs[i];
|
|
911
|
+
if (abs) nativeNullFiles.add(abs);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
callsByRel.set(normalizePath(path.relative(rootDir, r.file)), r.calls ?? []);
|
|
915
|
+
}
|
|
916
|
+
nativeParsed = true;
|
|
917
|
+
} catch (e) {
|
|
918
|
+
debug(`this-dispatch native re-parse failed, falling back to WASM: ${toErrorMessage(e)}`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// WASM handles: (a) non-native extensions (e.g. .mts/.cts), (b) the entire
|
|
923
|
+
// file list when the native batch threw, and (c) individual files where the
|
|
924
|
+
// native addon returned null (per-file parse error).
|
|
925
|
+
const wasmAbs = nativeParsed
|
|
926
|
+
? [
|
|
927
|
+
...absFiles.filter((f) => !NATIVE_SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase())),
|
|
928
|
+
...nativeNullFiles,
|
|
929
|
+
]
|
|
930
|
+
: absFiles;
|
|
931
|
+
const wasmResults =
|
|
932
|
+
wasmAbs.length > 0
|
|
933
|
+
? await parseFilesWasmForBackfill(wasmAbs, rootDir, { symbolsOnly: true })
|
|
934
|
+
: new Map<string, ExtractorOutput>();
|
|
935
|
+
for (const [relPath, symbols] of wasmResults) {
|
|
936
|
+
callsByRel.set(relPath, symbols.calls ?? []);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const newEdges: Array<[number, number, string, number, number, string]> = [];
|
|
940
|
+
|
|
941
|
+
for (const [relPath, calls] of callsByRel) {
|
|
942
|
+
for (const call of calls) {
|
|
943
|
+
// Only 'this' and 'super' are class-instance receivers in JS/TS.
|
|
944
|
+
// 'self' refers to WindowOrWorkerGlobalScope — not a class instance — so
|
|
945
|
+
// filtering it here prevents spurious dispatch edges from Worker call sites.
|
|
946
|
+
if (call.receiver !== 'this' && call.receiver !== 'super') continue;
|
|
947
|
+
|
|
948
|
+
const callerRow = findCallerByLineStmt.get(relPath, call.line, call.line) as
|
|
949
|
+
| { id: number; name: string }
|
|
950
|
+
| undefined;
|
|
951
|
+
if (!callerRow) continue;
|
|
952
|
+
|
|
953
|
+
const targets = resolveThisDispatch(
|
|
954
|
+
call.name,
|
|
955
|
+
callerRow.name,
|
|
956
|
+
call.receiver as 'this' | 'super',
|
|
957
|
+
chaCtx,
|
|
958
|
+
lookup,
|
|
959
|
+
relPath,
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
for (const t of targets) {
|
|
963
|
+
if (t.id === callerRow.id) continue; // skip self-loops
|
|
964
|
+
const key = `${callerRow.id}|${t.id}`;
|
|
965
|
+
if (seen.has(key)) continue;
|
|
966
|
+
seen.add(key);
|
|
967
|
+
const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
|
|
968
|
+
if (conf <= 0) continue;
|
|
969
|
+
// Tag super-dispatch edges distinctly so runPostNativeCha can exclude them
|
|
970
|
+
// from further CHA expansion (super calls are not virtual dispatch).
|
|
971
|
+
const technique = call.receiver === 'super' ? 'super-dispatch' : 'cha';
|
|
972
|
+
newEdges.push([callerRow.id, t.id, 'calls', conf, 0, technique]);
|
|
973
|
+
targetIds.add(t.id);
|
|
974
|
+
affectedFiles.add(relPath);
|
|
975
|
+
if (t.file) affectedFiles.add(t.file);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (newEdges.length > 0) {
|
|
981
|
+
db.transaction(() => batchInsertEdges(db, newEdges))();
|
|
982
|
+
debug(`this/super dispatch post-pass: inserted ${newEdges.length} edge(s)`);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Free WASM parse trees — mirrors the cleanup in backfillNativeDroppedFiles
|
|
986
|
+
for (const [, symbols] of wasmResults) {
|
|
987
|
+
const tree = (symbols as { _tree?: { delete?: () => void } })._tree;
|
|
988
|
+
if (tree && typeof tree.delete === 'function') {
|
|
989
|
+
try {
|
|
990
|
+
tree.delete();
|
|
991
|
+
} catch {
|
|
992
|
+
/* ignore cleanup errors */
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
(symbols as { _tree?: unknown; _langId?: unknown })._tree = undefined;
|
|
996
|
+
(symbols as { _tree?: unknown; _langId?: unknown })._langId = undefined;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
return { elapsedMs: performance.now() - t0, targetIds, affectedFiles };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
interface PostPassTimings {
|
|
1003
|
+
gapDetectMs: number;
|
|
1004
|
+
chaMs: number;
|
|
1005
|
+
thisDispatchMs: number;
|
|
1006
|
+
reclassifyMs: number;
|
|
1007
|
+
techniqueBackfillMs: number;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
385
1010
|
/** Format timing result from native orchestrator phases + JS post-processing. */
|
|
386
1011
|
function formatNativeTimingResult(
|
|
387
1012
|
p: Record<string, number>,
|
|
388
1013
|
structurePatchMs: number,
|
|
389
1014
|
analysisTiming: { astMs: number; complexityMs: number; cfgMs: number; dataflowMs: number },
|
|
1015
|
+
postPass: PostPassTimings,
|
|
390
1016
|
): BuildResult {
|
|
391
1017
|
return {
|
|
392
1018
|
phases: {
|
|
@@ -399,6 +1025,11 @@ function formatNativeTimingResult(
|
|
|
399
1025
|
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
|
|
400
1026
|
structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
|
|
401
1027
|
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
|
|
1028
|
+
gapDetectMs: +postPass.gapDetectMs.toFixed(1),
|
|
1029
|
+
chaMs: +postPass.chaMs.toFixed(1),
|
|
1030
|
+
thisDispatchMs: +postPass.thisDispatchMs.toFixed(1),
|
|
1031
|
+
reclassifyMs: +postPass.reclassifyMs.toFixed(1),
|
|
1032
|
+
techniqueBackfillMs: +postPass.techniqueBackfillMs.toFixed(1),
|
|
402
1033
|
astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
|
|
403
1034
|
complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
|
|
404
1035
|
cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
|
|
@@ -599,10 +1230,35 @@ async function backfillNativeDroppedFiles(
|
|
|
599
1230
|
|
|
600
1231
|
if (missingAbs.length === 0) return;
|
|
601
1232
|
|
|
1233
|
+
// Parse all missing files via WASM first so we can distinguish real native
|
|
1234
|
+
// extractor failures (WASM finds symbols but native didn't) from files the
|
|
1235
|
+
// Rust engine legitimately skipped (gitignored artifacts, empty declaration
|
|
1236
|
+
// files, etc. where WASM also produces 0 symbols). Both categories are
|
|
1237
|
+
// backfilled — only the former triggers a WARN (#1566).
|
|
1238
|
+
const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
|
|
1239
|
+
|
|
1240
|
+
// Build two sets from wasmResults:
|
|
1241
|
+
// wasmParsedFiles — rel-paths present in wasmResults (WASM succeeded, even 0 symbols)
|
|
1242
|
+
// wasmFoundSymbols — subset where WASM found ≥1 symbol
|
|
1243
|
+
// Files absent from wasmParsedFiles were skipped by WASM entirely (extension
|
|
1244
|
+
// not in _extToLang, wasmExtractSymbols returned null, or a read error).
|
|
1245
|
+
// Those files do NOT end up in the batchInsertNodes loop below.
|
|
1246
|
+
const wasmParsedFiles = new Set<string>();
|
|
1247
|
+
const wasmFoundSymbols = new Set<string>();
|
|
1248
|
+
for (const [relPath, symbols] of wasmResults) {
|
|
1249
|
+
wasmParsedFiles.add(relPath);
|
|
1250
|
+
if ((symbols.definitions?.length ?? 0) > 0 || (symbols.exports?.length ?? 0) > 0) {
|
|
1251
|
+
wasmFoundSymbols.add(relPath);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
602
1255
|
// Classify drops so users see per-extension reasons instead of just a count
|
|
603
1256
|
// (#1011). `unsupported-by-native` is a legitimate parser limit (no Rust
|
|
604
1257
|
// extractor); `native-extractor-failure` indicates a real native bug since
|
|
605
|
-
// the language IS supported by the addon yet
|
|
1258
|
+
// the language IS supported by the addon yet WASM found symbols the native
|
|
1259
|
+
// engine should have extracted. Files where both engines produce 0 symbols
|
|
1260
|
+
// are legitimately empty (e.g. gitignored napi-generated declaration stubs)
|
|
1261
|
+
// and logged at debug level only.
|
|
606
1262
|
const { byReason, totals } = classifyNativeDrops(missingRel);
|
|
607
1263
|
if (totals['unsupported-by-native'] > 0) {
|
|
608
1264
|
const buckets = byReason['unsupported-by-native'];
|
|
@@ -611,12 +1267,54 @@ async function backfillNativeDroppedFiles(
|
|
|
611
1267
|
);
|
|
612
1268
|
}
|
|
613
1269
|
if (totals['native-extractor-failure'] > 0) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1270
|
+
// Three-way split of native-extractor-failure files:
|
|
1271
|
+
// realFailureBuckets — WASM found symbols → real Rust extractor bug (WARN)
|
|
1272
|
+
// emptyFileBuckets — WASM parsed but found 0 symbols → gitignored/empty (debug)
|
|
1273
|
+
// These DO receive a file-node insert in the loop below.
|
|
1274
|
+
// wasmSkipBuckets — WASM skipped entirely (ext unknown or parse error) →
|
|
1275
|
+
// no file-node insert, and no WARN (debug only, distinct
|
|
1276
|
+
// message to avoid overstating backfill coverage).
|
|
1277
|
+
const allFailurePaths = byReason['native-extractor-failure'];
|
|
1278
|
+
const realFailureBuckets = new Map<string, string[]>();
|
|
1279
|
+
const emptyFileBuckets = new Map<string, string[]>();
|
|
1280
|
+
const wasmSkipBuckets = new Map<string, string[]>();
|
|
1281
|
+
for (const [ext, paths] of allFailurePaths) {
|
|
1282
|
+
for (const relPath of paths) {
|
|
1283
|
+
let bucket: Map<string, string[]>;
|
|
1284
|
+
if (wasmFoundSymbols.has(relPath)) {
|
|
1285
|
+
bucket = realFailureBuckets;
|
|
1286
|
+
} else if (wasmParsedFiles.has(relPath)) {
|
|
1287
|
+
bucket = emptyFileBuckets;
|
|
1288
|
+
} else {
|
|
1289
|
+
bucket = wasmSkipBuckets;
|
|
1290
|
+
}
|
|
1291
|
+
let list = bucket.get(ext);
|
|
1292
|
+
if (!list) {
|
|
1293
|
+
list = [];
|
|
1294
|
+
bucket.set(ext, list);
|
|
1295
|
+
}
|
|
1296
|
+
list.push(relPath);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (realFailureBuckets.size > 0) {
|
|
1300
|
+
const realCount = [...realFailureBuckets.values()].reduce((s, a) => s + a.length, 0);
|
|
1301
|
+
warn(
|
|
1302
|
+
`Native orchestrator dropped ${realCount} file(s) across ${realFailureBuckets.size} extension(s) in natively-supported languages — likely a Rust extractor bug. Backfilling via WASM:${formatDropExtensionSummary(realFailureBuckets)}`,
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (emptyFileBuckets.size > 0) {
|
|
1306
|
+
const emptyCount = [...emptyFileBuckets.values()].reduce((s, a) => s + a.length, 0);
|
|
1307
|
+
debug(
|
|
1308
|
+
`Native orchestrator skipped ${emptyCount} file(s) in natively-supported languages that also produced 0 symbols via WASM (likely gitignored or empty); backfilling file nodes:${formatDropExtensionSummary(emptyFileBuckets)}`,
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
if (wasmSkipBuckets.size > 0) {
|
|
1312
|
+
const skipCount = [...wasmSkipBuckets.values()].reduce((s, a) => s + a.length, 0);
|
|
1313
|
+
debug(
|
|
1314
|
+
`Native orchestrator skipped ${skipCount} file(s) in natively-supported languages that WASM also could not parse (unregistered extension or parse error); no file-node inserted:${formatDropExtensionSummary(wasmSkipBuckets)}`,
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
618
1317
|
}
|
|
619
|
-
const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
|
|
620
1318
|
|
|
621
1319
|
const rows: unknown[][] = [];
|
|
622
1320
|
const exportKeys: unknown[][] = [];
|
|
@@ -740,6 +1438,50 @@ async function backfillNativeDroppedFiles(
|
|
|
740
1438
|
}
|
|
741
1439
|
}
|
|
742
1440
|
|
|
1441
|
+
/**
|
|
1442
|
+
* Backfill the `technique` column on `calls` edges written by the native Rust
|
|
1443
|
+
* orchestrator, which does not write the column itself.
|
|
1444
|
+
*
|
|
1445
|
+
* For full builds, all `calls` edges in the DB are new so a global UPDATE is
|
|
1446
|
+
* correct. For incremental builds, only changed-file source nodes are updated
|
|
1447
|
+
* to avoid overwriting previously-set technique values on unchanged edges.
|
|
1448
|
+
*/
|
|
1449
|
+
function backfillEdgeTechniquesAfterNativeOrchestrator(
|
|
1450
|
+
db: BetterSqlite3Database,
|
|
1451
|
+
isFullBuild: boolean,
|
|
1452
|
+
changedFiles: string[] | undefined,
|
|
1453
|
+
): void {
|
|
1454
|
+
// Quiet incremental: no files changed → no new edges inserted, nothing to tag.
|
|
1455
|
+
// Running the global UPDATE here would mis-tag pre-migration NULL-technique edges
|
|
1456
|
+
// from unchanged files as 'ts-native'.
|
|
1457
|
+
if (!isFullBuild && changedFiles && changedFiles.length === 0) {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (isFullBuild || !changedFiles) {
|
|
1461
|
+
db.prepare(
|
|
1462
|
+
"UPDATE edges SET technique = 'ts-native' WHERE kind = 'calls' AND technique IS NULL",
|
|
1463
|
+
).run();
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
// Incremental: scope to source nodes whose file is one of the changed files.
|
|
1467
|
+
// Chunk to stay within SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (999 on older builds).
|
|
1468
|
+
const CHUNK_SIZE = 500;
|
|
1469
|
+
const tx = db.transaction(() => {
|
|
1470
|
+
for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
|
|
1471
|
+
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
1472
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
1473
|
+
db.prepare(
|
|
1474
|
+
`UPDATE edges SET technique = 'ts-native'
|
|
1475
|
+
WHERE kind = 'calls' AND technique IS NULL
|
|
1476
|
+
AND source_id IN (
|
|
1477
|
+
SELECT id FROM nodes WHERE file IN (${placeholders})
|
|
1478
|
+
)`,
|
|
1479
|
+
).run(...chunk);
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
tx();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
743
1485
|
/**
|
|
744
1486
|
* Try the native build orchestrator.
|
|
745
1487
|
*
|
|
@@ -814,7 +1556,7 @@ export async function tryNativeOrchestrator(
|
|
|
814
1556
|
// Even on no-op rebuilds, dropped-language files added since the last
|
|
815
1557
|
// full build are still missing from `nodes`/`file_hashes` (#1083), and
|
|
816
1558
|
// WASM-only files deleted from disk leave stale rows behind (#1073).
|
|
817
|
-
// The orchestrator's
|
|
1559
|
+
// The orchestrator's collect_files skipped them, so its earlyExit
|
|
818
1560
|
// doesn't imply DB consistency. Run the gap repair before returning.
|
|
819
1561
|
const gap = detectDroppedLanguageGap(ctx);
|
|
820
1562
|
if (gap.missingAbs.length > 0 || gap.staleRel.length > 0) {
|
|
@@ -856,9 +1598,9 @@ export async function tryNativeOrchestrator(
|
|
|
856
1598
|
built_at: new Date().toISOString(),
|
|
857
1599
|
});
|
|
858
1600
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
)
|
|
1601
|
+
// The build summary is logged after the JS edge-writing post-passes below
|
|
1602
|
+
// (dropped-language backfill, CHA, this/super dispatch) so the reported
|
|
1603
|
+
// counts include their edges (#1452).
|
|
862
1604
|
|
|
863
1605
|
// ── Post-native structure + analysis ──────────────────────────────
|
|
864
1606
|
let analysisTiming = {
|
|
@@ -881,19 +1623,165 @@ export async function tryNativeOrchestrator(
|
|
|
881
1623
|
ctx.opts.cfg !== false ||
|
|
882
1624
|
ctx.opts.dataflow !== false);
|
|
883
1625
|
|
|
1626
|
+
// ── DB handoff ────────────────────────────────────────────────────────────
|
|
1627
|
+
// Ensure a proper better-sqlite3 connection is open before any post-pass that
|
|
1628
|
+
// writes edges (dropped-language backfill, CHA) and before structure/analysis.
|
|
1629
|
+
// When analysis fallback is needed the handoff already happened above; when
|
|
1630
|
+
// neither structure nor analysis is needed the proxy conversion is deferred to
|
|
1631
|
+
// here so CHA and technique-backfill can still write rows.
|
|
884
1632
|
if (needsStructure || needsAnalysisFallback) {
|
|
885
|
-
// When analysis fallback is needed, handoff to better-sqlite3 — the
|
|
886
|
-
// analysis engine uses the suspend/resume WAL pattern that requires a
|
|
887
|
-
// real better-sqlite3 connection, not the NativeDbProxy.
|
|
888
1633
|
if (needsAnalysisFallback && ctx.nativeFirstProxy) {
|
|
889
1634
|
closeNativeDb(ctx, 'pre-analysis-fallback');
|
|
890
1635
|
ctx.db = openDb(ctx.dbPath);
|
|
891
1636
|
ctx.nativeFirstProxy = false;
|
|
892
1637
|
} else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
|
|
893
|
-
// DB reopen failed — return partial result
|
|
894
|
-
return formatNativeTimingResult(p, 0, analysisTiming
|
|
1638
|
+
// DB reopen failed — return partial result (no post-pass phases completed)
|
|
1639
|
+
return formatNativeTimingResult(p, 0, analysisTiming, {
|
|
1640
|
+
gapDetectMs: 0,
|
|
1641
|
+
chaMs: 0,
|
|
1642
|
+
thisDispatchMs: 0,
|
|
1643
|
+
reclassifyMs: 0,
|
|
1644
|
+
techniqueBackfillMs: 0,
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// ── Edge-writing post-passes (run before structure so roles see full graph) ──
|
|
1650
|
+
|
|
1651
|
+
// Engine parity: the native orchestrator silently drops files whose
|
|
1652
|
+
// Rust extractor/grammar is missing or fails (e.g. HCL, Scala, Swift on
|
|
1653
|
+
// stale native binaries). WASM handles those — backfill via WASM so both
|
|
1654
|
+
// engines process the same file set (#967).
|
|
1655
|
+
//
|
|
1656
|
+
// Detect the gap once (fs walk + 2 DB queries) and use it for both gating
|
|
1657
|
+
// and the backfill itself. On quiet incrementals we still pay the walk so
|
|
1658
|
+
// we can detect brand-new files in dropped-language extensions — a gap that
|
|
1659
|
+
// the orchestrator's `detect_removed_files` filter (#1070) leaves open
|
|
1660
|
+
// (#1083, #1091). The pre-check is cheap because the expensive part (WASM
|
|
1661
|
+
// re-parse of the missing set) is gated below.
|
|
1662
|
+
const gapDetectStart = performance.now();
|
|
1663
|
+
const gap = detectDroppedLanguageGap(ctx);
|
|
1664
|
+
const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;
|
|
1665
|
+
if (backfillHappened) {
|
|
1666
|
+
await backfillNativeDroppedFiles(ctx, gap);
|
|
1667
|
+
}
|
|
1668
|
+
const gapDetectMs = performance.now() - gapDetectStart;
|
|
1669
|
+
|
|
1670
|
+
// Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
|
|
1671
|
+
// whose raw receiver info the Rust pipeline does not persist to DB.
|
|
1672
|
+
// Runs BEFORE the CHA expansion pass so that super.method() → Parent.method edges
|
|
1673
|
+
// (technique='cha') are in the DB when runPostNativeCha expands them to sibling
|
|
1674
|
+
// class overrides (e.g. PostMixin.m → B.m when PostMixin and B both extend A).
|
|
1675
|
+
const {
|
|
1676
|
+
elapsedMs: thisDispatchMs,
|
|
1677
|
+
targetIds: thisDispatchTargetIds,
|
|
1678
|
+
affectedFiles: thisDispatchAffectedFiles,
|
|
1679
|
+
} = await runPostNativeThisDispatch(
|
|
1680
|
+
ctx.db as unknown as BetterSqlite3Database,
|
|
1681
|
+
ctx.rootDir,
|
|
1682
|
+
result.changedFiles,
|
|
1683
|
+
!!result.isFullBuild,
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
// Phase 8.6: expand CHA call edges (interface dispatch → concrete implementations).
|
|
1687
|
+
// Returns the affected files so role re-classification below can be scoped to
|
|
1688
|
+
// the nodes whose fan-in/out actually changed.
|
|
1689
|
+
//
|
|
1690
|
+
// Runs AFTER this/super dispatch so super.method() edges are already in the DB.
|
|
1691
|
+
// The 'cha-expanded' technique tag on this pass's own output prevents re-expansion
|
|
1692
|
+
// of those edges in subsequent incremental builds, while 'cha'-tagged edges from
|
|
1693
|
+
// this/super dispatch remain eligible for expansion here.
|
|
1694
|
+
//
|
|
1695
|
+
// Function-as-object-property methods (`fn.method = function() {}`) are extracted
|
|
1696
|
+
// natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
|
|
1697
|
+
// no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
|
|
1698
|
+
const chaStart = performance.now();
|
|
1699
|
+
const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(
|
|
1700
|
+
ctx.db as unknown as BetterSqlite3Database,
|
|
1701
|
+
// null = full build (scan all call→method edges); array = incremental (gate queries decide scope)
|
|
1702
|
+
result.isFullBuild ? null : (result.changedFiles ?? null),
|
|
1703
|
+
);
|
|
1704
|
+
const chaMs = performance.now() - chaStart;
|
|
1705
|
+
|
|
1706
|
+
// Role re-classification after JS edge-writing post-passes.
|
|
1707
|
+
// The Rust orchestrator classifies roles before these post-passes (CHA,
|
|
1708
|
+
// this-dispatch) add edges, so roles for the edge endpoints are stale.
|
|
1709
|
+
// Scoped to the files containing those endpoints: a new edge only changes
|
|
1710
|
+
// fan-in/out for its own source and target nodes, so re-classifying their
|
|
1711
|
+
// files restores correctness without re-running the classifier over the
|
|
1712
|
+
// whole graph (which cost ~130ms per build on codegraph itself and was a
|
|
1713
|
+
// major part of the v3.12.0 native full-build benchmark regression).
|
|
1714
|
+
let reclassifyMs = 0;
|
|
1715
|
+
if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
|
|
1716
|
+
const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
|
|
1717
|
+
// When edges were inserted but all their endpoint nodes have null `file`
|
|
1718
|
+
// columns (rare but possible), affectedFiles stays empty even though
|
|
1719
|
+
// fan-in/out changed. Fall back to full-graph re-classification in that
|
|
1720
|
+
// case — scoped classification with an empty set would be a no-op, leaving
|
|
1721
|
+
// roles stale for those nodes.
|
|
1722
|
+
const scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
|
|
1723
|
+
const reclassifyStart = performance.now();
|
|
1724
|
+
try {
|
|
1725
|
+
const { classifyNodeRoles } = (await import('../../../../features/structure.js')) as {
|
|
1726
|
+
classifyNodeRoles: (
|
|
1727
|
+
db: BetterSqlite3Database,
|
|
1728
|
+
changedFiles?: string[] | null,
|
|
1729
|
+
) => Record<string, number>;
|
|
1730
|
+
};
|
|
1731
|
+
classifyNodeRoles(ctx.db as unknown as BetterSqlite3Database, scopedFiles);
|
|
1732
|
+
debug(
|
|
1733
|
+
scopedFiles
|
|
1734
|
+
? `Post-pass role re-classification complete (${scopedFiles.length} file(s))`
|
|
1735
|
+
: 'Post-pass role re-classification complete (full graph — null-file endpoints)',
|
|
1736
|
+
);
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
|
|
1739
|
+
}
|
|
1740
|
+
reclassifyMs = performance.now() - reclassifyStart;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Backfill the `technique` column on `calls` edges written by the Rust
|
|
1744
|
+
// orchestrator, which does not write the column. Runs after all edge-writing
|
|
1745
|
+
// phases (including the WASM dropped-language backfill, CHA post-pass, and
|
|
1746
|
+
// this/super dispatch) so every new edge in this build cycle gets a label.
|
|
1747
|
+
const techniqueBackfillStart = performance.now();
|
|
1748
|
+
backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
|
|
1749
|
+
const techniqueBackfillMs = performance.now() - techniqueBackfillStart;
|
|
1750
|
+
|
|
1751
|
+
// Re-count nodes/edges now that all edge-writing post-passes have run: the
|
|
1752
|
+
// Rust orchestrator captured its counts before the JS post-passes added
|
|
1753
|
+
// edges, so both its summary and build_meta under-report (#1452).
|
|
1754
|
+
//
|
|
1755
|
+
// Fast path: skip the COUNT(*) scan when no post-pass wrote any edges.
|
|
1756
|
+
// COUNT(*) on large tables (50K+ edges) is non-trivial, especially via the
|
|
1757
|
+
// NativeDbProxy napi-rs round-trip. When all post-passes were no-ops, the
|
|
1758
|
+
// Rust orchestrator's counts are still accurate — no re-count needed.
|
|
1759
|
+
let finalNodeCount = result.nodeCount ?? 0;
|
|
1760
|
+
let finalEdgeCount = result.edgeCount ?? 0;
|
|
1761
|
+
const postPassWroteData = backfillHappened || chaEdgeCount > 0 || thisDispatchTargetIds.size > 0;
|
|
1762
|
+
if (postPassWroteData) {
|
|
1763
|
+
try {
|
|
1764
|
+
const counts = (ctx.db as unknown as BetterSqlite3Database)
|
|
1765
|
+
.prepare('SELECT (SELECT COUNT(*) FROM nodes) AS n, (SELECT COUNT(*) FROM edges) AS e')
|
|
1766
|
+
.get() as { n: number; e: number };
|
|
1767
|
+
if (counts.n !== finalNodeCount || counts.e !== finalEdgeCount) {
|
|
1768
|
+
finalNodeCount = counts.n;
|
|
1769
|
+
finalEdgeCount = counts.e;
|
|
1770
|
+
setBuildMeta(ctx.db, { node_count: finalNodeCount, edge_count: finalEdgeCount });
|
|
1771
|
+
}
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
debug(`Post-pass node/edge re-count failed: ${toErrorMessage(err)}`);
|
|
895
1774
|
}
|
|
1775
|
+
}
|
|
1776
|
+
info(
|
|
1777
|
+
`Native build orchestrator completed: ${finalNodeCount} nodes, ${finalEdgeCount} edges, ${result.fileCount ?? 0} files`,
|
|
1778
|
+
);
|
|
896
1779
|
|
|
1780
|
+
// ── Structure and analysis fallback (run after edge-writing so roles see full graph) ──
|
|
1781
|
+
// Reconstruct fileSymbols once for both structure and analysis to avoid two
|
|
1782
|
+
// expensive DB scans. The DB handoff above already ensured ctx.db is a proper
|
|
1783
|
+
// better-sqlite3 connection when either flag is set.
|
|
1784
|
+
if (needsStructure || needsAnalysisFallback) {
|
|
897
1785
|
const fileSymbols = reconstructFileSymbolsFromDb(ctx);
|
|
898
1786
|
|
|
899
1787
|
if (needsStructure) {
|
|
@@ -910,33 +1798,12 @@ export async function tryNativeOrchestrator(
|
|
|
910
1798
|
}
|
|
911
1799
|
}
|
|
912
1800
|
|
|
913
|
-
// Engine parity: the native orchestrator silently drops files whose
|
|
914
|
-
// Rust extractor/grammar is missing or fails (e.g. HCL, Scala, Swift on
|
|
915
|
-
// stale native binaries). WASM handles those — backfill via WASM so both
|
|
916
|
-
// engines process the same file set (#967).
|
|
917
|
-
//
|
|
918
|
-
// Detect the gap once (fs walk + 2 DB queries, ~20–30ms) and use it for
|
|
919
|
-
// both gating and the backfill itself. On dirty incrementals/full builds
|
|
920
|
-
// the orchestrator signals trigger backfill, so the walk happens once
|
|
921
|
-
// (instead of redundantly inside backfill). On quiet incrementals we
|
|
922
|
-
// still pay the walk so we can detect brand-new files in dropped-language
|
|
923
|
-
// extensions — a gap that the orchestrator's `detect_removed_files`
|
|
924
|
-
// filter (#1070) leaves open (#1083, #1091). The pre-check is cheap
|
|
925
|
-
// because the expensive part (WASM re-parse of the missing set) is
|
|
926
|
-
// gated below.
|
|
927
|
-
const removedCount = result.removedCount ?? 0;
|
|
928
|
-
const changedCount = result.changedCount ?? 0;
|
|
929
|
-
const gap = detectDroppedLanguageGap(ctx);
|
|
930
|
-
if (
|
|
931
|
-
result.isFullBuild ||
|
|
932
|
-
removedCount > 0 ||
|
|
933
|
-
changedCount > 0 ||
|
|
934
|
-
gap.missingAbs.length > 0 ||
|
|
935
|
-
gap.staleRel.length > 0
|
|
936
|
-
) {
|
|
937
|
-
await backfillNativeDroppedFiles(ctx, gap);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
1801
|
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
941
|
-
return formatNativeTimingResult(p, structurePatchMs, analysisTiming
|
|
1802
|
+
return formatNativeTimingResult(p, structurePatchMs, analysisTiming, {
|
|
1803
|
+
gapDetectMs,
|
|
1804
|
+
chaMs,
|
|
1805
|
+
thisDispatchMs,
|
|
1806
|
+
reclassifyMs,
|
|
1807
|
+
techniqueBackfillMs,
|
|
1808
|
+
});
|
|
942
1809
|
}
|