@remnic/coding-graph 9.6.22 → 9.6.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-5I2DBHOQ.js → chunk-7DI5P62Q.js} +2 -2
- package/dist/{chunk-CPYJACC5.js → chunk-ZLCF3XQK.js} +72 -8
- package/dist/chunk-ZLCF3XQK.js.map +1 -0
- package/dist/cypher/query-parser.js +2 -2
- package/dist/graph-store.d.ts +27 -0
- package/dist/graph-store.js +1 -1
- package/dist/index.js +302 -22
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/edge-provenance-scope.test.ts +457 -0
- package/src/engine/emit.ts +55 -8
- package/src/engine/extractors.ts +45 -11
- package/src/graph-store.ts +166 -9
- package/src/heuristic-resolution.test.ts +596 -0
- package/src/heuristic-resolution.ts +395 -0
- package/src/reindex.test.ts +57 -0
- package/src/reindex.ts +18 -6
- package/dist/chunk-CPYJACC5.js.map +0 -1
- /package/dist/{chunk-5I2DBHOQ.js.map → chunk-7DI5P62Q.js.map} +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase A heuristic edge resolution tests (issue #1891).
|
|
3
|
+
*
|
|
4
|
+
* deriveHeuristicEdges: pure function FileIR[] → per-file EdgeIR[] + stats.
|
|
5
|
+
* The store's pass-2 (batch map + full-DB dst fallback, conservative
|
|
6
|
+
* ambiguity drops) does final id resolution; this module only decides
|
|
7
|
+
* WHICH (srcQualifiedName → dstQualifiedName, CALLS) assertions a fresh
|
|
8
|
+
* parse supports, so every emitted edge must be derivable from the IR
|
|
9
|
+
* alone.
|
|
10
|
+
*/
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
deriveHeuristicEdges,
|
|
16
|
+
HEURISTIC_CONFIDENCE_SAME_FILE,
|
|
17
|
+
HEURISTIC_CONFIDENCE_IMPORT_BOUND,
|
|
18
|
+
} from "./heuristic-resolution.js";
|
|
19
|
+
import type { EdgeIR, FileIR } from "./graph-store.js";
|
|
20
|
+
|
|
21
|
+
function firstFile(result: { files: readonly { edges: readonly EdgeIR[] }[] }): { edges: readonly EdgeIR[] } {
|
|
22
|
+
const file = result.files[0];
|
|
23
|
+
assert.ok(file, "expected at least one file in the result");
|
|
24
|
+
return file;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fileIR(overrides: Partial<FileIR> & { path: string }): FileIR {
|
|
28
|
+
return {
|
|
29
|
+
language: "typescript",
|
|
30
|
+
contentHash: `h-${overrides.path}`,
|
|
31
|
+
symbols: [],
|
|
32
|
+
imports: [],
|
|
33
|
+
exports: [],
|
|
34
|
+
callSites: [],
|
|
35
|
+
routes: [],
|
|
36
|
+
...overrides,
|
|
37
|
+
} as FileIR;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const span = (startByte: number, endByte: number) => ({ startByte, endByte });
|
|
41
|
+
|
|
42
|
+
test("same-file call resolves to a CALLS edge with heuristic provenance", () => {
|
|
43
|
+
const ir = fileIR({
|
|
44
|
+
path: "main.ts",
|
|
45
|
+
symbols: [
|
|
46
|
+
{ kind: "function", name: "greet", qualifiedName: "greet", span: span(0, 70) },
|
|
47
|
+
{ kind: "function", name: "format", qualifiedName: "format", span: span(71, 132) },
|
|
48
|
+
],
|
|
49
|
+
callSites: [{ calleeNameCandidates: ["format"], span: span(40, 46) }],
|
|
50
|
+
});
|
|
51
|
+
const result = deriveHeuristicEdges([ir]);
|
|
52
|
+
const { stats } = result;
|
|
53
|
+
assert.equal(result.files.length, 1);
|
|
54
|
+
assert.deepEqual(firstFile(result).edges, [
|
|
55
|
+
{
|
|
56
|
+
srcQualifiedName: "greet",
|
|
57
|
+
dstQualifiedName: "format",
|
|
58
|
+
type: "CALLS",
|
|
59
|
+
confidence: HEURISTIC_CONFIDENCE_SAME_FILE,
|
|
60
|
+
provenance: "heuristic",
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
assert.equal(stats.callSites, 1);
|
|
64
|
+
assert.equal(stats.resolved, 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("import-bound cross-file call emits an edge for the store to resolve", () => {
|
|
68
|
+
const ir = fileIR({
|
|
69
|
+
path: "util.ts",
|
|
70
|
+
symbols: [
|
|
71
|
+
{ kind: "function", name: "shout", qualifiedName: "shout", span: span(30, 110) },
|
|
72
|
+
],
|
|
73
|
+
imports: [{ module: "./main", importedNames: ["greet"], span: span(0, 29) }],
|
|
74
|
+
callSites: [{ calleeNameCandidates: ["greet"], span: span(60, 67) }],
|
|
75
|
+
});
|
|
76
|
+
const result = deriveHeuristicEdges([ir]);
|
|
77
|
+
assert.deepEqual(firstFile(result).edges, [
|
|
78
|
+
{
|
|
79
|
+
srcQualifiedName: "shout",
|
|
80
|
+
dstQualifiedName: "greet",
|
|
81
|
+
type: "CALLS",
|
|
82
|
+
confidence: HEURISTIC_CONFIDENCE_IMPORT_BOUND,
|
|
83
|
+
provenance: "heuristic",
|
|
84
|
+
dstPathHint: "main",
|
|
85
|
+
dstImporterLanguage: "typescript",
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("unresolvable bare names are skipped and counted, never emitted", () => {
|
|
91
|
+
const ir = fileIR({
|
|
92
|
+
path: "main.ts",
|
|
93
|
+
symbols: [
|
|
94
|
+
{ kind: "function", name: "greet", qualifiedName: "greet", span: span(0, 70) },
|
|
95
|
+
],
|
|
96
|
+
callSites: [{ calleeNameCandidates: ["log"], span: span(20, 25) }],
|
|
97
|
+
});
|
|
98
|
+
const result = deriveHeuristicEdges([ir]);
|
|
99
|
+
const { stats } = result;
|
|
100
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
101
|
+
assert.equal(stats.skippedUnresolved, 1);
|
|
102
|
+
assert.equal(stats.resolved, 0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("call site outside any symbol span is skipped and counted", () => {
|
|
106
|
+
const ir = fileIR({
|
|
107
|
+
path: "main.ts",
|
|
108
|
+
symbols: [
|
|
109
|
+
{ kind: "function", name: "format", qualifiedName: "format", span: span(50, 100) },
|
|
110
|
+
],
|
|
111
|
+
// Top-level call before the first symbol.
|
|
112
|
+
callSites: [{ calleeNameCandidates: ["format"], span: span(0, 8) }],
|
|
113
|
+
});
|
|
114
|
+
const result = deriveHeuristicEdges([ir]);
|
|
115
|
+
const { stats } = result;
|
|
116
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
117
|
+
assert.equal(stats.skippedNoEnclosingSymbol, 1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("innermost enclosing symbol wins as src (nested spans)", () => {
|
|
121
|
+
const ir = fileIR({
|
|
122
|
+
path: "main.ts",
|
|
123
|
+
symbols: [
|
|
124
|
+
{ kind: "class", name: "Outer", qualifiedName: "Outer", span: span(0, 200) },
|
|
125
|
+
{ kind: "method", name: "run", qualifiedName: "Outer.run", span: span(20, 120) },
|
|
126
|
+
{ kind: "function", name: "helper", qualifiedName: "helper", span: span(210, 260) },
|
|
127
|
+
],
|
|
128
|
+
callSites: [{ calleeNameCandidates: ["helper"], span: span(60, 68) }],
|
|
129
|
+
});
|
|
130
|
+
const result = deriveHeuristicEdges([ir]);
|
|
131
|
+
assert.equal(firstFile(result).edges.length, 1);
|
|
132
|
+
assert.equal(firstFile(result).edges[0]?.srcQualifiedName, "Outer.run");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("ambiguous same-file name is skipped conservatively and counted", () => {
|
|
136
|
+
const ir = fileIR({
|
|
137
|
+
path: "main.ts",
|
|
138
|
+
symbols: [
|
|
139
|
+
{ kind: "function", name: "run", qualifiedName: "run", span: span(0, 50) },
|
|
140
|
+
// Same bare name at a different span (e.g. overload-style duplicate).
|
|
141
|
+
{ kind: "function", name: "format", qualifiedName: "a.format", span: span(60, 100) },
|
|
142
|
+
{ kind: "function", name: "format", qualifiedName: "b.format", span: span(110, 150) },
|
|
143
|
+
],
|
|
144
|
+
callSites: [{ calleeNameCandidates: ["format"], span: span(20, 28) }],
|
|
145
|
+
});
|
|
146
|
+
const result = deriveHeuristicEdges([ir]);
|
|
147
|
+
const { stats } = result;
|
|
148
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
149
|
+
assert.equal(stats.skippedAmbiguous, 1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("second callee candidate is tried when the first cannot resolve", () => {
|
|
153
|
+
const ir = fileIR({
|
|
154
|
+
path: "main.ts",
|
|
155
|
+
symbols: [
|
|
156
|
+
{ kind: "function", name: "run", qualifiedName: "run", span: span(0, 50) },
|
|
157
|
+
{ kind: "function", name: "fallback", qualifiedName: "fallback", span: span(60, 100) },
|
|
158
|
+
],
|
|
159
|
+
callSites: [{ calleeNameCandidates: ["missing", "fallback"], span: span(10, 20) }],
|
|
160
|
+
});
|
|
161
|
+
const result = deriveHeuristicEdges([ir]);
|
|
162
|
+
const { stats } = result;
|
|
163
|
+
assert.equal(firstFile(result).edges.length, 1);
|
|
164
|
+
assert.equal(firstFile(result).edges[0]?.dstQualifiedName, "fallback");
|
|
165
|
+
assert.equal(stats.resolved, 1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("recursive self-call emits a self edge", () => {
|
|
169
|
+
const ir = fileIR({
|
|
170
|
+
path: "main.ts",
|
|
171
|
+
symbols: [
|
|
172
|
+
{ kind: "function", name: "walk", qualifiedName: "walk", span: span(0, 90) },
|
|
173
|
+
],
|
|
174
|
+
callSites: [{ calleeNameCandidates: ["walk"], span: span(40, 46) }],
|
|
175
|
+
});
|
|
176
|
+
const result = deriveHeuristicEdges([ir]);
|
|
177
|
+
assert.deepEqual(firstFile(result).edges.map((e: EdgeIR) => [e.srcQualifiedName, e.dstQualifiedName]), [
|
|
178
|
+
["walk", "walk"],
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("duplicate src→dst call sites are deduped to one edge", () => {
|
|
183
|
+
const ir = fileIR({
|
|
184
|
+
path: "main.ts",
|
|
185
|
+
symbols: [
|
|
186
|
+
{ kind: "function", name: "greet", qualifiedName: "greet", span: span(0, 90) },
|
|
187
|
+
{ kind: "function", name: "format", qualifiedName: "format", span: span(100, 150) },
|
|
188
|
+
],
|
|
189
|
+
callSites: [
|
|
190
|
+
{ calleeNameCandidates: ["format"], span: span(20, 26) },
|
|
191
|
+
{ calleeNameCandidates: ["format"], span: span(50, 56) },
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
const result = deriveHeuristicEdges([ir]);
|
|
195
|
+
const { stats } = result;
|
|
196
|
+
assert.equal(firstFile(result).edges.length, 1);
|
|
197
|
+
assert.equal(stats.callSites, 2);
|
|
198
|
+
assert.equal(stats.resolved, 2);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("files with no call sites assert an explicit empty edge set", () => {
|
|
202
|
+
const ir = fileIR({
|
|
203
|
+
path: "empty.ts",
|
|
204
|
+
symbols: [
|
|
205
|
+
{ kind: "function", name: "solo", qualifiedName: "solo", span: span(0, 20) },
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
const result = deriveHeuristicEdges([ir]);
|
|
209
|
+
// Empty ARRAY (not undefined): the fresh parse asserts "no heuristic
|
|
210
|
+
// edges", so stale heuristic edges from a prior version are cleaned up.
|
|
211
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("output is deterministic and byte-stable across runs (rule 38)", () => {
|
|
215
|
+
const ir = fileIR({
|
|
216
|
+
path: "main.ts",
|
|
217
|
+
symbols: [
|
|
218
|
+
{ kind: "function", name: "a", qualifiedName: "a", span: span(0, 40) },
|
|
219
|
+
{ kind: "function", name: "b", qualifiedName: "b", span: span(50, 90) },
|
|
220
|
+
{ kind: "function", name: "c", qualifiedName: "c", span: span(100, 140) },
|
|
221
|
+
],
|
|
222
|
+
callSites: [
|
|
223
|
+
{ calleeNameCandidates: ["c"], span: span(10, 12) },
|
|
224
|
+
{ calleeNameCandidates: ["b"], span: span(20, 22) },
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
const one = JSON.stringify(deriveHeuristicEdges([ir]));
|
|
228
|
+
const two = JSON.stringify(deriveHeuristicEdges([ir]));
|
|
229
|
+
assert.equal(one, two);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("a nested symbol under a different parent is NOT visible to a sibling caller", () => {
|
|
233
|
+
const ir = fileIR({
|
|
234
|
+
path: "main.ts",
|
|
235
|
+
symbols: [
|
|
236
|
+
{ kind: "function", name: "outer", qualifiedName: "outer", span: span(0, 100) },
|
|
237
|
+
{ kind: "function", name: "helper", qualifiedName: "outer.helper", span: span(10, 60) },
|
|
238
|
+
{ kind: "function", name: "run", qualifiedName: "run", span: span(110, 180) },
|
|
239
|
+
],
|
|
240
|
+
callSites: [{ calleeNameCandidates: ["helper"], span: span(130, 138) }],
|
|
241
|
+
});
|
|
242
|
+
const result = deriveHeuristicEdges([ir]);
|
|
243
|
+
const { stats } = result;
|
|
244
|
+
assert.deepEqual(firstFile(result).edges, [], "outer.helper is not in run's lexical scope");
|
|
245
|
+
assert.equal(stats.skippedUnresolved, 1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("shadowing: the innermost visible declaration wins over a top-level one", () => {
|
|
249
|
+
const ir = fileIR({
|
|
250
|
+
path: "main.ts",
|
|
251
|
+
symbols: [
|
|
252
|
+
{ kind: "function", name: "greet", qualifiedName: "greet", span: span(0, 100) },
|
|
253
|
+
{ kind: "function", name: "format", qualifiedName: "greet.format", span: span(10, 50) },
|
|
254
|
+
{ kind: "function", name: "format", qualifiedName: "format", span: span(110, 160) },
|
|
255
|
+
],
|
|
256
|
+
callSites: [{ calleeNameCandidates: ["format"], span: span(60, 68) }],
|
|
257
|
+
});
|
|
258
|
+
const result = deriveHeuristicEdges([ir]);
|
|
259
|
+
assert.equal(firstFile(result).edges.length, 1);
|
|
260
|
+
assert.equal(
|
|
261
|
+
firstFile(result).edges[0]?.dstQualifiedName,
|
|
262
|
+
"greet.format",
|
|
263
|
+
"the nested declaration shadows the top-level one for calls inside greet",
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("a caller's own nested declaration is visible to it", () => {
|
|
268
|
+
const ir = fileIR({
|
|
269
|
+
path: "main.ts",
|
|
270
|
+
symbols: [
|
|
271
|
+
{ kind: "function", name: "outer", qualifiedName: "outer", span: span(0, 100) },
|
|
272
|
+
{ kind: "function", name: "helper", qualifiedName: "outer.helper", span: span(10, 40) },
|
|
273
|
+
],
|
|
274
|
+
// Call inside outer but outside helper.
|
|
275
|
+
callSites: [{ calleeNameCandidates: ["helper"], span: span(60, 68) }],
|
|
276
|
+
});
|
|
277
|
+
const result = deriveHeuristicEdges([ir]);
|
|
278
|
+
assert.deepEqual(
|
|
279
|
+
firstFile(result).edges.map((e: EdgeIR) => [e.srcQualifiedName, e.dstQualifiedName]),
|
|
280
|
+
[["outer", "outer.helper"]],
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("external-package imports never bind bare names (no false in-repo edges)", () => {
|
|
285
|
+
const ir = fileIR({
|
|
286
|
+
path: "util.ts",
|
|
287
|
+
symbols: [
|
|
288
|
+
{ kind: "function", name: "shout", qualifiedName: "shout", span: span(30, 110) },
|
|
289
|
+
],
|
|
290
|
+
imports: [{ module: "lodash", importedNames: ["map"], span: span(0, 29) }],
|
|
291
|
+
callSites: [{ calleeNameCandidates: ["map"], span: span(60, 65) }],
|
|
292
|
+
});
|
|
293
|
+
const result = deriveHeuristicEdges([ir]);
|
|
294
|
+
const { stats } = result;
|
|
295
|
+
assert.deepEqual(firstFile(result).edges, [], "lodash's map must not bind an in-repo symbol");
|
|
296
|
+
assert.equal(stats.skippedUnresolved, 1);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("relative-module imports bind with a normalized in-repo hint", () => {
|
|
300
|
+
const ir = fileIR({
|
|
301
|
+
path: "src/util.ts",
|
|
302
|
+
symbols: [
|
|
303
|
+
{ kind: "function", name: "shout", qualifiedName: "shout", span: span(30, 110) },
|
|
304
|
+
],
|
|
305
|
+
imports: [{ module: "../lib/main.js", importedNames: ["greet"], span: span(0, 29) }],
|
|
306
|
+
callSites: [{ calleeNameCandidates: ["greet"], span: span(60, 67) }],
|
|
307
|
+
});
|
|
308
|
+
const result = deriveHeuristicEdges([ir]);
|
|
309
|
+
assert.equal(firstFile(result).edges.length, 1);
|
|
310
|
+
assert.equal(firstFile(result).edges[0]?.confidence, HEURISTIC_CONFIDENCE_IMPORT_BOUND);
|
|
311
|
+
assert.equal(firstFile(result).edges[0]?.dstPathHint, "lib/main");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("an import escaping the repo root never binds (cursor review)", () => {
|
|
315
|
+
const ir = fileIR({
|
|
316
|
+
path: "util.ts",
|
|
317
|
+
symbols: [
|
|
318
|
+
{ kind: "function", name: "shout", qualifiedName: "shout", span: span(30, 110) },
|
|
319
|
+
],
|
|
320
|
+
// dirname("util.ts") = "." so ../lib escapes the repo root — files.path
|
|
321
|
+
// is canonical and can never match a "../" hint.
|
|
322
|
+
imports: [{ module: "../lib/main.js", importedNames: ["greet"], span: span(0, 29) }],
|
|
323
|
+
callSites: [{ calleeNameCandidates: ["greet"], span: span(60, 67) }],
|
|
324
|
+
});
|
|
325
|
+
const result = deriveHeuristicEdges([ir]);
|
|
326
|
+
const { stats } = result;
|
|
327
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
328
|
+
assert.equal(stats.skippedUnresolved, 1);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("member-access call sites never bind bare names (codex review)", () => {
|
|
332
|
+
const ir = fileIR({
|
|
333
|
+
path: "main.ts",
|
|
334
|
+
symbols: [
|
|
335
|
+
{ kind: "function", name: "run", qualifiedName: "run", span: span(0, 90) },
|
|
336
|
+
{ kind: "function", name: "connect", qualifiedName: "connect", span: span(100, 150) },
|
|
337
|
+
],
|
|
338
|
+
// client.connect() — the property name matches a visible bare symbol
|
|
339
|
+
// but the receiver decides the target; Phase A must skip it.
|
|
340
|
+
callSites: [{ calleeNameCandidates: ["connect"], memberAccess: true, span: span(30, 40) }],
|
|
341
|
+
});
|
|
342
|
+
const result = deriveHeuristicEdges([ir]);
|
|
343
|
+
const { stats } = result;
|
|
344
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
345
|
+
assert.equal(stats.skippedMemberAccess, 1);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("aliased imports bind by local name and emit the exported dst (codex review)", () => {
|
|
349
|
+
const ir = fileIR({
|
|
350
|
+
path: "util.ts",
|
|
351
|
+
symbols: [
|
|
352
|
+
{ kind: "function", name: "shout", qualifiedName: "shout", span: span(30, 110) },
|
|
353
|
+
],
|
|
354
|
+
imports: [
|
|
355
|
+
{
|
|
356
|
+
module: "./main",
|
|
357
|
+
importedNames: ["greet"],
|
|
358
|
+
bindings: [{ exported: "greet", local: "salute" }],
|
|
359
|
+
span: span(0, 29),
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
// Call site uses the LOCAL alias...
|
|
363
|
+
callSites: [{ calleeNameCandidates: ["salute"], span: span(60, 68) }],
|
|
364
|
+
});
|
|
365
|
+
const result = deriveHeuristicEdges([ir]);
|
|
366
|
+
assert.equal(firstFile(result).edges.length, 1);
|
|
367
|
+
// ...but the edge's dst is the EXPORTED name in the target file.
|
|
368
|
+
assert.equal(firstFile(result).edges[0]?.dstQualifiedName, "greet");
|
|
369
|
+
assert.equal(firstFile(result).edges[0]?.dstPathHint, "main");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("an ambiguous first candidate does not block a later candidate's import binding (cursor review)", () => {
|
|
373
|
+
const ir = fileIR({
|
|
374
|
+
path: "util.ts",
|
|
375
|
+
symbols: [
|
|
376
|
+
{ kind: "function", name: "shout", qualifiedName: "shout", span: span(40, 120) },
|
|
377
|
+
{ kind: "function", name: "dup", qualifiedName: "a.dup", span: span(130, 160) },
|
|
378
|
+
{ kind: "function", name: "dup", qualifiedName: "b.dup", span: span(170, 200) },
|
|
379
|
+
],
|
|
380
|
+
imports: [{ module: "./main", importedNames: ["greet"], span: span(0, 29) }],
|
|
381
|
+
callSites: [{ calleeNameCandidates: ["dup", "greet"], span: span(60, 70) }],
|
|
382
|
+
});
|
|
383
|
+
const result = deriveHeuristicEdges([ir]);
|
|
384
|
+
assert.equal(firstFile(result).edges.length, 1);
|
|
385
|
+
assert.equal(firstFile(result).edges[0]?.dstQualifiedName, "greet");
|
|
386
|
+
assert.equal(firstFile(result).edges[0]?.dstPathHint, "main");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("python dot-style relative imports bind with package-path hints (cursor review)", () => {
|
|
390
|
+
const ir = fileIR({
|
|
391
|
+
path: "pkg/app/views.py",
|
|
392
|
+
language: "python",
|
|
393
|
+
symbols: [
|
|
394
|
+
{ kind: "function", name: "render", qualifiedName: "render", span: span(30, 110) },
|
|
395
|
+
],
|
|
396
|
+
imports: [
|
|
397
|
+
{ module: ".models", importedNames: ["User"], span: span(0, 25) },
|
|
398
|
+
{ module: "..lib.utils", importedNames: ["helper"], span: span(26, 55) },
|
|
399
|
+
],
|
|
400
|
+
callSites: [
|
|
401
|
+
{ calleeNameCandidates: ["User"], span: span(60, 64) },
|
|
402
|
+
{ calleeNameCandidates: ["helper"], span: span(70, 76) },
|
|
403
|
+
],
|
|
404
|
+
});
|
|
405
|
+
const result = deriveHeuristicEdges([ir]);
|
|
406
|
+
const hints = firstFile(result).edges.map((e: EdgeIR) => [e.dstQualifiedName, e.dstPathHint]);
|
|
407
|
+
assert.deepEqual(hints, [
|
|
408
|
+
["User", "pkg/app/models"],
|
|
409
|
+
["helper", "pkg/lib/utils"],
|
|
410
|
+
]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("python dot-style import escaping the root never binds", () => {
|
|
414
|
+
const ir = fileIR({
|
|
415
|
+
path: "views.py",
|
|
416
|
+
language: "python",
|
|
417
|
+
symbols: [
|
|
418
|
+
{ kind: "function", name: "render", qualifiedName: "render", span: span(30, 110) },
|
|
419
|
+
],
|
|
420
|
+
imports: [{ module: "..outside", importedNames: ["thing"], span: span(0, 25) }],
|
|
421
|
+
callSites: [{ calleeNameCandidates: ["thing"], span: span(60, 65) }],
|
|
422
|
+
});
|
|
423
|
+
const result = deriveHeuristicEdges([ir]);
|
|
424
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("same exported name from two modules yields two hinted edges (codex review)", () => {
|
|
428
|
+
const ir = fileIR({
|
|
429
|
+
path: "util.ts",
|
|
430
|
+
symbols: [
|
|
431
|
+
{ kind: "function", name: "run", qualifiedName: "run", span: span(60, 160) },
|
|
432
|
+
],
|
|
433
|
+
imports: [
|
|
434
|
+
{ module: "./a", importedNames: ["foo"], bindings: [{ exported: "foo", local: "fooA" }], span: span(0, 28) },
|
|
435
|
+
{ module: "./b", importedNames: ["foo"], bindings: [{ exported: "foo", local: "fooB" }], span: span(29, 57) },
|
|
436
|
+
],
|
|
437
|
+
callSites: [
|
|
438
|
+
{ calleeNameCandidates: ["fooA"], span: span(80, 86) },
|
|
439
|
+
{ calleeNameCandidates: ["fooB"], span: span(100, 106) },
|
|
440
|
+
],
|
|
441
|
+
});
|
|
442
|
+
const result = deriveHeuristicEdges([ir]);
|
|
443
|
+
assert.deepEqual(
|
|
444
|
+
firstFile(result).edges.map((e: EdgeIR) => [e.dstQualifiedName, e.dstPathHint]),
|
|
445
|
+
[
|
|
446
|
+
["foo", "a"],
|
|
447
|
+
["foo", "b"],
|
|
448
|
+
],
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("a bare call inside a method never binds a sibling method (codex review round 8)", () => {
|
|
453
|
+
const ir = fileIR({
|
|
454
|
+
path: "main.ts",
|
|
455
|
+
symbols: [
|
|
456
|
+
{ kind: "class", name: "C", qualifiedName: "C", span: span(0, 200) },
|
|
457
|
+
{ kind: "method", name: "run", qualifiedName: "C.run", span: span(10, 100) },
|
|
458
|
+
{ kind: "method", name: "helper", qualifiedName: "C.helper", span: span(110, 190) },
|
|
459
|
+
{ kind: "function", name: "helper", qualifiedName: "helper", span: span(210, 260) },
|
|
460
|
+
],
|
|
461
|
+
// bare helper() inside C.run — this.helper() would be memberAccess.
|
|
462
|
+
callSites: [{ calleeNameCandidates: ["helper"], span: span(40, 48) }],
|
|
463
|
+
});
|
|
464
|
+
const result = deriveHeuristicEdges([ir]);
|
|
465
|
+
assert.deepEqual(
|
|
466
|
+
firstFile(result).edges.map((e: EdgeIR) => [e.srcQualifiedName, e.dstQualifiedName]),
|
|
467
|
+
[["C.run", "helper"]],
|
|
468
|
+
"binds the file-level function, never the sibling method C.helper",
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("with only a sibling method available, a bare call stays unresolved", () => {
|
|
473
|
+
const ir = fileIR({
|
|
474
|
+
path: "main.ts",
|
|
475
|
+
symbols: [
|
|
476
|
+
{ kind: "class", name: "C", qualifiedName: "C", span: span(0, 200) },
|
|
477
|
+
{ kind: "method", name: "run", qualifiedName: "C.run", span: span(10, 100) },
|
|
478
|
+
{ kind: "method", name: "helper", qualifiedName: "C.helper", span: span(110, 190) },
|
|
479
|
+
],
|
|
480
|
+
callSites: [{ calleeNameCandidates: ["helper"], span: span(40, 48) }],
|
|
481
|
+
});
|
|
482
|
+
const result = deriveHeuristicEdges([ir]);
|
|
483
|
+
const { stats } = result;
|
|
484
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
485
|
+
assert.equal(stats.skippedUnresolved, 1);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("absent bindings falls back to importedNames; explicit [] binds nothing (rounds 9 + 12)", () => {
|
|
489
|
+
const base = {
|
|
490
|
+
path: "util.ts",
|
|
491
|
+
symbols: [
|
|
492
|
+
{ kind: "function" as const, name: "shout", qualifiedName: "shout", span: span(30, 110) },
|
|
493
|
+
],
|
|
494
|
+
callSites: [{ calleeNameCandidates: ["greet"], span: span(60, 67) }],
|
|
495
|
+
};
|
|
496
|
+
// Absent field (pre-bindings emitter / hand-built IR) -> fallback binds.
|
|
497
|
+
const absent = deriveHeuristicEdges([
|
|
498
|
+
fileIR({ ...base, imports: [{ module: "./main", importedNames: ["greet"], span: span(0, 29) }] }),
|
|
499
|
+
]);
|
|
500
|
+
assert.equal(firstFile(absent).edges.length, 1);
|
|
501
|
+
// Explicit [] (default/namespace import: exported symbol unknowable) ->
|
|
502
|
+
// never re-bound through the fallback.
|
|
503
|
+
const explicit = deriveHeuristicEdges([
|
|
504
|
+
fileIR({
|
|
505
|
+
...base,
|
|
506
|
+
imports: [{ module: "./main", importedNames: ["greet"], bindings: [], span: span(0, 29) }],
|
|
507
|
+
}),
|
|
508
|
+
]);
|
|
509
|
+
assert.deepEqual(firstFile(explicit).edges, []);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("pure parent-package python imports hint the package directory (codex review round 10)", () => {
|
|
513
|
+
const ir = fileIR({
|
|
514
|
+
path: "pkg/app/views.py",
|
|
515
|
+
language: "python",
|
|
516
|
+
symbols: [
|
|
517
|
+
{ kind: "function", name: "render", qualifiedName: "render", span: span(30, 110) },
|
|
518
|
+
],
|
|
519
|
+
imports: [
|
|
520
|
+
{ module: ".", importedNames: ["sibling"], span: span(0, 20) },
|
|
521
|
+
{ module: "..", importedNames: ["helper"], span: span(21, 45) },
|
|
522
|
+
],
|
|
523
|
+
callSites: [
|
|
524
|
+
{ calleeNameCandidates: ["sibling"], span: span(60, 67) },
|
|
525
|
+
{ calleeNameCandidates: ["helper"], span: span(70, 76) },
|
|
526
|
+
],
|
|
527
|
+
});
|
|
528
|
+
const result = deriveHeuristicEdges([ir]);
|
|
529
|
+
assert.deepEqual(
|
|
530
|
+
firstFile(result).edges.map((e: EdgeIR) => [e.dstQualifiedName, e.dstPathHint]),
|
|
531
|
+
[
|
|
532
|
+
["sibling", "pkg/app"],
|
|
533
|
+
["helper", "pkg"],
|
|
534
|
+
],
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("implicit-this languages bind sibling methods on bare calls (codex review round 11)", () => {
|
|
539
|
+
const ir = fileIR({
|
|
540
|
+
path: "C.java",
|
|
541
|
+
language: "java",
|
|
542
|
+
symbols: [
|
|
543
|
+
{ kind: "class", name: "C", qualifiedName: "C", span: span(0, 200) },
|
|
544
|
+
{ kind: "method", name: "run", qualifiedName: "C.run", span: span(10, 100) },
|
|
545
|
+
{ kind: "method", name: "helper", qualifiedName: "C.helper", span: span(110, 190) },
|
|
546
|
+
],
|
|
547
|
+
// Java: helper() inside run() legally targets the sibling method.
|
|
548
|
+
callSites: [{ calleeNameCandidates: ["helper"], span: span(40, 48) }],
|
|
549
|
+
});
|
|
550
|
+
const result = deriveHeuristicEdges([ir]);
|
|
551
|
+
assert.deepEqual(
|
|
552
|
+
firstFile(result).edges.map((e: EdgeIR) => [e.srcQualifiedName, e.dstQualifiedName]),
|
|
553
|
+
[["C.run", "C.helper"]],
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("php bare calls inside methods never bind sibling methods (codex review round 12)", () => {
|
|
558
|
+
const ir = fileIR({
|
|
559
|
+
path: "C.php",
|
|
560
|
+
language: "php",
|
|
561
|
+
symbols: [
|
|
562
|
+
{ kind: "class", name: "C", qualifiedName: "C", span: span(0, 200) },
|
|
563
|
+
{ kind: "method", name: "run", qualifiedName: "C.run", span: span(10, 100) },
|
|
564
|
+
{ kind: "method", name: "helper", qualifiedName: "C.helper", span: span(110, 190) },
|
|
565
|
+
],
|
|
566
|
+
callSites: [{ calleeNameCandidates: ["helper"], span: span(40, 48) }],
|
|
567
|
+
});
|
|
568
|
+
const result = deriveHeuristicEdges([ir]);
|
|
569
|
+
assert.deepEqual(firstFile(result).edges, []);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("function-local imports bind only within their enclosing symbol (codex review round 12)", () => {
|
|
573
|
+
const ir = fileIR({
|
|
574
|
+
path: "app.py",
|
|
575
|
+
language: "python",
|
|
576
|
+
symbols: [
|
|
577
|
+
{ kind: "function", name: "setup", qualifiedName: "setup", span: span(0, 100) },
|
|
578
|
+
{ kind: "function", name: "run", qualifiedName: "run", span: span(110, 200) },
|
|
579
|
+
],
|
|
580
|
+
// from .dep import helper INSIDE setup's span.
|
|
581
|
+
imports: [{ module: ".dep", importedNames: ["helper"], span: span(10, 40) }],
|
|
582
|
+
callSites: [
|
|
583
|
+
// Inside setup: binds.
|
|
584
|
+
{ calleeNameCandidates: ["helper"], span: span(60, 66) },
|
|
585
|
+
// Inside sibling run: must NOT bind.
|
|
586
|
+
{ calleeNameCandidates: ["helper"], span: span(140, 146) },
|
|
587
|
+
],
|
|
588
|
+
});
|
|
589
|
+
const result = deriveHeuristicEdges([ir]);
|
|
590
|
+
const { stats } = result;
|
|
591
|
+
assert.deepEqual(
|
|
592
|
+
firstFile(result).edges.map((e: EdgeIR) => e.srcQualifiedName),
|
|
593
|
+
["setup"],
|
|
594
|
+
);
|
|
595
|
+
assert.equal(stats.skippedUnresolved, 1);
|
|
596
|
+
});
|