Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
68296d7
feat(resolver): phase 8.4 — barrel file re-export chain resolution fo…
carlos-alm Jun 4, 2026
27b6054
fix(resolver): remove unreachable ?? null after confirmed .has() chec…
carlos-alm Jun 4, 2026
20de6c5
test(wasm): document #1297 guard in chained-barrel regression test
carlos-alm Jun 4, 2026
60a0f51
feat(resolver): phase 8.5 — enhanced dynamic dispatch resolution (CHA…
carlos-alm Jun 4, 2026
7128562
fix(resolver): document Phase 8.2 side-effect and strengthen barrel t…
carlos-alm Jun 4, 2026
8386fda
fix(cha): propagate newExpressions through WASM worker serialization …
carlos-alm Jun 4, 2026
1b2b5b3
fix(parity): re-classify roles after CHA post-pass for native engine …
carlos-alm Jun 4, 2026
cd3d312
fix: resolve merge conflicts with main
carlos-alm Jun 4, 2026
a704f5b
fix(cha): add debug log + transaction wrapper to runPostNativeCha (#1…
carlos-alm Jun 4, 2026
a4c1505
fix(bench): add CHA-expanded C# edges to expected-edges manifest (#1302)
carlos-alm Jun 4, 2026
24db38b
fix(cha): extend RTA query to non-class nodes, skip filter without ev…
carlos-alm Jun 4, 2026
4b4b2ff
docs(bench): jelly vs codegraph call resolution comparison on JS/TS f…
carlos-alm Jun 4, 2026
39f43bb
Revert "research(bench): Jelly vs codegraph call resolution compariso…
carlos-alm Jun 4, 2026
6c80dfb
feat(stats): by_technique breakdown in codegraph stats (Phase 8.6 fol…
carlos-alm Jun 4, 2026
442da45
fix(cha): add missing technique field to CHA dispatch EdgeRowTuple pu…
carlos-alm Jun 5, 2026
1617aef
fix(cha): correct inaccurate cycle guard comment in resolveThisDispatch
carlos-alm Jun 5, 2026
91be059
fix(cha): scope existingPairs dedup and compute file-pair-aware confi…
carlos-alm Jun 5, 2026
b7688f5
feat(cha): transitive multi-level class hierarchy expansion in CHA di…
carlos-alm Jun 5, 2026
ab40427
fix(cha): drop zero-confidence edges and scope seenByPair to calls in…
carlos-alm Jun 5, 2026
0634bd5
merge: sync with main to resolve conflicts in build-edges and native-…
carlos-alm Jun 5, 2026
779f7de
fix(cha): chunk IN-clause params in applyEdgeTechniques and backfillE…
carlos-alm Jun 5, 2026
2fb19be
test(cha): skip transitive CHA native tests until Rust binary updated…
carlos-alm Jun 5, 2026
739db85
fix(cha): tag runPostNativeCha edges with technique='cha' (#1302)
carlos-alm Jun 5, 2026
ae96ac3
fix(cha): guard CHA dedup against ptsEdgeRows to prevent duplicate ed…
carlos-alm Jun 5, 2026
d6e4335
test(bench): add JS/TS super.method() class-inheritance fixtures (#1315)
carlos-alm Jun 5, 2026
7a1f368
fix(cha): export CHA_DISPATCH_PENALTY and consume it in native-orches…
carlos-alm Jun 5, 2026
6841a26
test: replace skipIf/silent-pass patterns with todo and ctx.skip (#1325)
carlos-alm Jun 5, 2026
f8b1284
merge: sync with origin/main, keep review fixes for CHA and test patt…
carlos-alm Jun 5, 2026
aa8d6f8
merge: sync with origin/main (#1325)
carlos-alm Jun 5, 2026
34c74fd
feat(resolver): resolve prototype-based method calls (#1317)
carlos-alm Jun 5, 2026
0ab5291
fix(test): retry rmSync with backoff on Windows EBUSY in embedding-re…
carlos-alm Jun 5, 2026
6021541
fix: resolve merge conflicts with main
carlos-alm Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/domain/graph/builder/call-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,40 @@ export function resolveByMethodOrGlobal(
? call.receiver.slice('this.'.length)
: call.receiver;
const typeEntry = typeMap.get(effectiveReceiver) ?? typeMap.get(call.receiver);
const typeName = typeEntry
let typeName = typeEntry
? typeof typeEntry === 'string'
? typeEntry
: (typeEntry as { type?: string }).type
: null;

// Handle inline new-expression receivers: `(new Foo).bar()` or `(new Foo()).bar()`.
// extractReceiverName returns the raw node text for non-identifier nodes, so `(new A).t()`
// produces receiver='(new A)'. Extract the constructor name directly.
if (!typeName && call.receiver) {
const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver);
if (m?.[1]) typeName = m[1];
}

if (typeName) {
const typed = lookup.byName(`${typeName}.${call.name}`).filter((n) => n.kind === 'method');
if (typed.length > 0) return typed;

// Prototype alias: `Foo.prototype.bar = identifier` seeds typeMap['Foo.bar'] = { type: identifier }.
// Checked after the symbol-DB lookup so an actual method definition always wins.
const protoEntry = typeMap.get(`${typeName}.${call.name}`);
const protoTarget = protoEntry
? typeof protoEntry === 'string'
? protoEntry
: (protoEntry as { type?: string }).type
: null;
if (protoTarget) {
const resolved = lookup
.byName(protoTarget)
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
if (resolved.length > 0) return resolved;
}
}

// Phase 8.3d: composite pts key — `obj.prop = fn` seeds typeMap['obj.prop'] = { type: 'fn' }.
// When a call site references `obj.prop` as a callback, resolve directly to the target fn.
const compositeEntry = typeMap.get(`${call.receiver}.${call.name}`);
Expand Down
2 changes: 1 addition & 1 deletion src/domain/graph/builder/stages/build-edges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ interface NativeEdge {
}

/** Phase 8.5: confidence penalty applied to CHA-dispatch edges. */
const CHA_DISPATCH_PENALTY = 0.1;
export const CHA_DISPATCH_PENALTY = 0.1;

// ── Node lookup setup ───────────────────────────────────────────────────

Expand Down
8 changes: 5 additions & 3 deletions src/domain/graph/builder/stages/native-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
readFileSafe,
} from '../helpers.js';
import { NativeDbProxy } from '../native-db-proxy.js';
import { CHA_DISPATCH_PENALTY } from './build-edges.js';
import { closeNativeDb } from './native-db-lifecycle.js';

// ── Native orchestrator types ──────────────────────────────────────────
Expand Down Expand Up @@ -467,7 +468,7 @@ function runPostNativeCha(db: BetterSqlite3Database): Set<number> {

// Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
// Include the caller node's file so confidence can be computed file-pair-aware,
// matching the WASM path's computeConfidence(callerFile, targetFile, null) - 0.1 formula.
// matching the WASM path's computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY formula.
const callToMethods = db
.prepare(`
SELECT e.source_id, tgt.name AS method_name, src.file AS caller_file
Expand Down Expand Up @@ -534,10 +535,11 @@ function runPostNativeCha(db: BetterSqlite3Database): Set<number> {
const key = `${source_id}|${methodNode.id}`;
if (seen.has(key)) continue;
seen.add(key);
// Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - 0.1 penalty)
// Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - CHA_DISPATCH_PENALTY)
// Skip zero-confidence edges to match buildFileCallEdges / buildChaPostPass behaviour.
const conf =
computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) - 0.1;
computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) -
CHA_DISPATCH_PENALTY;
if (conf <= 0) continue;
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
newTargetIds.add(methodNode.id);
Expand Down
168 changes: 161 additions & 7 deletions src/extractors/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
// Extract typeMap with intra-file return-type propagation
extractTypeMapWalk(tree.rootNode, typeMap, returnTypeMap, callAssignments, fnRefBindings);

// Prototype-based method definitions: `Foo.prototype.bar = fn` and `Foo.prototype = { bar: fn }`
extractPrototypeMethodsWalk(tree.rootNode, definitions, typeMap);

// Phase 8.3c: Extract call-site argument bindings for parameter-flow pts analysis
extractParamBindingsWalk(tree.rootNode, paramBindings);

Expand Down Expand Up @@ -476,7 +479,7 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
if (declarator?.type !== 'variable_declarator') continue;
const nameN = declarator.childForFieldName('name');
const valueN = declarator.childForFieldName('value');
if (!nameN || nameN.type !== 'identifier' || !valueN) continue;
if (nameN?.type !== 'identifier' || !valueN) continue;
// Skip functions — already captured by query patterns
const valType = valueN.type;
if (valType === 'arrow_function' || valType === 'function_expression' || valType === 'function')
Expand Down Expand Up @@ -612,6 +615,8 @@ function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput {
ctx.callAssignments,
ctx.fnRefBindings,
);
// Prototype-based method definitions: `Foo.prototype.bar = fn` and `Foo.prototype = { bar: fn }`
extractPrototypeMethodsWalk(tree.rootNode, ctx.definitions, ctx.typeMap!);
// Phase 8.3c: Extract call-site argument bindings for parameter-flow pts analysis
extractParamBindingsWalk(tree.rootNode, ctx.paramBindings!);
// Phase 8.5: collect all `new X()` constructor names for RTA instantiation tracking
Expand Down Expand Up @@ -1162,7 +1167,7 @@ function extractSimpleTypeName(typeAnnotationNode: TreeSitterNode): string | nul
}

function extractNewExprTypeName(newExprNode: TreeSitterNode): string | null {
if (!newExprNode || newExprNode.type !== 'new_expression') return null;
if (newExprNode?.type !== 'new_expression') return null;
const ctor = newExprNode.childForFieldName('constructor') || newExprNode.child(1);
if (!ctor) return null;
if (ctor.type === 'identifier') return ctor.text;
Expand Down Expand Up @@ -1275,7 +1280,7 @@ function storeReturnType(
function findReturnNewExprType(bodyNode: TreeSitterNode): string | null {
for (let i = 0; i < bodyNode.childCount; i++) {
const child = bodyNode.child(i);
if (!child || child.type !== 'return_statement') continue;
if (child?.type !== 'return_statement') continue;
for (let j = 0; j < child.childCount; j++) {
const expr = child.child(j);
if (expr?.type === 'new_expression') return extractNewExprTypeName(expr);
Expand Down Expand Up @@ -1442,7 +1447,7 @@ function handleVarDeclaratorTypeMap(
fnRefBindings?: FnRefBinding[],
): void {
const nameN = node.childForFieldName('name');
if (!nameN || nameN.type !== 'identifier') return;
if (nameN?.type !== 'identifier') return;

const typeAnno = findChild(node, 'type_annotation');
const valueN = node.childForFieldName('value');
Expand Down Expand Up @@ -1531,7 +1536,7 @@ function handleVarDeclaratorTypeMap(
function handleParamTypeMap(node: TreeSitterNode, typeMap: Map<string, TypeMapEntry>): void {
const nameNode =
node.childForFieldName('pattern') || node.childForFieldName('left') || node.child(0);
if (!nameNode || nameNode.type !== 'identifier') return;
if (nameNode?.type !== 'identifier') return;
const typeAnno = findChild(node, 'type_annotation');
if (typeAnno) {
const typeName = extractSimpleTypeName(typeAnno);
Expand Down Expand Up @@ -1924,7 +1929,7 @@ function extractCallbackDefinition(
fn?: TreeSitterNode | null,
): Definition | null {
if (!fn) fn = callNode.childForFieldName('function');
if (!fn || fn.type !== 'member_expression') return null;
if (fn?.type !== 'member_expression') return null;

const prop = fn.childForFieldName('property');
if (!prop) return null;
Expand Down Expand Up @@ -2035,7 +2040,7 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {
// Skip await_expression wrapper if present
if (current && current.type === 'await_expression') current = current.parent;
// We should now be at a variable_declarator (or not, if standalone import())
if (!current || current.type !== 'variable_declarator') return [];
if (current?.type !== 'variable_declarator') return [];

const nameNode = current.childForFieldName('name');
if (!nameNode) return [];
Expand Down Expand Up @@ -2078,3 +2083,152 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {

return [];
}

// ── Phase 8.X: Prototype-based method extraction ────────────────────────────

/**
* Walk the AST and extract prototype-based method definitions and aliases.
*
* Handles three patterns:
* 1. `Foo.prototype.bar = function(){...}` — emits Foo.bar as method definition
* 2. `Foo.prototype.bar = identifier` — sets typeMap['Foo.bar'] = { type: identifier }
* 3. `Foo.prototype = { bar: fn, ... }` — emits defs and typeMap entries per property
*
* Emitting definitions under the canonical `ClassName.methodName` name lets the
* existing typeMap-based call resolver find them when a typed receiver dispatches
* `instance.method()` (lookup.byName('C.foo') in resolveByMethodOrGlobal).
*
* typeMap entries for identifier aliases (`Foo.bar → { type: 'someId' }`) are
* consumed by the prototype-alias fallback added to resolveByMethodOrGlobal.
*/
function extractPrototypeMethodsWalk(
rootNode: TreeSitterNode,
definitions: Definition[],
typeMap: Map<string, TypeMapEntry>,
): void {
function walk(node: TreeSitterNode, depth: number): void {
if (depth >= MAX_WALK_DEPTH) return;
if (node.type === 'expression_statement') {
const expr = node.child(0);
if (expr?.type === 'assignment_expression') {
const lhs = expr.childForFieldName('left');
const rhs = expr.childForFieldName('right');
if (lhs && rhs) handlePrototypeAssignment(lhs, rhs, definitions, typeMap);
}
}
for (let i = 0; i < node.childCount; i++) {
walk(node.child(i)!, depth + 1);
}
}
walk(rootNode, 0);
}

/**
* Handle an assignment_expression that may be a prototype assignment.
*
* Matches:
* - `Foo.prototype.bar = rhs` (lhs ends in .prototype.bar)
* - `Foo.prototype = { ... }` (lhs ends in .prototype, rhs is object literal)
*/
function handlePrototypeAssignment(
lhs: TreeSitterNode,
rhs: TreeSitterNode,
definitions: Definition[],
typeMap: Map<string, TypeMapEntry>,
): void {
if (lhs.type !== 'member_expression') return;

const lhsObj = lhs.childForFieldName('object');
const lhsProp = lhs.childForFieldName('property');
if (!lhsObj || !lhsProp) return;

// Pattern 1: `Foo.prototype.bar = rhs`
// lhs.object is `Foo.prototype` (member_expression), lhs.property is `bar`
if (
lhsObj.type === 'member_expression' &&
(lhsProp.type === 'property_identifier' || lhsProp.type === 'identifier')
) {
const protoObj = lhsObj.childForFieldName('object');
const protoProp = lhsObj.childForFieldName('property');
if (
protoObj?.type === 'identifier' &&
protoProp?.text === 'prototype' &&
!BUILTIN_GLOBALS.has(protoObj.text)
) {
emitPrototypeMethod(protoObj.text, lhsProp.text, rhs, definitions, typeMap);
}
return;
}

// Pattern 2: `Foo.prototype = { bar: fn, ... }`
// lhs.object is `Foo` (identifier), lhs.property is `prototype`
if (
lhsObj.type === 'identifier' &&
lhsProp.text === 'prototype' &&
!BUILTIN_GLOBALS.has(lhsObj.text) &&
rhs.type === 'object'
) {
extractPrototypeObjectLiteral(lhsObj.text, rhs, definitions, typeMap);
}
}

/** Emit one prototype method definition or typeMap alias for `ClassName.methodName = rhs`. */
function emitPrototypeMethod(
className: string,
methodName: string,
rhs: TreeSitterNode,
definitions: Definition[],
typeMap: Map<string, TypeMapEntry>,
): void {
const fullName = `${className}.${methodName}`;
if (rhs.type === 'function_expression' || rhs.type === 'arrow_function') {
definitions.push({
name: fullName,
kind: 'method',
line: nodeStartLine(rhs),
endLine: nodeEndLine(rhs),
});
} else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) {
// Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' }
// Consumed by the prototype-alias fallback in resolveByMethodOrGlobal.
setTypeMapEntry(typeMap, fullName, rhs.text, 0.9);
}
}

/** Iterate over an object literal assigned to `Foo.prototype` and emit defs/aliases. */
function extractPrototypeObjectLiteral(
className: string,
objNode: TreeSitterNode,
definitions: Definition[],
typeMap: Map<string, TypeMapEntry>,
): void {
for (let i = 0; i < objNode.childCount; i++) {
const child = objNode.child(i);
if (!child) continue;

if (child.type === 'method_definition') {
// Shorthand method: `Foo.prototype = { bar() {} }`
const nameNode = child.childForFieldName('name');
if (nameNode) {
definitions.push({
name: `${className}.${nameNode.text}`,
kind: 'method',
line: nodeStartLine(child),
endLine: nodeEndLine(child),
});
}
continue;
}

if (child.type !== 'pair') continue;

const keyNode = child.childForFieldName('key');
const valueNode = child.childForFieldName('value');
if (!keyNode || !valueNode) continue;

const methodName = keyNode.type === 'string' ? keyNode.text.replace(/['"]/g, '') : keyNode.text;
if (!methodName) continue;

emitPrototypeMethod(className, methodName, valueNode, definitions, typeMap);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,55 @@
"kind": "calls",
"mode": "constructor",
"notes": "new UserService() — class instantiation tracked as consumption"
},
{
"source": { "name": "Dog.speak", "file": "inheritance.js" },
"target": { "name": "Animal.speak", "file": "inheritance.js" },
"kind": "calls",
"mode": "class-inheritance",
"notes": "super.speak() in Dog.speak resolves to Animal.speak via CHA parents map"
},
{
"source": { "name": "Puppy.speak", "file": "inheritance.js" },
"target": { "name": "Dog.speak", "file": "inheritance.js" },
"kind": "calls",
"mode": "class-inheritance",
"notes": "super.speak() in Puppy.speak resolves to Dog.speak (nearest parent), not Animal.speak"
},
{
"source": { "name": "DoubleCounter.count", "file": "inheritance.js" },
"target": { "name": "Counter.count", "file": "inheritance.js" },
"kind": "calls",
"mode": "class-inheritance",
"notes": "static super.count() in DoubleCounter.count resolves to Counter.count via CHA parents map"
},
{
"source": { "name": "runPrototypes", "file": "prototypes.js" },
"target": { "name": "C", "file": "prototypes.js" },
"kind": "calls",
"mode": "constructor",
"notes": "new C() — constructor call to function-based constructor"
},
{
"source": { "name": "runPrototypes", "file": "prototypes.js" },
"target": { "name": "C.foo", "file": "prototypes.js" },
"kind": "calls",
"mode": "receiver-typed",
"notes": "v.foo() — v typed as C via new C(), C.foo defined via C.prototype = { foo: fn }"
},
{
"source": { "name": "testPrototypeAlias", "file": "prototypes2.js" },
"target": { "name": "A", "file": "prototypes2.js" },
"kind": "calls",
"mode": "constructor",
"notes": "new A() — inline constructor call in new A().t()"
},
{
"source": { "name": "testPrototypeAlias", "file": "prototypes2.js" },
"target": { "name": "f", "file": "prototypes2.js" },
"kind": "calls",
"mode": "receiver-typed",
"notes": "new A().t() — A.prototype.t = f alias; inline new receiver resolved to type A, typeMap['A.t'] = f"
}
]
}
Loading
Loading