Skip to content

Commit 4299168

Browse files
committed
fix: resolve merge conflicts with main
Merge origin/main (16a1182, 34988f8) into feat/phase-8.3e: - expected-edges.json: keep define-property edges (pts-define-property, pts-create-prototype) from this branch AND inheritance/prototypes edges from main's #1325; merged file now has 30 total edges - embedding-regression.test.ts: use main's retry-backoff afterAll and TestContext/ctx.skip() pattern; import from src/db/index.js
2 parents 81a0041 + 16a1182 commit 4299168

13 files changed

Lines changed: 404 additions & 63 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1010
1111
> **Never silently skip verification.** If tests, builds, or any verification step cannot run or fails for any reason (compilation errors, platform issues, missing dependencies), STOP and report the issue to the user immediately. Never silently proceed with unverified changes. Let the user decide whether to proceed — do not make that decision yourself.
1212
13-
> **Scope discipline — open issues, don't expand scope.** When you encounter a problem unrelated to the current task — pre-existing bugs, code that needs a bigger refactor, or any defect that does not directly affect the result of what you're doing — open a GitHub issue with `gh issue create` and keep going. Only fix it inline when it directly affects the correctness or outcome of the work in front of you. This keeps PRs focused (one concern per PR) while ensuring the finding is not lost.
13+
> **Scope discipline — open issues, don't expand scope.** When you encounter anything out of scope — a pre-existing bug, a refactor opportunity, a potential improvement, a missing feature, or any other finding that doesn't directly affect the correctness of the current task**immediately open a GitHub issue with `gh issue create` before continuing**. Do not hold the finding in memory or defer it to a comment. Only address it inline when it directly blocks the result of the work in front of you. This keeps PRs focused (one concern per PR) while ensuring no finding is lost.
1414
1515
> **Prioritize the best architecture, not the smallest diff.** Do not default to the simplest or most localized fix. Choose the approach that fits the codebase's architecture best, even when that means larger changes, moving code across modules, or restructuring an abstraction. Do not be afraid of bigger changes — a larger diff that leaves the design healthier is preferable to a small diff that entrenches a poor structure. Surface the architectural reasoning to the user; don't silently shrink the change to avoid the work.
1616

src/domain/graph/builder/call-resolver.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,40 @@ export function resolveByMethodOrGlobal(
7373
? call.receiver.slice('this.'.length)
7474
: call.receiver;
7575
const typeEntry = typeMap.get(effectiveReceiver) ?? typeMap.get(call.receiver);
76-
const typeName = typeEntry
76+
let typeName = typeEntry
7777
? typeof typeEntry === 'string'
7878
? typeEntry
7979
: (typeEntry as { type?: string }).type
8080
: null;
81+
82+
// Handle inline new-expression receivers: `(new Foo).bar()` or `(new Foo()).bar()`.
83+
// extractReceiverName returns the raw node text for non-identifier nodes, so `(new A).t()`
84+
// produces receiver='(new A)'. Extract the constructor name directly.
85+
if (!typeName && call.receiver) {
86+
const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver);
87+
if (m?.[1]) typeName = m[1];
88+
}
89+
8190
if (typeName) {
8291
const typed = lookup.byName(`${typeName}.${call.name}`).filter((n) => n.kind === 'method');
8392
if (typed.length > 0) return typed;
93+
94+
// Prototype alias: `Foo.prototype.bar = identifier` seeds typeMap['Foo.bar'] = { type: identifier }.
95+
// Checked after the symbol-DB lookup so an actual method definition always wins.
96+
const protoEntry = typeMap.get(`${typeName}.${call.name}`);
97+
const protoTarget = protoEntry
98+
? typeof protoEntry === 'string'
99+
? protoEntry
100+
: (protoEntry as { type?: string }).type
101+
: null;
102+
if (protoTarget) {
103+
const resolved = lookup
104+
.byName(protoTarget)
105+
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
106+
if (resolved.length > 0) return resolved;
107+
}
84108
}
109+
85110
// Phase 8.3d: composite pts key — `obj.prop = fn` seeds typeMap['obj.prop'] = { type: 'fn' }.
86111
// When a call site references `obj.prop` as a callback, resolve directly to the target fn.
87112
const compositeEntry = typeMap.get(`${call.receiver}.${call.name}`);

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ interface NativeEdge {
7878
}
7979

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

8383
// ── Node lookup setup ───────────────────────────────────────────────────
8484

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
readFileSafe,
5353
} from '../helpers.js';
5454
import { NativeDbProxy } from '../native-db-proxy.js';
55+
import { CHA_DISPATCH_PENALTY } from './build-edges.js';
5556
import { closeNativeDb } from './native-db-lifecycle.js';
5657

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

