Skip to content

Commit c6acf26

Browse files
committed
fix(native): resolve this-dispatch in func-prop methods when no extends edges exist
runPostNativeThisDispatch had two bugs that prevented it from resolving `this.g()` inside `f.h = function() {}` (func-prop methods) on the native path: 1. Guard required at least one `extends` edge — func-prop this-dispatch (`f.h = function(){ this.g() }`) works by treating the dot-prefix of the caller name (`f` from `f.h`) as the class prefix; no class inheritance needed. 2. Full-build file selection only unioned files from `extends` edges — files containing only func-prop method nodes were never re-parsed for this-dispatch. 3. Empty `parents` map caused early return — wrong when func-prop methods exist without any class hierarchy (`resolveThisDispatch` handles empty parents by doing direct class-prefix lookup on the first loop iteration). Fix: expand the guard to also fire when dot-named `method` nodes exist in the DB; skip the `parents` query entirely when no `extends` edges exist (avoiding the unnecessary DB round-trip); remove the empty-parents bail-out; add func-prop method nodes to the full-build file selection query. Also adds `this-dispatch-func-prop` to TECHNIQUE_MAP in the resolution benchmark so the mode is correctly bucketed as `cha-rta` if it appears in JS fixture edges. docs check acknowledged Closes #1512
1 parent e5ea649 commit c6acf26

2 files changed

Lines changed: 43 additions & 23 deletions

File tree

src/domain/graph/builder/stages/native-orchestrator.ts

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,13 @@ const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs'
701701
* `this`/`super` receivers, then resolves them through the class hierarchy stored
702702
* in DB `extends` edges — mirroring what `buildChaPostPass` does on the WASM path.
703703
*
704-
* Only runs when `extends` edges exist in the DB; if there is no inheritance
705-
* hierarchy there is nothing to resolve via `this`/`super` dispatch.
704+
* Also handles function-as-object-property methods (`f.h = function() { this.g() }`):
705+
* these use `this` to reference sibling properties on the same object (`f`), so
706+
* `resolveThisDispatch` resolves them by treating the dot-prefix of the caller name
707+
* (`f` from `f.h`) as the class and looking up `f.g` directly — no `extends` edge needed.
708+
*
709+
* Runs when either `extends` edges exist (class inheritance) OR dot-named `method`
710+
* nodes exist (func-prop assignments); skips only when neither is present.
706711
*/
707712
async function runPostNativeThisDispatch(
708713
db: BetterSqlite3Database,
@@ -715,26 +720,39 @@ async function runPostNativeThisDispatch(
715720
// Files containing endpoints of newly inserted edges — lets the caller scope
716721
// role re-classification to the nodes whose fan-in/out actually changed.
717722
const affectedFiles = new Set<string>();
718-
// Fast guard: need at least one extends edge for this/super to have meaning
719-
const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
720-
if (!hasExtends) return { elapsedMs: 0, targetIds, affectedFiles };
721723

722-
// Build parents map: child class → direct parent class (from `extends` edges)
723-
const parentRows = db
724-
.prepare(`
725-
SELECT src.name AS child_name, tgt.name AS parent_name
726-
FROM edges e
727-
JOIN nodes src ON e.source_id = src.id
728-
JOIN nodes tgt ON e.target_id = tgt.id
729-
WHERE e.kind = 'extends'
730-
`)
731-
.all() as Array<{ child_name: string; parent_name: string }>;
724+
// Fast guard: need at least one extends edge (class inheritance) OR a dot-named
725+
// method node (func-prop assignment: `f.h = function() { this.g() }`) for
726+
// this/super dispatch to produce any edges.
727+
const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
728+
const hasFuncPropMethod = db
729+
.prepare(`SELECT 1 FROM nodes WHERE kind = 'method' AND INSTR(name, '.') > 0 LIMIT 1`)
730+
.get();
731+
if (!hasExtends && !hasFuncPropMethod) return { elapsedMs: 0, targetIds, affectedFiles };
732+
733+
// Build parents map: child class → direct parent class (from `extends` edges).
734+
// May be empty when only func-prop methods exist (no class inheritance) —
735+
// resolveThisDispatch handles that case via direct class-prefix lookup.
736+
const parentRows = hasExtends
737+
? (db
738+
.prepare(`
739+
SELECT src.name AS child_name, tgt.name AS parent_name
740+
FROM edges e
741+
JOIN nodes src ON e.source_id = src.id
742+
JOIN nodes tgt ON e.target_id = tgt.id
743+
WHERE e.kind = 'extends'
744+
`)
745+
.all() as Array<{ child_name: string; parent_name: string }>)
746+
: [];
732747

733748
const parents = new Map<string, string>();
734749
for (const row of parentRows) {
735750
if (!parents.has(row.child_name)) parents.set(row.child_name, row.parent_name);
736751
}
737-
if (parents.size === 0) return { elapsedMs: 0, targetIds, affectedFiles };
752+
// Note: parents may be empty when hasFuncPropMethod but !hasExtends — that is
753+
// intentional. resolveThisDispatch still resolves `this.g()` inside `f.h` by
754+
// treating `f` (the dot-prefix of callerName `f.h`) as the class and looking
755+
// up `f.g` directly via lookup.byName(), without traversing the parents chain.
738756

739757
const chaCtx: ChaContext = {
740758
implementors: new Map(), // not needed for this/super resolution
@@ -747,13 +765,11 @@ async function runPostNativeThisDispatch(
747765
// On a full build we do NOT re-parse every JS/TS file — that would WASM-parse
748766
// the entire project on top of the native pass, causing a massive regression
749767
// (measured: +358% ms/file on codegraph itself). Instead we restrict to files
750-
// that are part of the class inheritance hierarchy: both subclass files (which
751-
// contain `super.X()` calls dispatching to a parent) and parent-class files
752-
// (whose method bodies contain `this.X()` calls that CHA must resolve). Any
753-
// file not in the hierarchy has no `extends` relationship, so `this`/`super`
754-
// calls in it either resolve locally (same-class dispatch, already handled by
755-
// the direct-call edge) or have no class context — and will be skipped by
756-
// `resolveThisDispatch` anyway.
768+
// that are part of the class inheritance hierarchy (both subclass files with
769+
// `super.X()` calls and parent-class files with `this.X()` calls) OR that
770+
// contain dot-named method nodes (func-prop assignments whose bodies may call
771+
// `this.sibling()`). Any file not in either set has no class or object context
772+
// where `this`/`super` dispatch would produce new edges.
757773
let relFiles: string[];
758774
if (isFullBuild || !changedFiles) {
759775
const rows = db
@@ -768,6 +784,9 @@ async function runPostNativeThisDispatch(
768784
FROM edges e
769785
JOIN nodes tgt ON e.target_id = tgt.id
770786
WHERE e.kind = 'extends' AND tgt.file IS NOT NULL
787+
UNION
788+
SELECT file FROM nodes
789+
WHERE kind = 'method' AND INSTR(name, '.') > 0 AND file IS NOT NULL
771790
)
772791
`)
773792
.all() as Array<{ file: string }>;

tests/benchmarks/resolution/resolution-benchmark.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const TECHNIQUE_MAP: Record<string, string> = {
8585
'class-inheritance': 'cha-rta',
8686
'class-hierarchy': 'cha-rta',
8787
'trait-dispatch': 'cha-rta',
88+
'this-dispatch-func-prop': 'cha-rta',
8889
're-export': 'barrel',
8990
closure: 'points-to',
9091
'higher-order': 'points-to',

0 commit comments

Comments
 (0)