Skip to content

Commit 058f0f4

Browse files
authored
Merge branch 'main' into feat/prototype-resolver-1317
2 parents 667866e + 6031440 commit 058f0f4

7 files changed

Lines changed: 190 additions & 3 deletions

File tree

crates/codegraph-core/src/edge_builder.rs

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

463-
// Broader fallback: same-file suffix scan to pick up CHA-expanded targets
464-
// (subclasses that override the method).
463+
// Broader fallback: same-file suffix scan. Always restrict to the caller's
464+
// own class prefix — regardless of how many matches are found — to avoid
465+
// false-positive edges to unrelated classes in the same file.
466+
// (e.g. this.area() inside Shape.describe must never yield Calculator.area,
467+
// even when Calculator.area is the only method with that name in the file.)
465468
let suffix = format!(".{}", call.name);
466469
if let Some(file_nodes) = ctx.nodes_by_file.get(rel_path) {
467470
let same_file_methods: Vec<&NodeInfo> = file_nodes.iter()
468471
.filter(|n| n.kind == "method" && n.name.ends_with(&suffix))
469472
.copied()
470473
.collect();
471-
if !same_file_methods.is_empty() { return same_file_methods; }
474+
if !same_file_methods.is_empty() {
475+
if let Some(dot_pos) = caller_name.find('.') {
476+
let caller_prefix = format!("{}.", &caller_name[..dot_pos]);
477+
let caller_scoped: Vec<&NodeInfo> = same_file_methods.iter()
478+
.filter(|n| n.name.starts_with(&caller_prefix))
479+
.copied()
480+
.collect();
481+
if !caller_scoped.is_empty() { return caller_scoped; }
482+
}
483+
}
472484
}
473485
}
474486
return exact; // empty

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 {
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+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Two classes in one file; only one defines area().
2+
// this.area() inside Caller.run must NOT resolve to Sibling.area
3+
// even when Sibling.area is the only method with that suffix in the file.
4+
// The caller's own class (Caller) has no area() → the edge must be omitted.
5+
6+
export class Caller {
7+
run(): string {
8+
return `result=${this.area()}`;
9+
}
10+
}
11+
12+
export class Sibling {
13+
area(): number {
14+
return 42;
15+
}
16+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* this-dispatch scope: same-file fallback must not emit false-positive edges
3+
* to methods in unrelated classes.
4+
*
5+
* Fixtures:
6+
* - shapes.ts — three unrelated classes (Shape, Calculator, Formatter) all
7+
* defining area(). this.area() inside Shape.describe must resolve only to
8+
* Shape.area (multi-match disambiguation path).
9+
* - single-sibling.ts — two classes: Caller (no area()) and Sibling (area()).
10+
* this.area() inside Caller.run must NOT resolve to Sibling.area even though
11+
* it is the only method with that suffix in the file (single-match path).
12+
*
13+
* Covers the Rust edge_builder fix in issue #1324.
14+
*/
15+
16+
import fs from 'node:fs';
17+
import os from 'node:os';
18+
import path from 'node:path';
19+
import Database from 'better-sqlite3';
20+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
21+
import { buildGraph } from '../../src/domain/graph/builder.js';
22+
import type { EngineMode } from '../../src/types.js';
23+
24+
const FIXTURE_DIR = path.join(import.meta.dirname, '..', 'fixtures', 'this-dispatch-scope');
25+
26+
interface CallEdgeRow {
27+
caller_name: string;
28+
callee_name: string;
29+
}
30+
31+
function readCallEdges(dbPath: string): CallEdgeRow[] {
32+
const db = new Database(dbPath, { readonly: true });
33+
try {
34+
return db
35+
.prepare(
36+
`SELECT n1.name AS caller_name, n2.name AS callee_name
37+
FROM edges e
38+
JOIN nodes n1 ON e.source_id = n1.id
39+
JOIN nodes n2 ON e.target_id = n2.id
40+
WHERE e.kind = 'calls'`,
41+
)
42+
.all() as CallEdgeRow[];
43+
} finally {
44+
db.close();
45+
}
46+
}
47+
48+
const ENGINES: EngineMode[] = ['wasm', 'native'];
49+
50+
describe.each(ENGINES)('this-dispatch scope (%s)', (engine) => {
51+
let tmpDir: string;
52+
let callEdges: CallEdgeRow[] = [];
53+
54+
beforeAll(async () => {
55+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `codegraph-this-scope-${engine}-`));
56+
fs.cpSync(FIXTURE_DIR, tmpDir, { recursive: true });
57+
await buildGraph(tmpDir, { incremental: false, skipRegistry: true, engine });
58+
callEdges = readCallEdges(path.join(tmpDir, '.codegraph', 'graph.db'));
59+
}, 60_000);
60+
61+
afterAll(() => {
62+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
63+
});
64+
65+
it('emits Shape.describe → Shape.area (correct this-dispatch)', () => {
66+
const edge = callEdges.find(
67+
(e) => e.caller_name === 'Shape.describe' && e.callee_name === 'Shape.area',
68+
);
69+
expect(
70+
edge,
71+
`Expected Shape.describe → Shape.area edge.\nActual edges:\n${JSON.stringify(callEdges, null, 2)}`,
72+
).toBeDefined();
73+
});
74+
75+
// Native binary v3.11.2 does not include the edge_builder.rs fix for issue #1324 yet.
76+
// These assertions are active for WASM and will be re-enabled for native once a new
77+
// binary is published that includes the Rust fix.
78+
if (engine === 'native') {
79+
it.todo('does NOT emit Shape.describe → Calculator.area (native binary gap #1324)');
80+
it.todo('does NOT emit Shape.describe → Formatter.area (native binary gap #1324)');
81+
it.todo(
82+
'does NOT emit Caller.run → Sibling.area (single-match false-positive, native binary gap #1324)',
83+
);
84+
} else {
85+
it('does NOT emit Shape.describe → Calculator.area (unrelated class, same method name)', () => {
86+
const edge = callEdges.find(
87+
(e) => e.caller_name === 'Shape.describe' && e.callee_name === 'Calculator.area',
88+
);
89+
expect(
90+
edge,
91+
`Expected NO Shape.describe → Calculator.area edge (false-positive from same-file scan).\nActual edges:\n${JSON.stringify(callEdges, null, 2)}`,
92+
).toBeUndefined();
93+
});
94+
95+
it('does NOT emit Shape.describe → Formatter.area (unrelated class, same method name)', () => {
96+
const edge = callEdges.find(
97+
(e) => e.caller_name === 'Shape.describe' && e.callee_name === 'Formatter.area',
98+
);
99+
expect(
100+
edge,
101+
`Expected NO Shape.describe → Formatter.area edge (false-positive from same-file scan).\nActual edges:\n${JSON.stringify(callEdges, null, 2)}`,
102+
).toBeUndefined();
103+
});
104+
105+
// single-sibling.ts: only one class (Sibling) has area(); Caller does not.
106+
// The single-match arm must still check the caller's own class — Caller.run
107+
// must not gain a false edge to Sibling.area.
108+
it('does NOT emit Caller.run → Sibling.area (single-match false-positive, same-file scan)', () => {
109+
const edge = callEdges.find(
110+
(e) => e.caller_name === 'Caller.run' && e.callee_name === 'Sibling.area',
111+
);
112+
expect(
113+
edge,
114+
`Expected NO Caller.run → Sibling.area edge (false-positive from single-match suffix scan).\nActual edges:\n${JSON.stringify(callEdges, null, 2)}`,
115+
).toBeUndefined();
116+
});
117+
}
118+
});

0 commit comments

Comments
 (0)