Skip to content

Commit 04d1e5c

Browse files
authored
test(native): verify UNION file-selection narrowing excludes class-method files
* fix(native): expand super-dispatch edges into CHA sibling overrides (#1544) runPostNativeCha was filtering out edges with technique='cha', which prevented it from expanding the super.method() → Parent.method edges inserted by runPostNativeThisDispatch (which also uses technique='cha'). Additionally, runPostNativeCha was running before runPostNativeThisDispatch, so the super-dispatch seed edges were not yet in the DB when the BFS expansion ran. Two changes fix parity with WASM: 1. Reorder so runPostNativeThisDispatch runs first, putting the technique='cha' seed edges in the DB before the expansion pass reads them. 2. Rename the CHA expansion output to technique='cha-expanded' and update both filter queries (native and WASM runChaPostPass) from != 'cha' to != 'cha-expanded'. This lets the expansion pass see 'cha'-tagged super-dispatch edges while still preventing infinite re-expansion of its own output edges. After this fix, native produces 6 CHA edges that WASM already emitted (e.g. PostMixin.m → B.m across jelly-micro fixtures where PostMixin and B both extend A). Closes #1544 * test(native): verify UNION file-selection narrowing excludes class-method files (#1550) Adds a regression test confirming that the NOT IN guard in the third UNION arm of runPostNativeThisDispatch correctly excludes class-method files (e.g. Foo.bar) from the full-build re-parse set, while still resolving func-prop this-dispatch (obj.run → obj.helper). The SQL fix was merged in #1506; this test closes the tracking issue with a dedicated guard. Closes #1550 * test(native): tighten over-scan assertions and restrict to native engine - Replace vacuously-true cross-owner negative assertions (Foo.bar → obj.*) with cross-file edge assertions scoped to native via it.skipIf - The SQL UNION guard lives in runPostNativeThisDispatch which only runs for native builds; wasm never enters that code path - Query now includes src_file/tgt_file so assertions can check file provenance - Expand fixture doc comment to explain the seen-set dedup behaviour and why cross-file edges are the observable failure signal
1 parent aca4b46 commit 04d1e5c

1 file changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Regression test for #1550: UNION file-selection arm in runPostNativeThisDispatch
3+
* must not over-scan class-method files.
4+
*
5+
* The third UNION arm selects files containing dot-named method nodes for
6+
* func-prop this-dispatch (`f.h = function(){ this.g() }`). Before the fix it
7+
* matched ALL dot-qualified method names including class methods like
8+
* `Foo.bar`, pulling every class-method file into the WASM re-parse set on
9+
* full builds.
10+
*
11+
* The fix adds:
12+
* AND SUBSTR(n.name, 1, INSTR(n.name, '.') - 1) NOT IN (
13+
* SELECT name FROM nodes WHERE kind IN ('class','struct','interface','type')
14+
* AND name IS NOT NULL
15+
* )
16+
*
17+
* This test verifies two things:
18+
* 1. Func-prop this-dispatch still resolves correctly (regression guard).
19+
* 2. Class-method files that have NO extends edges do not emit cross-file
20+
* this-dispatch edges as a result of the UNION over-scan (native only —
21+
* the SQL UNION guard lives entirely inside runPostNativeThisDispatch,
22+
* which is never called for the wasm engine).
23+
*/
24+
25+
import fs from 'node:fs';
26+
import os from 'node:os';
27+
import path from 'node:path';
28+
import Database from 'better-sqlite3';
29+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
30+
import { buildGraph } from '../../src/domain/graph/builder.js';
31+
import type { EngineMode } from '../../src/types.js';
32+
33+
/**
34+
* Two-file fixture:
35+
*
36+
* func-prop.js — contains a func-prop object: `obj.helper` and `obj.run` where
37+
* `obj.run` calls `this.helper()`. The UNION arm must pick up this file so
38+
* the this-dispatch edge `obj.run → obj.helper` is emitted.
39+
*
40+
* class-only.js — contains a standalone class Foo with methods Foo.bar and
41+
* Foo.baz. Foo.bar calls `this.baz()`. There are NO extends edges for Foo.
42+
* Before the fix the third UNION arm would include this file in relFiles
43+
* because `Foo.bar` is a dot-qualified method name. With the fix, the NOT IN
44+
* sub-select recognises `Foo` as a class name and excludes class-only.js.
45+
*
46+
* The over-scan observable for native: if class-only.js enters relFiles, the
47+
* post-pass re-parses it and processes `this.baz()` in Foo.bar. Because
48+
* Foo.bar → Foo.baz is already in the DB from the native primary pass (and
49+
* therefore in the seen-set), the post-pass does not add a duplicate. The
50+
* distinguishable failure is therefore a cross-file edge: any edge whose
51+
* source node lives in class-only.js and whose target lives in func-prop.js
52+
* (or vice-versa) would indicate the dispatch mechanism mis-routed a call.
53+
* Such an edge is structurally impossible given the fixture (Foo.bar calls
54+
* this.baz(), so resolveThisDispatch looks up Foo.baz, not obj.*), but
55+
* asserting it makes the test self-documenting and catches refactors that
56+
* change the dispatch lookup strategy.
57+
*/
58+
const FIXTURE: Record<string, string> = {
59+
'func-prop.js': `
60+
function obj() {}
61+
obj.helper = function() { return 42; }
62+
obj.run = function() {
63+
this.helper();
64+
}
65+
`,
66+
'class-only.js': `
67+
class Foo {
68+
bar() {
69+
this.baz();
70+
}
71+
baz() {
72+
return 1;
73+
}
74+
}
75+
`,
76+
};
77+
78+
const ENGINES: EngineMode[] = ['wasm', 'native'];
79+
80+
describe.each(ENGINES)('UNION file-selection narrowing (#1550, %s)', (engine) => {
81+
let tmpDir: string;
82+
let callEdges: Array<{ src: string; tgt: string; src_file: string; tgt_file: string }>;
83+
84+
beforeAll(async () => {
85+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `cg-1550-${engine}-`));
86+
for (const [rel, content] of Object.entries(FIXTURE)) {
87+
fs.writeFileSync(path.join(tmpDir, rel), content);
88+
}
89+
await buildGraph(tmpDir, { incremental: false, skipRegistry: true, engine });
90+
91+
const dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
92+
const db = new Database(dbPath, { readonly: true });
93+
try {
94+
callEdges = db
95+
.prepare(
96+
`SELECT n1.name AS src, n2.name AS tgt,
97+
n1.file AS src_file, n2.file AS tgt_file
98+
FROM edges e
99+
JOIN nodes n1 ON e.source_id = n1.id
100+
JOIN nodes n2 ON e.target_id = n2.id
101+
WHERE e.kind = 'calls'
102+
ORDER BY n1.name, n2.name`,
103+
)
104+
.all() as Array<{ src: string; tgt: string; src_file: string; tgt_file: string }>;
105+
} finally {
106+
db.close();
107+
}
108+
}, 60_000);
109+
110+
afterAll(() => {
111+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
112+
});
113+
114+
// --- func-prop resolution must still work ---
115+
116+
it('emits obj.run → obj.helper (func-prop this-dispatch)', () => {
117+
const edge = callEdges.find((e) => e.src === 'obj.run' && e.tgt === 'obj.helper');
118+
expect(
119+
edge,
120+
`Expected obj.run → obj.helper edge.\nAll edges: ${JSON.stringify(callEdges, null, 2)}`,
121+
).toBeDefined();
122+
});
123+
124+
// --- class-method file must not emit cross-file edges (native only) ---
125+
//
126+
// The SQL UNION guard lives inside runPostNativeThisDispatch, which is only
127+
// called during a native build. For the wasm engine, class-only.js is never
128+
// added to the re-parse set by this code path, so the assertions below are
129+
// native-specific.
130+
131+
it.skipIf(engine !== 'native')(
132+
'does NOT emit any cross-file edge from class-only.js to func-prop.js',
133+
() => {
134+
const crossFileEdges = callEdges.filter(
135+
(e) => e.src_file?.endsWith('class-only.js') && e.tgt_file?.endsWith('func-prop.js'),
136+
);
137+
expect(
138+
crossFileEdges,
139+
`Expected no calls from class-only.js nodes to func-prop.js nodes.\n` +
140+
`Cross-file edges: ${JSON.stringify(crossFileEdges, null, 2)}\n` +
141+
`All edges: ${JSON.stringify(callEdges, null, 2)}`,
142+
).toHaveLength(0);
143+
},
144+
);
145+
146+
it.skipIf(engine !== 'native')(
147+
'does NOT emit any cross-file edge from func-prop.js to class-only.js',
148+
() => {
149+
const crossFileEdges = callEdges.filter(
150+
(e) => e.src_file?.endsWith('func-prop.js') && e.tgt_file?.endsWith('class-only.js'),
151+
);
152+
expect(
153+
crossFileEdges,
154+
`Expected no calls from func-prop.js nodes to class-only.js nodes.\n` +
155+
`Cross-file edges: ${JSON.stringify(crossFileEdges, null, 2)}\n` +
156+
`All edges: ${JSON.stringify(callEdges, null, 2)}`,
157+
).toHaveLength(0);
158+
},
159+
);
160+
});

0 commit comments

Comments
 (0)