Skip to content

Commit df3c251

Browse files
committed
fix: resolve merge conflicts with feat/prototype-resolver-1317
2 parents 69a9e89 + 194507c commit df3c251

12 files changed

Lines changed: 246 additions & 16 deletions

File tree

biome.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,13 @@
2525
"semicolons": "always",
2626
"trailingCommas": "all"
2727
}
28-
}
28+
},
29+
"overrides": [
30+
{
31+
"includes": ["tests/benchmarks/resolution/fixtures/**"],
32+
"linter": {
33+
"enabled": false
34+
}
35+
}
36+
]
2937
}

crates/codegraph-core/src/edge_builder.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,15 +483,27 @@ fn resolve_call_targets<'a>(
483483
if !class_scoped.is_empty() { return class_scoped; }
484484
}
485485

486-
// Broader fallback: same-file suffix scan to pick up CHA-expanded targets
487-
// (subclasses that override the method).
486+
// Broader fallback: same-file suffix scan. Always restrict to the caller's
487+
// own class prefix — regardless of how many matches are found — to avoid
488+
// false-positive edges to unrelated classes in the same file.
489+
// (e.g. this.area() inside Shape.describe must never yield Calculator.area,
490+
// even when Calculator.area is the only method with that name in the file.)
488491
let suffix = format!(".{}", call.name);
489492
if let Some(file_nodes) = ctx.nodes_by_file.get(rel_path) {
490493
let same_file_methods: Vec<&NodeInfo> = file_nodes.iter()
491494
.filter(|n| n.kind == "method" && n.name.ends_with(&suffix))
492495
.copied()
493496
.collect();
494-
if !same_file_methods.is_empty() { return same_file_methods; }
497+
if !same_file_methods.is_empty() {
498+
if let Some(dot_pos) = caller_name.find('.') {
499+
let caller_prefix = format!("{}.", &caller_name[..dot_pos]);
500+
let caller_scoped: Vec<&NodeInfo> = same_file_methods.iter()
501+
.filter(|n| n.name.starts_with(&caller_prefix))
502+
.copied()
503+
.collect();
504+
if !caller_scoped.is_empty() { return caller_scoped; }
505+
}
506+
}
495507
}
496508
}
497509
return exact; // empty

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,21 +1079,33 @@ function buildFileCallEdges(
10791079
// direct call to the same target in the same function body can upgrade confidence
10801080
// rather than being silently dropped by the dedup guard.
10811081
const scopedPtsKey = caller.callerName != null ? `${caller.callerName}::${call.name}` : null;
1082+
// Module-level calls (callerName === null) use the '<module>' sentinel emitted by
1083+
// extractSpreadForOfWalk for top-level for-of loops. Look it up as a fallback so
1084+
// that `for (const f of arr) { f(); }` at module scope resolves correctly.
1085+
const modulePtsKey =
1086+
caller.callerName === null && ptsMap?.has(`<module>::${call.name}`)
1087+
? `<module>::${call.name}`
1088+
: null;
10821089
const flatPtsKey =
10831090
!call.dynamic && fnRefBindingLhs.has(call.name) && ptsMap?.has(call.name) ? call.name : null;
10841091
if (
10851092
targets.length === 0 &&
10861093
!call.receiver &&
10871094
ptsMap &&
1088-
(call.dynamic || (scopedPtsKey != null && ptsMap.has(scopedPtsKey)) || flatPtsKey != null)
1095+
(call.dynamic ||
1096+
(scopedPtsKey != null && ptsMap.has(scopedPtsKey)) ||
1097+
modulePtsKey != null ||
1098+
flatPtsKey != null)
10891099
) {
10901100
const ptsLookupName = call.dynamic
10911101
? call.name
10921102
: scopedPtsKey != null && ptsMap.has(scopedPtsKey)
10931103
? scopedPtsKey
1094-
: // flatPtsKey != null is guaranteed by the outer if condition: if neither
1095-
// call.dynamic nor scopedPtsKey matched, flatPtsKey != null must be true.
1096-
flatPtsKey!;
1104+
: modulePtsKey != null
1105+
? modulePtsKey
1106+
: // flatPtsKey != null is guaranteed by the outer if condition: if neither
1107+
// call.dynamic nor scopedPtsKey nor modulePtsKey matched, flatPtsKey must be non-null.
1108+
flatPtsKey!;
10971109
for (const alias of resolveViaPointsTo(ptsLookupName, ptsMap)) {
10981110
// Resolve the concrete alias target. Only `name` is needed here — receiver
10991111
// and line are not relevant for alias resolution (we are looking up the

src/domain/wasm-worker-entry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,11 @@ function serializeExtractorOutput(
819819
? { objectPropBindings: symbols.objectPropBindings }
820820
: {}),
821821
...(symbols.newExpressions?.length ? { newExpressions: symbols.newExpressions } : {}),
822+
...(symbols.returnTypeMap?.size
823+
? { returnTypeMap: Array.from(symbols.returnTypeMap.entries()) }
824+
: {}),
825+
...(symbols.callAssignments?.length ? { callAssignments: symbols.callAssignments } : {}),
826+
...(symbols.paramBindings?.length ? { paramBindings: symbols.paramBindings } : {}),
822827
};
823828
}
824829

src/domain/wasm-worker-pool.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ function deserializeResult(ser: SerializedExtractorOutput | null): ExtractorOutp
116116
out.objectRestParamBindings = ser.objectRestParamBindings;
117117
if (ser.objectPropBindings?.length) out.objectPropBindings = ser.objectPropBindings;
118118
if (ser.newExpressions?.length) out.newExpressions = ser.newExpressions;
119+
if (ser.returnTypeMap?.length) {
120+
const returnTypeMap = new Map<string, TypeMapEntry>();
121+
for (const [k, v] of ser.returnTypeMap) returnTypeMap.set(k, v);
122+
out.returnTypeMap = returnTypeMap;
123+
}
124+
if (ser.callAssignments?.length) out.callAssignments = ser.callAssignments;
125+
if (ser.paramBindings?.length) out.paramBindings = ser.paramBindings;
119126
return out;
120127
}
121128

