Skip to content

Commit 84f6c7c

Browse files
committed
test(1458): add Rust multi-class field collision unit test and end-to-end integration test
- Rust: field_annotation_multi_class_seeds_separate_scoped_keys confirms that two classes with identically-named fields produce separate class-scoped typeMap keys at confidence 0.9 (mirrors the TS prevents-cross-class-collision test). - Integration: issue-1458-cross-class-field-typemap.test.ts exercises the full buildGraph → resolver path (WASM engine) and asserts that OrderService.run resolves to OrderRepository.save and UserService.run to UserRepository.save, with no cross-class false edges.
1 parent 06a911e commit 84f6c7c

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

crates/codegraph-core/src/extractors/javascript.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3598,6 +3598,38 @@ mod tests {
35983598
assert_eq!(tm.unwrap().type_name, "HttpClient");
35993599
}
36003600

3601+
/// Issue #1458: two classes with identically-named field annotations must
3602+
/// produce separate class-scoped typeMap keys, not overwrite each other.
3603+
/// Mirrors the TS `prevents cross-class collision` test.
3604+
#[test]
3605+
fn field_annotation_multi_class_seeds_separate_scoped_keys() {
3606+
let s = parse_js(
3607+
"class OrderService {\n\
3608+
private repo: OrderRepository;\n\
3609+
}\n\
3610+
class UserService {\n\
3611+
private repo: UserRepository;\n\
3612+
}",
3613+
);
3614+
let order_entry = s.type_map.iter().find(|t| t.name == "OrderService.repo");
3615+
assert!(
3616+
order_entry.is_some(),
3617+
"type_map should contain 'OrderService.repo'; got: {:?}",
3618+
s.type_map.iter().map(|e| &e.name).collect::<Vec<_>>()
3619+
);
3620+
assert_eq!(order_entry.unwrap().type_name, "OrderRepository");
3621+
assert_eq!(order_entry.unwrap().confidence, 0.9);
3622+
3623+
let user_entry = s.type_map.iter().find(|t| t.name == "UserService.repo");
3624+
assert!(
3625+
user_entry.is_some(),
3626+
"type_map should contain 'UserService.repo'; got: {:?}",
3627+
s.type_map.iter().map(|e| &e.name).collect::<Vec<_>>()
3628+
);
3629+
assert_eq!(user_entry.unwrap().type_name, "UserRepository");
3630+
assert_eq!(user_entry.unwrap().confidence, 0.9);
3631+
}
3632+
36013633
/// Issue #1453 (edge 4): `const f = fn.bind(ctx)` must record a
36023634
/// fnRefBinding f → fn so later `f()` calls resolve through pts.
36033635
#[test]
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Integration test for #1458: cross-class field annotation typeMap collision.
3+
*
4+
* Two classes with identically-named fields (`repo`) caused the later class's
5+
* annotation to overwrite the earlier one's bare typeMap key. `this.repo.save()`
6+
* inside UserService would resolve to OrderRepository instead of UserRepository.
7+
*
8+
* Fix: handleFieldDefTypeMap seeds `ClassName.field` at confidence 0.9 as the
9+
* primary key; the resolver checks the class-scoped key before bare fallback keys
10+
* for `this.` receivers so the correct type is always chosen.
11+
*/
12+
13+
import fs from 'node:fs';
14+
import os from 'node:os';
15+
import path from 'node:path';
16+
import Database from 'better-sqlite3';
17+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
18+
import { buildGraph } from '../../src/domain/graph/builder.js';
19+
20+
const FIXTURE = {
21+
'services.ts': `
22+
class OrderRepository {
23+
save(order: unknown) {}
24+
}
25+
class UserRepository {
26+
save(user: unknown) {}
27+
}
28+
class OrderService {
29+
private repo: OrderRepository;
30+
run() { this.repo.save({}); }
31+
}
32+
class UserService {
33+
private repo: UserRepository;
34+
run() { this.repo.save({}); }
35+
}
36+
`,
37+
};
38+
39+
let tmpDir: string;
40+
41+
beforeAll(async () => {
42+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1458-'));
43+
for (const [rel, content] of Object.entries(FIXTURE)) {
44+
fs.writeFileSync(path.join(tmpDir, rel), content);
45+
}
46+
await buildGraph(tmpDir, { engine: 'wasm', incremental: false, skipRegistry: true });
47+
});
48+
49+
afterAll(() => {
50+
fs.rmSync(tmpDir, { recursive: true, force: true });
51+
});
52+
53+
function readCallEdges(dbPath: string) {
54+
const db = new Database(dbPath, { readonly: true });
55+
try {
56+
return db
57+
.prepare(
58+
`SELECT n1.name AS src, n2.name AS tgt
59+
FROM edges e
60+
JOIN nodes n1 ON e.source_id = n1.id
61+
JOIN nodes n2 ON e.target_id = n2.id
62+
WHERE e.kind = 'calls'
63+
ORDER BY n1.name, n2.name`,
64+
)
65+
.all() as Array<{ src: string; tgt: string }>;
66+
} finally {
67+
db.close();
68+
}
69+
}
70+
71+
describe('cross-class field annotation typeMap collision (#1458)', () => {
72+
it('resolves this.repo.save() inside OrderService.run to OrderRepository.save', () => {
73+
const dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
74+
const edges = readCallEdges(dbPath);
75+
const edge = edges.find(
76+
(e) => e.src === 'OrderService.run' && e.tgt === 'OrderRepository.save',
77+
);
78+
expect(
79+
edge,
80+
'OrderService.run → OrderRepository.save edge missing; cross-class collision may be present',
81+
).toBeDefined();
82+
});
83+
84+
it('resolves this.repo.save() inside UserService.run to UserRepository.save', () => {
85+
const dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
86+
const edges = readCallEdges(dbPath);
87+
const edge = edges.find((e) => e.src === 'UserService.run' && e.tgt === 'UserRepository.save');
88+
expect(
89+
edge,
90+
'UserService.run → UserRepository.save edge missing; cross-class collision may be present',
91+
).toBeDefined();
92+
});
93+
94+
it('does not emit a false edge from OrderService.run to UserRepository.save', () => {
95+
const dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
96+
const edges = readCallEdges(dbPath);
97+
const falseEdge = edges.find(
98+
(e) => e.src === 'OrderService.run' && e.tgt === 'UserRepository.save',
99+
);
100+
expect(falseEdge).toBeUndefined();
101+
});
102+
103+
it('does not emit a false edge from UserService.run to OrderRepository.save', () => {
104+
const dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
105+
const edges = readCallEdges(dbPath);
106+
const falseEdge = edges.find(
107+
(e) => e.src === 'UserService.run' && e.tgt === 'OrderRepository.save',
108+
);
109+
expect(falseEdge).toBeUndefined();
110+
});
111+
});

0 commit comments

Comments
 (0)