Skip to content

Commit 0ae2d57

Browse files
committed
fix: resolve merge conflicts with main
2 parents 58937cf + 16a1182 commit 0ae2d57

13 files changed

Lines changed: 412 additions & 66 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: 161 additions & 7 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

@@ -476,7 +479,7 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
476479
if (declarator?.type !== 'variable_declarator') continue;
477480
const nameN = declarator.childForFieldName('name');
478481
const valueN = declarator.childForFieldName('value');
479-
if (!nameN || nameN.type !== 'identifier' || !valueN) continue;
482+
if (nameN?.type !== 'identifier' || !valueN) continue;
480483
// Skip functions — already captured by query patterns
481484
const valType = valueN.type;
482485
if (valType === 'arrow_function' || valType === 'function_expression' || valType === 'function')
@@ -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
@@ -1162,7 +1167,7 @@ function extractSimpleTypeName(typeAnnotationNode: TreeSitterNode): string | nul
11621167
}
11631168

11641169
function extractNewExprTypeName(newExprNode: TreeSitterNode): string | null {
1165-
if (!newExprNode || newExprNode.type !== 'new_expression') return null;
1170+
if (newExprNode?.type !== 'new_expression') return null;
11661171
const ctor = newExprNode.childForFieldName('constructor') || newExprNode.child(1);
11671172
if (!ctor) return null;
11681173
if (ctor.type === 'identifier') return ctor.text;
@@ -1275,7 +1280,7 @@ function storeReturnType(
12751280
function findReturnNewExprType(bodyNode: TreeSitterNode): string | null {
12761281
for (let i = 0; i < bodyNode.childCount; i++) {
12771282
const child = bodyNode.child(i);
1278-
if (!child || child.type !== 'return_statement') continue;
1283+
if (child?.type !== 'return_statement') continue;
12791284
for (let j = 0; j < child.childCount; j++) {
12801285
const expr = child.child(j);
12811286
if (expr?.type === 'new_expression') return extractNewExprTypeName(expr);
@@ -1442,7 +1447,7 @@ function handleVarDeclaratorTypeMap(
14421447
fnRefBindings?: FnRefBinding[],
14431448
): void {
14441449
const nameN = node.childForFieldName('name');
1445-
if (!nameN || nameN.type !== 'identifier') return;
1450+
if (nameN?.type !== 'identifier') return;
14461451

14471452
const typeAnno = findChild(node, 'type_annotation');
14481453
const valueN = node.childForFieldName('value');
@@ -1545,7 +1550,7 @@ function handleVarDeclaratorTypeMap(
15451550
function handleParamTypeMap(node: TreeSitterNode, typeMap: Map<string, TypeMapEntry>): void {
15461551
const nameNode =
15471552
node.childForFieldName('pattern') || node.childForFieldName('left') || node.child(0);
1548-
if (!nameNode || nameNode.type !== 'identifier') return;
1553+
if (nameNode?.type !== 'identifier') return;
15491554
const typeAnno = findChild(node, 'type_annotation');
15501555
if (typeAnno) {
15511556
const typeName = extractSimpleTypeName(typeAnno);
@@ -1938,7 +1943,7 @@ function extractCallbackDefinition(
19381943
fn?: TreeSitterNode | null,
19391944
): Definition | null {
19401945
if (!fn) fn = callNode.childForFieldName('function');
1941-
if (!fn || fn.type !== 'member_expression') return null;
1946+
if (fn?.type !== 'member_expression') return null;
19421947

19431948
const prop = fn.childForFieldName('property');
19441949
if (!prop) return null;
@@ -2049,7 +2054,7 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {
20492054
// Skip await_expression wrapper if present
20502055
if (current && current.type === 'await_expression') current = current.parent;
20512056
// We should now be at a variable_declarator (or not, if standalone import())
2052-
if (!current || current.type !== 'variable_declarator') return [];
2057+
if (current?.type !== 'variable_declarator') return [];
20532058

20542059
const nameNode = current.childForFieldName('name');
20552060
if (!nameNode) return [];
@@ -2092,3 +2097,152 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {
20922097

20932098
return [];
20942099
}
2100+
2101+
// ── Phase 8.X: Prototype-based method extraction ────────────────────────────
2102+
2103+
/**
2104+
* Walk the AST and extract prototype-based method definitions and aliases.
2105+
*
2106+
* Handles three patterns:
2107+
* 1. `Foo.prototype.bar = function(){...}` — emits Foo.bar as method definition
2108+
* 2. `Foo.prototype.bar = identifier` — sets typeMap['Foo.bar'] = { type: identifier }
2109+
* 3. `Foo.prototype = { bar: fn, ... }` — emits defs and typeMap entries per property
2110+
*
2111+
* Emitting definitions under the canonical `ClassName.methodName` name lets the
2112+
* existing typeMap-based call resolver find them when a typed receiver dispatches
2113+
* `instance.method()` (lookup.byName('C.foo') in resolveByMethodOrGlobal).
2114+
*
2115+
* typeMap entries for identifier aliases (`Foo.bar → { type: 'someId' }`) are
2116+
* consumed by the prototype-alias fallback added to resolveByMethodOrGlobal.
2117+
*/
2118+
function extractPrototypeMethodsWalk(
2119+
rootNode: TreeSitterNode,
2120+
definitions: Definition[],
2121+
typeMap: Map<string, TypeMapEntry>,
2122+
): void {
2123+
function walk(node: TreeSitterNode, depth: number): void {
2124+
if (depth >= MAX_WALK_DEPTH) return;
2125+
if (node.type === 'expression_statement') {
2126+
const expr = node.child(0);
2127+
if (expr?.type === 'assignment_expression') {
2128+
const lhs = expr.childForFieldName('left');
2129+
const rhs = expr.childForFieldName('right');
2130+
if (lhs && rhs) handlePrototypeAssignment(lhs, rhs, definitions, typeMap);
2131+
}
2132+
}
2133+
for (let i = 0; i < node.childCount; i++) {
2134+
walk(node.child(i)!, depth + 1);
2135+
}
2136+
}
2137+
walk(rootNode, 0);
2138+
}
2139+
2140+
/**
2141+
* Handle an assignment_expression that may be a prototype assignment.
2142+
*
2143+
* Matches:
2144+
* - `Foo.prototype.bar = rhs` (lhs ends in .prototype.bar)
2145+
* - `Foo.prototype = { ... }` (lhs ends in .prototype, rhs is object literal)
2146+
*/
2147+
function handlePrototypeAssignment(
2148+
lhs: TreeSitterNode,
2149+
rhs: TreeSitterNode,
2150+
definitions: Definition[],
2151+
typeMap: Map<string, TypeMapEntry>,
2152+
): void {
2153+
if (lhs.type !== 'member_expression') return;
2154+
2155+
const lhsObj = lhs.childForFieldName('object');
2156+
const lhsProp = lhs.childForFieldName('property');
2157+
if (!lhsObj || !lhsProp) return;
2158+
2159+
// Pattern 1: `Foo.prototype.bar = rhs`
2160+
// lhs.object is `Foo.prototype` (member_expression), lhs.property is `bar`
2161+
if (
2162+
lhsObj.type === 'member_expression' &&
2163+
(lhsProp.type === 'property_identifier' || lhsProp.type === 'identifier')
2164+
) {
2165+
const protoObj = lhsObj.childForFieldName('object');
2166+
const protoProp = lhsObj.childForFieldName('property');
2167+
if (
2168+
protoObj?.type === 'identifier' &&
2169+
protoProp?.text === 'prototype' &&
2170+
!BUILTIN_GLOBALS.has(protoObj.text)
2171+
) {
2172+
emitPrototypeMethod(protoObj.text, lhsProp.text, rhs, definitions, typeMap);
2173+
}
2174+
return;
2175+
}
2176+
2177+
// Pattern 2: `Foo.prototype = { bar: fn, ... }`
2178+
// lhs.object is `Foo` (identifier), lhs.property is `prototype`
2179+
if (
2180+
lhsObj.type === 'identifier' &&
2181+
lhsProp.text === 'prototype' &&
2182+
!BUILTIN_GLOBALS.has(lhsObj.text) &&
2183+
rhs.type === 'object'
2184+
) {
2185+
extractPrototypeObjectLiteral(lhsObj.text, rhs, definitions, typeMap);
2186+
}
2187+
}
2188+
2189+
/** Emit one prototype method definition or typeMap alias for `ClassName.methodName = rhs`. */
2190+
function emitPrototypeMethod(
2191+
className: string,
2192+
methodName: string,
2193+
rhs: TreeSitterNode,
2194+
definitions: Definition[],
2195+
typeMap: Map<string, TypeMapEntry>,
2196+
): void {
2197+
const fullName = `${className}.${methodName}`;
2198+
if (rhs.type === 'function_expression' || rhs.type === 'arrow_function') {
2199+
definitions.push({
2200+
name: fullName,
2201+
kind: 'method',
2202+
line: nodeStartLine(rhs),
2203+
endLine: nodeEndLine(rhs),
2204+
});
2205+
} else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) {
2206+
// Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' }
2207+
// Consumed by the prototype-alias fallback in resolveByMethodOrGlobal.
2208+
setTypeMapEntry(typeMap, fullName, rhs.text, 0.9);
2209+
}
2210+
}
2211+
2212+
/** Iterate over an object literal assigned to `Foo.prototype` and emit defs/aliases. */
2213+
function extractPrototypeObjectLiteral(
2214+
className: string,
2215+
objNode: TreeSitterNode,
2216+
definitions: Definition[],
2217+
typeMap: Map<string, TypeMapEntry>,
2218+
): void {
2219+
for (let i = 0; i < objNode.childCount; i++) {
2220+
const child = objNode.child(i);
2221+
if (!child) continue;
2222+
2223+
if (child.type === 'method_definition') {
2224+
// Shorthand method: `Foo.prototype = { bar() {} }`
2225+
const nameNode = child.childForFieldName('name');
2226+
if (nameNode) {
2227+
definitions.push({
2228+
name: `${className}.${nameNode.text}`,
2229+
kind: 'method',
2230+
line: nodeStartLine(child),
2231+
endLine: nodeEndLine(child),
2232+
});
2233+
}
2234+
continue;
2235+
}
2236+
2237+
if (child.type !== 'pair') continue;
2238+
2239+
const keyNode = child.childForFieldName('key');
2240+
const valueNode = child.childForFieldName('value');
2241+
if (!keyNode || !valueNode) continue;
2242+
2243+
const methodName = keyNode.type === 'string' ? keyNode.text.replace(/['"]/g, '') : keyNode.text;
2244+
if (!methodName) continue;
2245+
2246+
emitPrototypeMethod(className, methodName, valueNode, definitions, typeMap);
2247+
}
2248+
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,55 @@
149149
"kind": "calls",
150150
"mode": "dynamic",
151151
"notes": "greet.apply(user, ['Hey']) — .apply() extracts greet as the callee"
152+
},
153+
{
154+
"source": { "name": "Dog.speak", "file": "inheritance.js" },
155+
"target": { "name": "Animal.speak", "file": "inheritance.js" },
156+
"kind": "calls",
157+
"mode": "class-inheritance",
158+
"notes": "super.speak() in Dog.speak resolves to Animal.speak via CHA parents map"
159+
},
160+
{
161+
"source": { "name": "Puppy.speak", "file": "inheritance.js" },
162+
"target": { "name": "Dog.speak", "file": "inheritance.js" },
163+
"kind": "calls",
164+
"mode": "class-inheritance",
165+
"notes": "super.speak() in Puppy.speak resolves to Dog.speak (nearest parent), not Animal.speak"
166+
},
167+
{
168+
"source": { "name": "DoubleCounter.count", "file": "inheritance.js" },
169+
"target": { "name": "Counter.count", "file": "inheritance.js" },
170+
"kind": "calls",
171+
"mode": "class-inheritance",
172+
"notes": "static super.count() in DoubleCounter.count resolves to Counter.count via CHA parents map"
173+
},
174+
{
175+
"source": { "name": "runPrototypes", "file": "prototypes.js" },
176+
"target": { "name": "C", "file": "prototypes.js" },
177+
"kind": "calls",
178+
"mode": "constructor",
179+
"notes": "new C() — constructor call to function-based constructor"
180+
},
181+
{
182+
"source": { "name": "runPrototypes", "file": "prototypes.js" },
183+
"target": { "name": "C.foo", "file": "prototypes.js" },
184+
"kind": "calls",
185+
"mode": "receiver-typed",
186+
"notes": "v.foo() — v typed as C via new C(), C.foo defined via C.prototype = { foo: fn }"
187+
},
188+
{
189+
"source": { "name": "testPrototypeAlias", "file": "prototypes2.js" },
190+
"target": { "name": "A", "file": "prototypes2.js" },
191+
"kind": "calls",
192+
"mode": "constructor",
193+
"notes": "new A() — inline constructor call in new A().t()"
194+
},
195+
{
196+
"source": { "name": "testPrototypeAlias", "file": "prototypes2.js" },
197+
"target": { "name": "f", "file": "prototypes2.js" },
198+
"kind": "calls",
199+
"mode": "receiver-typed",
200+
"notes": "new A().t() — A.prototype.t = f alias; inline new receiver resolved to type A, typeMap['A.t'] = f"
152201
}
153202
]
154203
}

0 commit comments

Comments
 (0)