468469
// Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
469470
// Include the caller node's file so confidence can be computed file-pair-aware,
470-
// matching the WASM path's computeConfidence(callerFile, targetFile, null) - 0.1 formula.
471+
// matching the WASM path's computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY formula.
471472
const callToMethods = db
472473
.prepare(`
473474
SELECT e.source_id, tgt.name AS method_name, src.file AS caller_file
@@ -534,10 +535,11 @@ function runPostNativeCha(db: BetterSqlite3Database): Set<number> {
534535
const key = `${source_id}|${methodNode.id}`;
535536
if (seen.has(key)) continue;
536537
seen.add(key);
537-
// Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - 0.1 penalty)
538+
// Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - CHA_DISPATCH_PENALTY)
538539
// Skip zero-confidence edges to match buildFileCallEdges / buildChaPostPass behaviour.
539540
const conf =
540-
computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) - 0.1;
541+
computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) -
542+
CHA_DISPATCH_PENALTY;
541543
if (conf <= 0) continue;
542544
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
543545
newTargetIds.add(methodNode.id);

src/extractors/javascript.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,9 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
345345
// Extract typeMap with intra-file return-type propagation
346346
extractTypeMapWalk(tree.rootNode, typeMap, returnTypeMap, callAssignments, fnRefBindings);
347347

348+
// Prototype-based method definitions: `Foo.prototype.bar = fn` and `Foo.prototype = { bar: fn }`
349+
extractPrototypeMethodsWalk(tree.rootNode, definitions, typeMap);
350+
348351
// Phase 8.3c: Extract call-site argument bindings for parameter-flow pts analysis
349352
extractParamBindingsWalk(tree.rootNode, paramBindings);
350353

@@ -612,6 +615,8 @@ function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput {
612615
ctx.callAssignments,
613616
ctx.fnRefBindings,
614617
);
618+
// Prototype-based method definitions: `Foo.prototype.bar = fn` and `Foo.prototype = { bar: fn }`
619+
extractPrototypeMethodsWalk(tree.rootNode, ctx.definitions, ctx.typeMap!);
615620
// Phase 8.3c: Extract call-site argument bindings for parameter-flow pts analysis
616621
extractParamBindingsWalk(tree.rootNode, ctx.paramBindings!);
617622
// Phase 8.5: collect all `new X()` constructor names for RTA instantiation tracking
@@ -2197,3 +2202,152 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {
21972202

21982203
return [];
21992204
}
2205+
2206+
// ── Phase 8.X: Prototype-based method extraction ────────────────────────────
2207+
2208+
/**
2209+
* Walk the AST and extract prototype-based method definitions and aliases.
2210+
*
2211+
* Handles three patterns:
2212+
* 1. `Foo.prototype.bar = function(){...}` — emits Foo.bar as method definition
2213+
* 2. `Foo.prototype.bar = identifier` — sets typeMap['Foo.bar'] = { type: identifier }
2214+
* 3. `Foo.prototype = { bar: fn, ... }` — emits defs and typeMap entries per property
2215+
*
2216+
* Emitting definitions under the canonical `ClassName.methodName` name lets the
2217+
* existing typeMap-based call resolver find them when a typed receiver dispatches
2218+
* `instance.method()` (lookup.byName('C.foo') in resolveByMethodOrGlobal).
2219+
*
2220+
* typeMap entries for identifier aliases (`Foo.bar → { type: 'someId' }`) are
2221+
* consumed by the prototype-alias fallback added to resolveByMethodOrGlobal.
2222+
*/
2223+
function extractPrototypeMethodsWalk(
2224+
rootNode: TreeSitterNode,
2225+
definitions: Definition[],
2226+
typeMap: Map<string, TypeMapEntry>,
2227+
): void {
2228+
function walk(node: TreeSitterNode, depth: number): void {
2229+
if (depth >= MAX_WALK_DEPTH) return;
2230+
if (node.type === 'expression_statement') {
2231+
const expr = node.child(0);
2232+
if (expr?.type === 'assignment_expression') {
2233+
const lhs = expr.childForFieldName('left');
2234+
const rhs = expr.childForFieldName('right');
2235+
if (lhs && rhs) handlePrototypeAssignment(lhs, rhs, definitions, typeMap);
2236+
}
2237+
}
2238+
for (let i = 0; i < node.childCount; i++) {
2239+
walk(node.child(i)!, depth + 1);
2240+
}
2241+
}
2242+
walk(rootNode, 0);
2243+
}
2244+
2245+
/**
2246+
* Handle an assignment_expression that may be a prototype assignment.
2247+
*
2248+
* Matches:
2249+
* - `Foo.prototype.bar = rhs` (lhs ends in .prototype.bar)
2250+
* - `Foo.prototype = { ... }` (lhs ends in .prototype, rhs is object literal)
2251+
*/
2252+
function handlePrototypeAssignment(
2253+
lhs: TreeSitterNode,
2254+
rhs: TreeSitterNode,
2255+
definitions: Definition[],
2256+
typeMap: Map<string, TypeMapEntry>,
2257+
): void {
2258+
if (lhs.type !== 'member_expression') return;
2259+
2260+
const lhsObj = lhs.childForFieldName('object');
2261+
const lhsProp = lhs.childForFieldName('property');
2262+
if (!lhsObj || !lhsProp) return;
2263+
2264+
// Pattern 1: `Foo.prototype.bar = rhs`
2265+
// lhs.object is `Foo.prototype` (member_expression), lhs.property is `bar`
2266+
if (
2267+
lhsObj.type === 'member_expression' &&
2268+
(lhsProp.type === 'property_identifier' || lhsProp.type === 'identifier')
2269+
) {
2270+
const protoObj = lhsObj.childForFieldName('object');
2271+
const protoProp = lhsObj.childForFieldName('property');
2272+
if (
2273+
protoObj?.type === 'identifier' &&
2274+
protoProp?.text === 'prototype' &&
2275+
!BUILTIN_GLOBALS.has(protoObj.text)
2276+
) {
2277+
emitPrototypeMethod(protoObj.text, lhsProp.text, rhs, definitions, typeMap);
2278+
}
2279+
return;
2280+
}
2281+
2282+
// Pattern 2: `Foo.prototype = { bar: fn, ... }`
2283+
// lhs.object is `Foo` (identifier), lhs.property is `prototype`
2284+
if (
2285+
lhsObj.type === 'identifier' &&
2286+
lhsProp.text === 'prototype' &&
2287+
!BUILTIN_GLOBALS.has(lhsObj.text) &&
2288+
rhs.type === 'object'
2289+
) {
2290+
extractPrototypeObjectLiteral(lhsObj.text, rhs, definitions, typeMap);
2291+
}
2292+
}
2293+
2294+
/** Emit one prototype method definition or typeMap alias for `ClassName.methodName = rhs`. */
2295+
function emitPrototypeMethod(
2296+
className: string,
2297+
methodName: string,
2298+
rhs: TreeSitterNode,
2299+
definitions: Definition[],
2300+
typeMap: Map<string, TypeMapEntry>,
2301+
): void {
2302+
const fullName = `${className}.${methodName}`;
2303+
if (rhs.type === 'function_expression' || rhs.type === 'arrow_function') {
2304+
definitions.push({
2305+
name: fullName,
2306+
kind: 'method',
2307+
line: nodeStartLine(rhs),
2308+
endLine: nodeEndLine(rhs),
2309+
});
2310+
} else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) {
2311+
// Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' }
2312+
// Consumed by the prototype-alias fallback in resolveByMethodOrGlobal.
2313+
setTypeMapEntry(typeMap, fullName, rhs.text, 0.9);
2314+
}
2315+
}
2316+
2317+
/** Iterate over an object literal assigned to `Foo.prototype` and emit defs/aliases. */
2318+
function extractPrototypeObjectLiteral(
2319+
className: string,
2320+
objNode: TreeSitterNode,
2321+
definitions: Definition[],
2322+
typeMap: Map<string, TypeMapEntry>,
2323+
): void {
2324+
for (let i = 0; i < objNode.childCount; i++) {
2325+
const child = objNode.child(i);
2326+
if (!child) continue;
2327+
2328+
if (child.type === 'method_definition') {
2329+
// Shorthand method: `Foo.prototype = { bar() {} }`
2330+
const nameNode = child.childForFieldName('name');
2331+
if (nameNode) {
2332+
definitions.push({
2333+
name: `${className}.${nameNode.text}`,
2334+
kind: 'method',
2335+
line: nodeStartLine(child),
2336+
endLine: nodeEndLine(child),
2337+
});
2338+
}
2339+
continue;
2340+
}
2341+
2342+
if (child.type !== 'pair') continue;
2343+
2344+
const keyNode = child.childForFieldName('key');
2345+
const valueNode = child.childForFieldName('value');
2346+
if (!keyNode || !valueNode) continue;
2347+
2348+
const methodName = keyNode.type === 'string' ? keyNode.text.replace(/['"]/g, '') : keyNode.text;
2349+
if (!methodName) continue;
2350+
2351+
emitPrototypeMethod(className, methodName, valueNode, definitions, typeMap);
2352+
}
2353+
}

tests/benchmarks/resolution/fixtures/javascript/expected-edges.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,55 @@
163163
"kind": "calls",
164164
"mode": "pts-create-prototype",
165165
"notes": "obj.f2() — resolved via Object.create({ f1, f2 })"
166+
},
167+
{
168+
"source": { "name": "Dog.speak", "file": "inheritance.js" },
169+
"target": { "name": "Animal.speak", "file": "inheritance.js" },
170+
"kind": "calls",
171+
"mode": "class-inheritance",
172+
"notes": "super.speak() in Dog.speak resolves to Animal.speak via CHA parents map"
173+
},
174+
{
175+
"source": { "name": "Puppy.speak", "file": "inheritance.js" },
176+
"target": { "name": "Dog.speak", "file": "inheritance.js" },
177+
"kind": "calls",
178+
"mode": "class-inheritance",
179+
"notes": "super.speak() in Puppy.speak resolves to Dog.speak (nearest parent), not Animal.speak"
180+
},
181+
{
182+
"source": { "name": "DoubleCounter.count", "file": "inheritance.js" },
183+
"target": { "name": "Counter.count", "file": "inheritance.js" },
184+
"kind": "calls",
185+
"mode": "class-inheritance",
186+
"notes": "static super.count() in DoubleCounter.count resolves to Counter.count via CHA parents map"
187+
},
188+
{
189+
"source": { "name": "runPrototypes", "file": "prototypes.js" },
190+
"target": { "name": "C", "file": "prototypes.js" },
191+
"kind": "calls",
192+
"mode": "constructor",
193+
"notes": "new C() — constructor call to function-based constructor"
194+
},
195+
{
196+
"source": { "name": "runPrototypes", "file": "prototypes.js" },
197+
"target": { "name": "C.foo", "file": "prototypes.js" },
198+
"kind": "calls",
199+
"mode": "receiver-typed",
200+
"notes": "v.foo() — v typed as C via new C(), C.foo defined via C.prototype = { foo: fn }"
201+
},
202+
{
203+
"source": { "name": "testPrototypeAlias", "file": "prototypes2.js" },
204+
"target": { "name": "A", "file": "prototypes2.js" },
205+
"kind": "calls",
206+
"mode": "constructor",
207+
"notes": "new A() — inline constructor call in new A().t()"
208+
},
209+
{
210+
"source": { "name": "testPrototypeAlias", "file": "prototypes2.js" },
211+
"target": { "name": "f", "file": "prototypes2.js" },
212+
"kind": "calls",
213+
"mode": "receiver-typed",
214+
"notes": "new A().t() — A.prototype.t = f alias; inline new receiver resolved to type A, typeMap['A.t'] = f"
166215
}
167216
]
168217
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Class hierarchy fixture — tests super.method() dispatch (class-inheritance resolution)
2+
3+
export class Animal {
4+
speak() {
5+
return 'generic sound';
6+
}
7+
}
8+
9+
export class Dog extends Animal {
10+
speak() {
11+
super.speak(); // super.method() → Animal.speak
12+
return 'woof';
13+
}
14+
}
15+
16+
export class Puppy extends Dog {
17+
speak() {
18+
super.speak(); // super.method() → Dog.speak (nearest parent, not Animal)
19+
return 'yip';
20+
}
21+
}
22+
23+
// Static super.method() — same resolution path as instance methods
24+
export class Counter {
25+
static count() {
26+
return 0;
27+
}
28+
}
29+
30+
export class DoubleCounter extends Counter {
31+
static count() {
32+
return super.count() * 2; // static super.method() → Counter.count
33+
}
34+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Pre-ES6 OOP via constructor function + prototype object literal.
2+
function C() {}
3+
4+
C.prototype = {
5+
foo: () => {},
6+
};
7+
8+
export function runPrototypes() {
9+
var v = new C();
10+
v.foo();
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Pre-ES6 OOP via prototype property assignment with identifier alias.
2+
const f = () => {};
3+
4+
class A {}
5+
6+
A.prototype.t = f;
7+
8+
export function testPrototypeAlias() {
9+
new A().t();
10+
}

tests/benchmarks/resolution/fixtures/typescript/expected-edges.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,20 @@
303303
"kind": "calls",
304304
"mode": "receiver-typed",
305305
"notes": "svc.removeUser() — svc typed as UserService via createService() return"
306+
},
307+
{
308+
"source": { "name": "TimestampLogger.log", "file": "super-dispatch.ts" },
309+
"target": { "name": "Logger.log", "file": "super-dispatch.ts" },
310+
"kind": "calls",
311+
"mode": "class-inheritance",
312+
"notes": "super.log() in TimestampLogger.log resolves to Logger.log via CHA parents map"
313+
},
314+
{
315+
"source": { "name": "PrefixLogger.log", "file": "super-dispatch.ts" },
316+
"target": { "name": "TimestampLogger.log", "file": "super-dispatch.ts" },
317+
"kind": "calls",
318+
"mode": "class-inheritance",
319+
"notes": "super.log() in PrefixLogger.log resolves to TimestampLogger.log (nearest parent)"
306320
}
307321
]
308322
}

0 commit comments

Comments
 (0)