src/domain/wasm-worker-protocol.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212

1313
import type {
1414
Call,
15+
CallAssignment,
1516
ClassRelation,
1617
DataflowResult,
1718
Definition,
1819
Export,
1920
Import,
2021
LanguageId,
22+
ParamBinding,
2123
TypeMapEntry,
2224
} from '../types.js';
2325

@@ -71,6 +73,9 @@ export interface SerializedExtractorOutput {
7173
objectRestParamBindings?: import('../types.js').ObjectRestParamBinding[];
7274
objectPropBindings?: import('../types.js').ObjectPropBinding[];
7375
newExpressions?: readonly string[];
76+
returnTypeMap?: Array<[string, TypeMapEntry]>;
77+
callAssignments?: CallAssignment[];
78+
paramBindings?: ParamBinding[];
7479
}
7580

7681
export interface WorkerParseResponseOk {

src/extractors/javascript.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,12 +1871,30 @@ function extractSpreadForOfWalk(
18711871
fnRefBindings: FnRefBinding[],
18721872
): void {
18731873
const funcStack: string[] = [];
1874+
// Tracks the enclosing class name so that method_definition nodes push a
1875+
// qualified name (e.g. 'Foo.bar') matching what findCaller returns from the
1876+
// definitions array (where class methods are stored as 'Foo.bar').
1877+
const classStack: string[] = [];
18741878

18751879
function walk(node: TreeSitterNode, depth: number): void {
18761880
if (depth >= MAX_WALK_DEPTH) return;
18771881

18781882
let pushedFunc = false;
1879-
if (node.type === 'function_declaration' || node.type === 'generator_function_declaration') {
1883+
let pushedClass = false;
1884+
if (
1885+
node.type === 'class_declaration' ||
1886+
node.type === 'abstract_class_declaration' ||
1887+
node.type === 'class'
1888+
) {
1889+
const nameNode = node.childForFieldName('name');
1890+
if (nameNode?.type === 'identifier') {
1891+
classStack.push(nameNode.text);
1892+
pushedClass = true;
1893+
}
1894+
} else if (
1895+
node.type === 'function_declaration' ||
1896+
node.type === 'generator_function_declaration'
1897+
) {
18801898
const nameNode = node.childForFieldName('name');
18811899
if (nameNode?.type === 'identifier') {
18821900
funcStack.push(nameNode.text);
@@ -1885,7 +1903,11 @@ function extractSpreadForOfWalk(
18851903
} else if (node.type === 'method_definition') {
18861904
const nameNode = node.childForFieldName('name');
18871905
if (nameNode) {
1888-
funcStack.push(nameNode.text);
1906+
// Qualify with the enclosing class name so the PTS key matches
1907+
// callerName from findCaller (which uses def.name = 'ClassName.method').
1908+
const enclosingClass = classStack.length > 0 ? classStack[classStack.length - 1] : null;
1909+
const qualifiedName = enclosingClass ? `${enclosingClass}.${nameNode.text}` : nameNode.text;
1910+
funcStack.push(qualifiedName);
18891911
pushedFunc = true;
18901912
}
18911913
} else if (node.type === 'variable_declarator') {
@@ -2027,6 +2049,7 @@ function extractSpreadForOfWalk(
20272049
}
20282050

20292051
if (pushedFunc) funcStack.pop();
2052+
if (pushedClass) classStack.pop();
20302053
}
20312054

20322055
walk(rootNode, 0);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,35 +130,35 @@
130130
"notes": "new UserService() — class instantiation tracked as consumption"
131131
},
132132
{
133-
"source": { "name": "defProp", "file": "define-property.js" },
133+
"source": { "name": "_defProp", "file": "define-property.js" },
134134
"target": { "name": "f1", "file": "define-property.js" },
135135
"kind": "calls",
136136
"mode": "pts-define-property",
137137
"notes": "obj.f() — resolved via Object.defineProperty(obj, \"f\", { value: f1 })"
138138
},
139139
{
140-
"source": { "name": "defProps", "file": "define-property.js" },
140+
"source": { "name": "_defProps", "file": "define-property.js" },
141141
"target": { "name": "f1", "file": "define-property.js" },
142142
"kind": "calls",
143143
"mode": "pts-define-property",
144144
"notes": "obj.f1() — resolved via Object.defineProperties(obj, { \"f1\": { value: f1 } })"
145145
},
146146
{
147-
"source": { "name": "defProps", "file": "define-property.js" },
147+
"source": { "name": "_defProps", "file": "define-property.js" },
148148
"target": { "name": "f2", "file": "define-property.js" },
149149
"kind": "calls",
150150
"mode": "pts-define-property",
151151
"notes": "obj.f2() — resolved via Object.defineProperties(obj, { \"f2\": { value: f2 } })"
152152
},
153153
{
154-
"source": { "name": "create", "file": "define-property.js" },
154+
"source": { "name": "_create", "file": "define-property.js" },
155155
"target": { "name": "f1", "file": "define-property.js" },
156156
"kind": "calls",
157157
"mode": "pts-create-prototype",
158158
"notes": "obj.f1() — resolved via Object.create({ f1, f2 })"
159159
},
160160
{
161-
"source": { "name": "create", "file": "define-property.js" },
161+
"source": { "name": "_create", "file": "define-property.js" },
162162
"target": { "name": "f2", "file": "define-property.js" },
163163
"kind": "calls",
164164
"mode": "pts-create-prototype",

tests/benchmarks/resolution/fixtures/javascript/inheritance.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ export class Counter {
2929

3030
export class DoubleCounter extends Counter {
3131
static count() {
32-
return Counter.count() * 2; // static super.method() → Counter.count
32+
return super.count() * 2; // static super.count() → Counter.count via CHA parents map
3333
}
3434
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Three unrelated classes in one file, each with an area() method.
2+
// this.area() inside Shape.describe must resolve only to Shape.area,
3+
// not to Calculator.area or Formatter.area.
4+
5+
export class Shape {
6+
describe(): string {
7+
return `area=${this.area()}`;
8+
}
9+
area(): number {
10+
return 0;
11+
}
12+
}
13+
14+
export class Calculator {
15+
area(): number {
16+
return 100;
17+
}
18+
}
19+
20+
export class Formatter {
21+
area(): string {
22+
return 'n/a';
23+
}
24+
}

0 commit comments

Comments
 (0)