Skip to content

Commit c199368

Browse files
authored
fix(parity): resolve C# static receiver calls in WASM engine (#1372) (#1395)
* fix(parity): resolve C# static receiver calls in WASM engine (#1372) The WASM resolver had no fallback for calls where the receiver is a class name used directly (e.g. `Validators.IsValidEmail(...)`). Static class names never appear in variable declarations, so typeMap has no entry for them. The old code fell through and returned an empty target set. Add a fallback in resolveByMethodOrGlobal: when typeName is null (no typeMap entry for the receiver), try looking up `${receiver}.${method}` directly in the symbol DB. This catches static dispatch where the receiver IS the class name, matching what the native engine already produces. The kind='method' filter keeps it scoped to genuine method calls. Fixes build-parity test failures for the csharp Repository fixture: - Program.Main → Validators.IsValidEmail - Program.RunWithValidation → Validators.ValidateUser - UserService.AddUser → Validators.ValidateUser * fix(resolver): use effectiveReceiver in static dispatch fallback (#1390) * feat(resolver): resolve iteration-callback edges for for-of, Set, and Array.from (JS) Fixes more1 jelly-micro benchmark from 0% → 100% recall (10/10 named edges). The PTS resolution for for-of, Set+for-of, Array.from, and spread patterns was already fully implemented. The 0% recall was caused by a naming mismatch in the fixture: _iterPlain/_iterSet (underscore-prefixed) vs the expected-edges.json which referenced iterPlain/iterSet (without prefix). Renaming the fixture functions to match the expected edges exposes all four PTS patterns resolving correctly. Closes #1378 * Revert "feat(resolver): resolve iteration-callback edges for for-of, Set, and Array.from (JS)" This reverts commit 70236a5. * test(1372): initialise edges to [] for clearer assertion failures * test(integration): pin prototype-method-resolution test to WASM engine The test was using auto engine (native-preferred), causing it to pick the published npm native binary which predates the prototype-method fixes. WASM correctly extracts Dog.prototype.bark and resolves all call edges. Fixes #1381 * fix: remove duplicate static-receiver fallback superseded by main's broader version
1 parent 0fe4bfc commit c199368

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Regression test for #1372 — WASM engine missing C# static receiver call edges.
3+
*
4+
* The gap: the WASM resolver had no fallback for calls where the receiver is a
5+
* class name used directly (e.g. `Validators.IsValidEmail(...)`). The class name
6+
* is never assigned to a local variable, so it has no typeMap entry, and the old
7+
* code returned empty. The fix adds a fallback that treats the receiver as a
8+
* potential class name and looks up `Receiver.Method` directly in the symbol DB.
9+
*/
10+
11+
import fs from 'node:fs';
12+
import os from 'node:os';
13+
import path from 'node:path';
14+
import Database from 'better-sqlite3';
15+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
16+
import { buildGraph } from '../../src/domain/graph/builder.js';
17+
18+
// ── Fixture ──────────────────────────────────────────────────────────────────
19+
20+
const FILES: Record<string, string> = {
21+
// Static class with two public methods.
22+
'Validators.cs': `
23+
namespace Demo;
24+
public static class Validators {
25+
public static bool IsValidEmail(string email) {
26+
return email.Contains("@");
27+
}
28+
public static bool IsValidName(string name) {
29+
return name.Length >= 2;
30+
}
31+
}
32+
`,
33+
// Caller class that references Validators via explicit static receiver.
34+
'Program.cs': `
35+
namespace Demo;
36+
public class Program {
37+
public static void Main() {
38+
bool ok = Validators.IsValidEmail("a@b.com");
39+
}
40+
public static void Run() {
41+
bool ok2 = Validators.IsValidName("Alice");
42+
}
43+
}
44+
`,
45+
};
46+
47+
// ── Helpers ───────────────────────────────────────────────────────────────────
48+
49+
function writeFixture(dir: string) {
50+
for (const [rel, content] of Object.entries(FILES)) {
51+
fs.writeFileSync(path.join(dir, rel), content.trimStart());
52+
}
53+
}
54+
55+
function readEdges(dbPath: string) {
56+
const db = new Database(dbPath, { readonly: true });
57+
try {
58+
return db
59+
.prepare(
60+
`SELECT n1.name AS src, n2.name AS tgt, e.kind
61+
FROM edges e
62+
JOIN nodes n1 ON e.source_id = n1.id
63+
JOIN nodes n2 ON e.target_id = n2.id
64+
WHERE e.kind = 'calls'
65+
ORDER BY n1.name, n2.name`,
66+
)
67+
.all() as Array<{ src: string; tgt: string; kind: string }>;
68+
} finally {
69+
db.close();
70+
}
71+
}
72+
73+
// ── Tests ─────────────────────────────────────────────────────────────────────
74+
75+
describe('C# static receiver call resolution (#1372)', () => {
76+
let tmpDir: string;
77+
let edges: Array<{ src: string; tgt: string; kind: string }> = [];
78+
79+
beforeAll(async () => {
80+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1372-'));
81+
writeFixture(tmpDir);
82+
await buildGraph(tmpDir, { engine: 'wasm', incremental: false, skipRegistry: true });
83+
edges = readEdges(path.join(tmpDir, '.codegraph', 'graph.db'));
84+
}, 60_000);
85+
86+
afterAll(() => {
87+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
88+
});
89+
90+
it('resolves static receiver call: Program.Main → Validators.IsValidEmail', () => {
91+
expect(edges).toContainEqual(
92+
expect.objectContaining({
93+
src: 'Program.Main',
94+
tgt: 'Validators.IsValidEmail',
95+
kind: 'calls',
96+
}),
97+
);
98+
});
99+
100+
it('resolves static receiver call: Program.Run → Validators.IsValidName', () => {
101+
expect(edges).toContainEqual(
102+
expect.objectContaining({ src: 'Program.Run', tgt: 'Validators.IsValidName', kind: 'calls' }),
103+
);
104+
});
105+
});

tests/integration/prototype-method-resolution.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ beforeAll(async () => {
4848
for (const [rel, content] of Object.entries(FIXTURE)) {
4949
fs.writeFileSync(path.join(tmpDir, rel), content);
5050
}
51-
await buildGraph(tmpDir, { incremental: false, skipRegistry: true });
51+
await buildGraph(tmpDir, { incremental: false, skipRegistry: true, engine: 'wasm' });
5252
});
5353

5454
afterAll(() => {

0 commit comments

Comments
 (0)