ucn 3.7.8 → 3.7.10
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/core/project.js +20 -5
- package/package.json +2 -1
- package/test/accuracy.test.js +0 -1863
- package/test/fixtures/go/go.mod +0 -3
- package/test/fixtures/go/main.go +0 -257
- package/test/fixtures/go/service.go +0 -187
- package/test/fixtures/java/DataService.java +0 -279
- package/test/fixtures/java/Main.java +0 -287
- package/test/fixtures/java/Utils.java +0 -199
- package/test/fixtures/java/pom.xml +0 -6
- package/test/fixtures/javascript/main.js +0 -109
- package/test/fixtures/javascript/package.json +0 -1
- package/test/fixtures/javascript/service.js +0 -88
- package/test/fixtures/javascript/utils.js +0 -67
- package/test/fixtures/python/main.py +0 -198
- package/test/fixtures/python/pyproject.toml +0 -3
- package/test/fixtures/python/service.py +0 -166
- package/test/fixtures/python/utils.py +0 -118
- package/test/fixtures/rust/Cargo.toml +0 -3
- package/test/fixtures/rust/main.rs +0 -253
- package/test/fixtures/rust/service.rs +0 -210
- package/test/fixtures/rust/utils.rs +0 -154
- package/test/fixtures/typescript/main.ts +0 -154
- package/test/fixtures/typescript/package.json +0 -1
- package/test/fixtures/typescript/repository.ts +0 -149
- package/test/fixtures/typescript/types.ts +0 -114
- package/test/mcp-edge-cases.js +0 -634
- package/test/parser.test.js +0 -13608
- package/test/public-repos-bugs.json +0 -32
- package/test/public-repos-test.js +0 -477
- package/test/systematic-test.js +0 -619
package/test/accuracy.test.js
DELETED
|
@@ -1,1863 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* UCN Heuristic Accuracy Tests
|
|
3
|
-
*
|
|
4
|
-
* Documents the exact boundaries of UCN's name-based heuristics.
|
|
5
|
-
* Every test asserts ACTUAL behavior — "LIMITATION" tests prove where UCN fails,
|
|
6
|
-
* "PASS" tests prove where it succeeds despite complexity.
|
|
7
|
-
*
|
|
8
|
-
* Run: node --test test/accuracy.test.js
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const { describe, it } = require('node:test');
|
|
12
|
-
const assert = require('node:assert');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const os = require('os');
|
|
16
|
-
const { ProjectIndex } = require('../core/project');
|
|
17
|
-
|
|
18
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
let counter = 0;
|
|
21
|
-
function tmp(files) {
|
|
22
|
-
const dir = path.join(os.tmpdir(), `ucn-acc-${Date.now()}-${++counter}`);
|
|
23
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
-
for (const [name, content] of Object.entries(files)) {
|
|
25
|
-
const fp = path.join(dir, name);
|
|
26
|
-
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
27
|
-
fs.writeFileSync(fp, content.replace(/^\n/, ''));
|
|
28
|
-
}
|
|
29
|
-
return dir;
|
|
30
|
-
}
|
|
31
|
-
function rm(d) { fs.rmSync(d, { recursive: true, force: true }); }
|
|
32
|
-
function idx(d, g) {
|
|
33
|
-
const i = new ProjectIndex(d);
|
|
34
|
-
i.build(g || null, { quiet: true });
|
|
35
|
-
return i;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// 1. FUNCTION ALIASING — local rename breaks name-based tracking
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
describe('1. Function Aliasing', () => {
|
|
43
|
-
|
|
44
|
-
it('FIXED: local alias — findCallers resolves through alias', () => {
|
|
45
|
-
const d = tmp({
|
|
46
|
-
'package.json': '{"name":"t"}',
|
|
47
|
-
'lib.js': `
|
|
48
|
-
function parse(input) { return input.split(','); }
|
|
49
|
-
const myParse = parse;
|
|
50
|
-
function caller() { return myParse('a,b,c'); }
|
|
51
|
-
module.exports = { parse, myParse, caller };
|
|
52
|
-
`});
|
|
53
|
-
try {
|
|
54
|
-
const index = idx(d);
|
|
55
|
-
const callers = index.findCallers('parse');
|
|
56
|
-
// myParse resolves to parse via alias tracking
|
|
57
|
-
assert.ok(callers.some(c => c.callerName === 'caller'),
|
|
58
|
-
'Alias tracking: myParse resolved to parse — caller() found');
|
|
59
|
-
} finally { rm(d); }
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('FIXED: local alias — findCallees resolves through alias', () => {
|
|
63
|
-
const d = tmp({
|
|
64
|
-
'package.json': '{"name":"t"}',
|
|
65
|
-
'lib.js': `
|
|
66
|
-
function parse(input) { return input.split(','); }
|
|
67
|
-
const myParse = parse;
|
|
68
|
-
function caller() { return myParse('a,b,c'); }
|
|
69
|
-
module.exports = { parse, myParse, caller };
|
|
70
|
-
`});
|
|
71
|
-
try {
|
|
72
|
-
const index = idx(d);
|
|
73
|
-
const def = index.symbols.get('caller')?.[0];
|
|
74
|
-
const callees = index.findCallees(def);
|
|
75
|
-
// Alias tracking: myParse → parse, resolved in symbol table
|
|
76
|
-
assert.ok(callees.some(c => c.name === 'parse'),
|
|
77
|
-
'Alias tracking: myParse resolved to parse in callees');
|
|
78
|
-
} finally { rm(d); }
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('FIXED: destructured rename resolved through alias', () => {
|
|
82
|
-
const d = tmp({
|
|
83
|
-
'package.json': '{"name":"t"}',
|
|
84
|
-
'parser.js': `
|
|
85
|
-
function parse(input) { return input.split(','); }
|
|
86
|
-
module.exports = { parse };
|
|
87
|
-
`,
|
|
88
|
-
'app.js': `
|
|
89
|
-
const { parse: csvParse } = require('./parser');
|
|
90
|
-
function process(input) { return csvParse(input); }
|
|
91
|
-
module.exports = { process };
|
|
92
|
-
`});
|
|
93
|
-
try {
|
|
94
|
-
const index = idx(d);
|
|
95
|
-
const callers = index.findCallers('parse');
|
|
96
|
-
// Alias tracking: csvParse → parse, resolved through destructured rename
|
|
97
|
-
assert.ok(callers.some(c => c.callerName === 'process'),
|
|
98
|
-
'Alias tracking: csvParse resolved to parse — process() found');
|
|
99
|
-
} finally { rm(d); }
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('FIXED: conditional ternary — both targets resolved', () => {
|
|
103
|
-
const d = tmp({
|
|
104
|
-
'package.json': '{"name":"t"}',
|
|
105
|
-
'lib.js': `
|
|
106
|
-
function parseCSV(input) { return input.split(','); }
|
|
107
|
-
function parseJSON(input) { return JSON.parse(input); }
|
|
108
|
-
function process(input, format) {
|
|
109
|
-
const parser = format === 'csv' ? parseCSV : parseJSON;
|
|
110
|
-
return parser(input);
|
|
111
|
-
}
|
|
112
|
-
module.exports = { parseCSV, parseJSON, process };
|
|
113
|
-
`});
|
|
114
|
-
try {
|
|
115
|
-
const index = idx(d);
|
|
116
|
-
// Both branches of ternary tracked as aliases
|
|
117
|
-
const callersCSV = index.findCallers('parseCSV');
|
|
118
|
-
const callersJSON = index.findCallers('parseJSON');
|
|
119
|
-
assert.ok(callersCSV.some(c => c.callerName === 'process'),
|
|
120
|
-
'Ternary alias: parseCSV branch detected');
|
|
121
|
-
assert.ok(callersJSON.some(c => c.callerName === 'process'),
|
|
122
|
-
'Ternary alias: parseJSON branch detected');
|
|
123
|
-
} finally { rm(d); }
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('FIXED: Python local alias resolved', () => {
|
|
127
|
-
const d = tmp({
|
|
128
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
129
|
-
'lib.py': `
|
|
130
|
-
def parse(text):
|
|
131
|
-
return text.split(',')
|
|
132
|
-
|
|
133
|
-
my_parse = parse
|
|
134
|
-
|
|
135
|
-
def caller():
|
|
136
|
-
return my_parse('a,b,c')
|
|
137
|
-
`});
|
|
138
|
-
try {
|
|
139
|
-
const index = idx(d);
|
|
140
|
-
const callers = index.findCallers('parse');
|
|
141
|
-
assert.ok(callers.some(c => c.callerName === 'caller'),
|
|
142
|
-
'Alias tracking: my_parse resolved to parse — caller() found');
|
|
143
|
-
} finally { rm(d); }
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('FIXED: functools.partial alias resolves to wrapped function', () => {
|
|
147
|
-
const d = tmp({
|
|
148
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
149
|
-
'lib.py': `
|
|
150
|
-
from functools import partial
|
|
151
|
-
|
|
152
|
-
def transform(data, mode):
|
|
153
|
-
return data.upper() if mode == 'upper' else data.lower()
|
|
154
|
-
|
|
155
|
-
upper = partial(transform, mode='upper')
|
|
156
|
-
|
|
157
|
-
def process(text):
|
|
158
|
-
return upper(text)
|
|
159
|
-
`});
|
|
160
|
-
try {
|
|
161
|
-
const index = idx(d);
|
|
162
|
-
const callers = index.findCallers('transform');
|
|
163
|
-
assert.ok(callers.some(c => c.callerName === 'process'),
|
|
164
|
-
'partial(transform) alias "upper" should resolve — process calls transform via upper');
|
|
165
|
-
} finally { rm(d); }
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('PASS: direct call still detected alongside alias', () => {
|
|
169
|
-
const d = tmp({
|
|
170
|
-
'package.json': '{"name":"t"}',
|
|
171
|
-
'lib.js': `
|
|
172
|
-
function parse(input) { return input.split(','); }
|
|
173
|
-
const myParse = parse;
|
|
174
|
-
function directCaller() { return parse('a,b,c'); }
|
|
175
|
-
function aliasCaller() { return myParse('a,b,c'); }
|
|
176
|
-
module.exports = { parse, myParse, directCaller, aliasCaller };
|
|
177
|
-
`});
|
|
178
|
-
try {
|
|
179
|
-
const index = idx(d);
|
|
180
|
-
const callers = index.findCallers('parse');
|
|
181
|
-
assert.ok(callers.some(c => c.callerName === 'directCaller'),
|
|
182
|
-
'Direct call to parse() is correctly detected');
|
|
183
|
-
} finally { rm(d); }
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ============================================================================
|
|
188
|
-
// 2. DYNAMIC DISPATCH — computed property, getattr, reflection
|
|
189
|
-
// ============================================================================
|
|
190
|
-
|
|
191
|
-
describe('2. Dynamic Dispatch', () => {
|
|
192
|
-
|
|
193
|
-
it('LIMITATION: JS computed property call — zero callees', () => {
|
|
194
|
-
const d = tmp({
|
|
195
|
-
'package.json': '{"name":"t"}',
|
|
196
|
-
'lib.js': `
|
|
197
|
-
function greet() { return 'hello'; }
|
|
198
|
-
function farewell() { return 'bye'; }
|
|
199
|
-
function callDynamic(obj, methodName) {
|
|
200
|
-
return obj[methodName]();
|
|
201
|
-
}
|
|
202
|
-
module.exports = { greet, farewell, callDynamic };
|
|
203
|
-
`});
|
|
204
|
-
try {
|
|
205
|
-
const index = idx(d);
|
|
206
|
-
const def = index.symbols.get('callDynamic')?.[0];
|
|
207
|
-
const callees = index.findCallees(def);
|
|
208
|
-
assert.strictEqual(callees.length, 0,
|
|
209
|
-
'obj[methodName]() — computed property produces zero callees');
|
|
210
|
-
} finally { rm(d); }
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('LIMITATION: Python getattr visitor pattern — invisible dispatch', () => {
|
|
214
|
-
const d = tmp({
|
|
215
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
216
|
-
'visitor.py': `
|
|
217
|
-
class Visitor:
|
|
218
|
-
def visit(self, node_type):
|
|
219
|
-
method = getattr(self, f'visit_{node_type}', None)
|
|
220
|
-
if method:
|
|
221
|
-
return method()
|
|
222
|
-
return None
|
|
223
|
-
|
|
224
|
-
def visit_add(self):
|
|
225
|
-
return 'add'
|
|
226
|
-
|
|
227
|
-
def visit_sub(self):
|
|
228
|
-
return 'subtract'
|
|
229
|
-
`});
|
|
230
|
-
try {
|
|
231
|
-
const index = idx(d);
|
|
232
|
-
const def = index.symbols.get('visit')?.[0];
|
|
233
|
-
const callees = index.findCallees(def);
|
|
234
|
-
// getattr constructs method name at runtime — invisible
|
|
235
|
-
assert.ok(!callees.some(c => c.name === 'visit_add'),
|
|
236
|
-
'getattr-dispatched methods invisible to AST');
|
|
237
|
-
assert.ok(!callees.some(c => c.name === 'visit_sub'),
|
|
238
|
-
'getattr-dispatched methods invisible to AST');
|
|
239
|
-
} finally { rm(d); }
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('LIMITATION: Java reflection — invisible invocation', () => {
|
|
243
|
-
const d = tmp({
|
|
244
|
-
'pom.xml': '<project><modelVersion>4.0.0</modelVersion><groupId>t</groupId><artifactId>t</artifactId><version>1</version></project>',
|
|
245
|
-
'Service.java': `
|
|
246
|
-
public class Service {
|
|
247
|
-
public String process(String input) {
|
|
248
|
-
return input.toUpperCase();
|
|
249
|
-
}
|
|
250
|
-
public Object callViaReflection(String methodName) throws Exception {
|
|
251
|
-
return this.getClass().getMethod(methodName).invoke(this);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
`});
|
|
255
|
-
try {
|
|
256
|
-
const index = idx(d);
|
|
257
|
-
const def = index.symbols.get('callViaReflection')?.[0];
|
|
258
|
-
const callees = index.findCallees(def);
|
|
259
|
-
assert.ok(!callees.some(c => c.name === 'process'),
|
|
260
|
-
'Reflection-based call invisible to AST');
|
|
261
|
-
} finally { rm(d); }
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('FIXED: Python handler registry — function arg detected as caller', () => {
|
|
265
|
-
const d = tmp({
|
|
266
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
267
|
-
'registry.py': `
|
|
268
|
-
class Registry:
|
|
269
|
-
def __init__(self):
|
|
270
|
-
self._handlers = {}
|
|
271
|
-
|
|
272
|
-
def register(self, name, handler):
|
|
273
|
-
self._handlers[name] = handler
|
|
274
|
-
|
|
275
|
-
def dispatch(self, name, *args):
|
|
276
|
-
return self._handlers[name](*args)
|
|
277
|
-
|
|
278
|
-
def handle_create(data):
|
|
279
|
-
return {'action': 'create'}
|
|
280
|
-
|
|
281
|
-
def setup():
|
|
282
|
-
r = Registry()
|
|
283
|
-
r.register('create', handle_create)
|
|
284
|
-
r.dispatch('create', {})
|
|
285
|
-
`});
|
|
286
|
-
try {
|
|
287
|
-
const index = idx(d);
|
|
288
|
-
// Function-argument detection: handle_create passed as arg to register()
|
|
289
|
-
// is detected as a caller relationship via isPotentialCallback
|
|
290
|
-
const callers = index.findCallers('handle_create');
|
|
291
|
-
assert.ok(callers.some(c => c.callerName === 'setup'),
|
|
292
|
-
'Function-argument detection: handle_create passed to register() — setup() found as caller');
|
|
293
|
-
// deadcode also sees it
|
|
294
|
-
const dead = index.deadcode({ includeExported: true });
|
|
295
|
-
assert.ok(!dead.some(d => d.name === 'handle_create'),
|
|
296
|
-
'deadcode correctly sees handle_create identifier in code — not reported dead');
|
|
297
|
-
} finally { rm(d); }
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// ============================================================================
|
|
302
|
-
// 3. INHERITANCE — method resolution through class hierarchy
|
|
303
|
-
// ============================================================================
|
|
304
|
-
|
|
305
|
-
describe('3. Inheritance', () => {
|
|
306
|
-
|
|
307
|
-
it('FIXED: Python self.method() resolves parent class method via inheritance', () => {
|
|
308
|
-
const d = tmp({
|
|
309
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
310
|
-
'animals.py': `
|
|
311
|
-
class Animal:
|
|
312
|
-
def speak(self):
|
|
313
|
-
return self.sound()
|
|
314
|
-
|
|
315
|
-
def sound(self):
|
|
316
|
-
return 'generic'
|
|
317
|
-
|
|
318
|
-
class Cat(Animal):
|
|
319
|
-
def purr(self):
|
|
320
|
-
return self.sound()
|
|
321
|
-
`});
|
|
322
|
-
try {
|
|
323
|
-
const index = idx(d);
|
|
324
|
-
const def = index.symbols.get('purr')?.[0];
|
|
325
|
-
const callees = index.findCallees(def);
|
|
326
|
-
// Cat inherits from Animal — self.sound() walks inheritance chain
|
|
327
|
-
assert.ok(callees.some(c => c.name === 'sound'),
|
|
328
|
-
'Inheritance traversal: self.sound() in Cat resolves to Animal.sound');
|
|
329
|
-
} finally { rm(d); }
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('PASS: self.method() resolves when method exists in same class', () => {
|
|
333
|
-
const d = tmp({
|
|
334
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
335
|
-
'animals.py': `
|
|
336
|
-
class Animal:
|
|
337
|
-
def speak(self):
|
|
338
|
-
return self.sound()
|
|
339
|
-
|
|
340
|
-
def sound(self):
|
|
341
|
-
return 'generic'
|
|
342
|
-
`});
|
|
343
|
-
try {
|
|
344
|
-
const index = idx(d);
|
|
345
|
-
const def = index.symbols.get('speak')?.[0];
|
|
346
|
-
const callees = index.findCallees(def);
|
|
347
|
-
assert.ok(callees.some(c => c.name === 'sound'),
|
|
348
|
-
'self.sound() in Animal resolves — sound() exists in same class');
|
|
349
|
-
} finally { rm(d); }
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('FIXED: JS this.method() resolves parent class method via inheritance', () => {
|
|
353
|
-
const d = tmp({
|
|
354
|
-
'package.json': '{"name":"t"}',
|
|
355
|
-
'classes.js': `
|
|
356
|
-
class Base {
|
|
357
|
-
helper() { return 42; }
|
|
358
|
-
}
|
|
359
|
-
class Child extends Base {
|
|
360
|
-
process() { return this.helper(); }
|
|
361
|
-
}
|
|
362
|
-
module.exports = { Base, Child };
|
|
363
|
-
`});
|
|
364
|
-
try {
|
|
365
|
-
const index = idx(d);
|
|
366
|
-
const def = index.symbols.get('process')?.[0];
|
|
367
|
-
const callees = index.findCallees(def);
|
|
368
|
-
assert.ok(callees.some(c => c.name === 'helper'),
|
|
369
|
-
'Inheritance traversal: this.helper() in Child resolves to Base.helper');
|
|
370
|
-
} finally { rm(d); }
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('LIMITATION: Go interface dispatch — cannot determine which impl', () => {
|
|
374
|
-
const d = tmp({
|
|
375
|
-
'go.mod': 'module test\ngo 1.21',
|
|
376
|
-
'main.go': `package main
|
|
377
|
-
|
|
378
|
-
type Processor interface {
|
|
379
|
-
Process() string
|
|
380
|
-
}
|
|
381
|
-
type TypeA struct{ data string }
|
|
382
|
-
type TypeB struct{ value int }
|
|
383
|
-
func (a *TypeA) Process() string { return a.data }
|
|
384
|
-
func (b *TypeB) Process() string { return string(rune(b.value)) }
|
|
385
|
-
func execute(p Processor) string { return p.Process() }
|
|
386
|
-
func main() {
|
|
387
|
-
a := &TypeA{data: "hello"}
|
|
388
|
-
execute(a)
|
|
389
|
-
}
|
|
390
|
-
`});
|
|
391
|
-
try {
|
|
392
|
-
const index = idx(d);
|
|
393
|
-
const def = index.symbols.get('execute')?.[0];
|
|
394
|
-
const callees = index.findCallees(def);
|
|
395
|
-
const processCallees = callees.filter(c => c.name === 'Process');
|
|
396
|
-
// Finds Process as callee but picks one arbitrarily (same-file heuristic)
|
|
397
|
-
assert.ok(processCallees.length <= 1,
|
|
398
|
-
'Interface dispatch: resolves to at most 1 Process (picks arbitrarily)');
|
|
399
|
-
} finally { rm(d); }
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it('LIMITATION: Rust trait dispatch — cannot determine which impl', () => {
|
|
403
|
-
const d = tmp({
|
|
404
|
-
'Cargo.toml': '[package]\nname = "t"\nversion = "0.1.0"\nedition = "2021"',
|
|
405
|
-
'src/main.rs': `
|
|
406
|
-
trait Processor {
|
|
407
|
-
fn process(&self) -> String;
|
|
408
|
-
}
|
|
409
|
-
struct TypeA { data: String }
|
|
410
|
-
struct TypeB { value: i32 }
|
|
411
|
-
impl Processor for TypeA {
|
|
412
|
-
fn process(&self) -> String { self.data.clone() }
|
|
413
|
-
}
|
|
414
|
-
impl Processor for TypeB {
|
|
415
|
-
fn process(&self) -> String { self.value.to_string() }
|
|
416
|
-
}
|
|
417
|
-
fn execute(p: &dyn Processor) -> String { p.process() }
|
|
418
|
-
fn main() {
|
|
419
|
-
let a = TypeA { data: "hello".to_string() };
|
|
420
|
-
execute(&a);
|
|
421
|
-
}
|
|
422
|
-
`});
|
|
423
|
-
try {
|
|
424
|
-
const index = idx(d);
|
|
425
|
-
const def = index.symbols.get('execute')?.[0];
|
|
426
|
-
const callees = index.findCallees(def);
|
|
427
|
-
const processCallees = callees.filter(c => c.name === 'process');
|
|
428
|
-
assert.ok(processCallees.length <= 1,
|
|
429
|
-
'Trait dispatch: resolves to at most 1 process (picks arbitrarily)');
|
|
430
|
-
} finally { rm(d); }
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
it('FIXED: Python multiple inheritance — MRO resolves to first parent', () => {
|
|
434
|
-
const d = tmp({
|
|
435
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
436
|
-
'mixin.py': `
|
|
437
|
-
class Flyable:
|
|
438
|
-
def move(self):
|
|
439
|
-
return 'fly'
|
|
440
|
-
|
|
441
|
-
class Swimmable:
|
|
442
|
-
def move(self):
|
|
443
|
-
return 'swim'
|
|
444
|
-
|
|
445
|
-
class Duck(Flyable, Swimmable):
|
|
446
|
-
def do_move(self):
|
|
447
|
-
return self.move()
|
|
448
|
-
`});
|
|
449
|
-
try {
|
|
450
|
-
const index = idx(d);
|
|
451
|
-
const def = index.symbols.get('do_move')?.[0];
|
|
452
|
-
const callees = index.findCallees(def);
|
|
453
|
-
// self.move() in Duck — walks inheritance: Duck → Flyable (first parent)
|
|
454
|
-
// Finds Flyable.move, matching Python MRO order
|
|
455
|
-
assert.ok(callees.some(c => c.name === 'move'),
|
|
456
|
-
'MRO traversal: self.move() in Duck resolves to Flyable.move');
|
|
457
|
-
} finally { rm(d); }
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
// ============================================================================
|
|
462
|
-
// 4. CUSTOM HIGHER-ORDER FUNCTIONS
|
|
463
|
-
// ============================================================================
|
|
464
|
-
|
|
465
|
-
describe('4. Custom Higher-Order Functions', () => {
|
|
466
|
-
|
|
467
|
-
it('FIXED: JS function ref to custom HOF detected via general arg detection', () => {
|
|
468
|
-
const d = tmp({
|
|
469
|
-
'package.json': '{"name":"t"}',
|
|
470
|
-
'lib.js': `
|
|
471
|
-
function execute(callback, data) { return callback(data); }
|
|
472
|
-
function processItem(item) { return item * 2; }
|
|
473
|
-
function main() { execute(processItem, 42); }
|
|
474
|
-
module.exports = { execute, processItem, main };
|
|
475
|
-
`});
|
|
476
|
-
try {
|
|
477
|
-
const index = idx(d);
|
|
478
|
-
const def = index.symbols.get('main')?.[0];
|
|
479
|
-
const callees = index.findCallees(def);
|
|
480
|
-
assert.ok(callees.some(c => c.name === 'execute'), 'execute() is detected');
|
|
481
|
-
assert.ok(callees.some(c => c.name === 'processItem'),
|
|
482
|
-
'Function-argument detection: processItem passed as arg — detected as callee');
|
|
483
|
-
} finally { rm(d); }
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('PASS: JS function ref to built-in HOF IS detected', () => {
|
|
487
|
-
const d = tmp({
|
|
488
|
-
'package.json': '{"name":"t"}',
|
|
489
|
-
'lib.js': `
|
|
490
|
-
function processItem(item) { return item * 2; }
|
|
491
|
-
function main() {
|
|
492
|
-
[1, 2, 3].map(processItem);
|
|
493
|
-
setTimeout(processItem, 100);
|
|
494
|
-
}
|
|
495
|
-
module.exports = { processItem, main };
|
|
496
|
-
`});
|
|
497
|
-
try {
|
|
498
|
-
const index = idx(d);
|
|
499
|
-
const def = index.symbols.get('main')?.[0];
|
|
500
|
-
const callees = index.findCallees(def);
|
|
501
|
-
assert.ok(callees.some(c => c.name === 'processItem'),
|
|
502
|
-
'.map() and setTimeout() are in HOF list — callback detected');
|
|
503
|
-
} finally { rm(d); }
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it('FIXED: JS retry/debounce/memoize — callback detected via general arg detection', () => {
|
|
507
|
-
const d = tmp({
|
|
508
|
-
'package.json': '{"name":"t"}',
|
|
509
|
-
'lib.js': `
|
|
510
|
-
function retry(fn, times) {
|
|
511
|
-
for (let i = 0; i < times; i++) {
|
|
512
|
-
try { return fn(); } catch(e) {}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
function fetchData() { return [1, 2, 3]; }
|
|
516
|
-
function main() { retry(fetchData, 3); }
|
|
517
|
-
module.exports = { retry, fetchData, main };
|
|
518
|
-
`});
|
|
519
|
-
try {
|
|
520
|
-
const index = idx(d);
|
|
521
|
-
const def = index.symbols.get('main')?.[0];
|
|
522
|
-
const callees = index.findCallees(def);
|
|
523
|
-
assert.ok(callees.some(c => c.name === 'retry'), 'retry() detected');
|
|
524
|
-
assert.ok(callees.some(c => c.name === 'fetchData'),
|
|
525
|
-
'Function-argument detection: fetchData passed as arg — detected as callee');
|
|
526
|
-
} finally { rm(d); }
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it('PASS: JS event emitter .on() detects callback at position 1', () => {
|
|
530
|
-
const d = tmp({
|
|
531
|
-
'package.json': '{"name":"t"}',
|
|
532
|
-
'lib.js': `
|
|
533
|
-
function onMessage(data) { console.log(data); }
|
|
534
|
-
function setup() {
|
|
535
|
-
const emitter = { on: function() {} };
|
|
536
|
-
emitter.on('message', onMessage);
|
|
537
|
-
}
|
|
538
|
-
module.exports = { onMessage, setup };
|
|
539
|
-
`});
|
|
540
|
-
try {
|
|
541
|
-
const index = idx(d);
|
|
542
|
-
const def = index.symbols.get('setup')?.[0];
|
|
543
|
-
const callees = index.findCallees(def);
|
|
544
|
-
assert.ok(callees.some(c => c.name === 'onMessage'),
|
|
545
|
-
'.on("event", handler) — "on" is in HOF list, position 1 detected');
|
|
546
|
-
} finally { rm(d); }
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
it('FIXED: Python function arg detected via general arg detection', () => {
|
|
550
|
-
const d = tmp({
|
|
551
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
552
|
-
'lib.py': `
|
|
553
|
-
def process(x):
|
|
554
|
-
return x * 2
|
|
555
|
-
|
|
556
|
-
def main():
|
|
557
|
-
items = [1, 2, 3]
|
|
558
|
-
result = list(map(process, items))
|
|
559
|
-
return result
|
|
560
|
-
`});
|
|
561
|
-
try {
|
|
562
|
-
const index = idx(d);
|
|
563
|
-
const def = index.symbols.get('main')?.[0];
|
|
564
|
-
const callees = index.findCallees(def);
|
|
565
|
-
// Function-argument detection: process passed as arg to map()
|
|
566
|
-
// detected via isPotentialCallback (validated against symbol table)
|
|
567
|
-
assert.ok(callees.some(c => c.name === 'process'),
|
|
568
|
-
'Function-argument detection: process passed to map() — detected as callee');
|
|
569
|
-
} finally { rm(d); }
|
|
570
|
-
});
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// ============================================================================
|
|
574
|
-
// 5. INDIRECT CALLS — arrays, factories, closures
|
|
575
|
-
// ============================================================================
|
|
576
|
-
|
|
577
|
-
describe('5. Indirect Calls', () => {
|
|
578
|
-
|
|
579
|
-
it('LIMITATION: functions in array called via iteration', () => {
|
|
580
|
-
const d = tmp({
|
|
581
|
-
'package.json': '{"name":"t"}',
|
|
582
|
-
'lib.js': `
|
|
583
|
-
function step1() { return 1; }
|
|
584
|
-
function step2() { return 2; }
|
|
585
|
-
function step3() { return 3; }
|
|
586
|
-
function runPipeline() {
|
|
587
|
-
const steps = [step1, step2, step3];
|
|
588
|
-
return steps.map(s => s());
|
|
589
|
-
}
|
|
590
|
-
module.exports = { step1, step2, step3, runPipeline };
|
|
591
|
-
`});
|
|
592
|
-
try {
|
|
593
|
-
const index = idx(d);
|
|
594
|
-
const def = index.symbols.get('runPipeline')?.[0];
|
|
595
|
-
const callees = index.findCallees(def);
|
|
596
|
-
// steps.map(s => s()) — HOF detects 's' but it's a parameter, not a known function
|
|
597
|
-
// step1/step2/step3 appear in array literal, not as HOF arguments
|
|
598
|
-
assert.ok(!callees.some(c => c.name === 'step1'),
|
|
599
|
-
'Functions stored in array and called via iteration — not linked');
|
|
600
|
-
} finally { rm(d); }
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it('LIMITATION: function returned from factory', () => {
|
|
604
|
-
const d = tmp({
|
|
605
|
-
'package.json': '{"name":"t"}',
|
|
606
|
-
'lib.js': `
|
|
607
|
-
function createHandler(type) {
|
|
608
|
-
if (type === 'json') return parseJSON;
|
|
609
|
-
return parseText;
|
|
610
|
-
}
|
|
611
|
-
function parseJSON(input) { return JSON.parse(input); }
|
|
612
|
-
function parseText(input) { return input; }
|
|
613
|
-
function process(type, input) {
|
|
614
|
-
const handler = createHandler(type);
|
|
615
|
-
return handler(input);
|
|
616
|
-
}
|
|
617
|
-
module.exports = { createHandler, parseJSON, parseText, process };
|
|
618
|
-
`});
|
|
619
|
-
try {
|
|
620
|
-
const index = idx(d);
|
|
621
|
-
const def = index.symbols.get('process')?.[0];
|
|
622
|
-
const callees = index.findCallees(def);
|
|
623
|
-
assert.ok(callees.some(c => c.name === 'createHandler'), 'createHandler is callee');
|
|
624
|
-
assert.ok(!callees.some(c => c.name === 'parseJSON'),
|
|
625
|
-
'handler(input) — return value of factory, cannot trace through');
|
|
626
|
-
} finally { rm(d); }
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it('PASS: Promise .then()/.catch() function refs detected', () => {
|
|
630
|
-
const d = tmp({
|
|
631
|
-
'package.json': '{"name":"t"}',
|
|
632
|
-
'lib.js': `
|
|
633
|
-
function fetchData(url) { return Promise.resolve({data: url}); }
|
|
634
|
-
function transform(response) { return response.data; }
|
|
635
|
-
function handleError(err) { console.error(err); }
|
|
636
|
-
function main() {
|
|
637
|
-
fetchData('/api')
|
|
638
|
-
.then(transform)
|
|
639
|
-
.catch(handleError);
|
|
640
|
-
}
|
|
641
|
-
module.exports = { fetchData, transform, handleError, main };
|
|
642
|
-
`});
|
|
643
|
-
try {
|
|
644
|
-
const index = idx(d);
|
|
645
|
-
const def = index.symbols.get('main')?.[0];
|
|
646
|
-
const callees = index.findCallees(def);
|
|
647
|
-
assert.ok(callees.some(c => c.name === 'fetchData'), 'fetchData detected');
|
|
648
|
-
assert.ok(callees.some(c => c.name === 'transform'),
|
|
649
|
-
'.then(transform) — detected via HOF list');
|
|
650
|
-
assert.ok(callees.some(c => c.name === 'handleError'),
|
|
651
|
-
'.catch(handleError) — detected via HOF list');
|
|
652
|
-
} finally { rm(d); }
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it('LIMITATION: Python closure captures function ref', () => {
|
|
656
|
-
const d = tmp({
|
|
657
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
658
|
-
'lib.py': `
|
|
659
|
-
def make_processor(fn):
|
|
660
|
-
def wrapper(data):
|
|
661
|
-
return fn(data)
|
|
662
|
-
return wrapper
|
|
663
|
-
|
|
664
|
-
def double(x):
|
|
665
|
-
return x * 2
|
|
666
|
-
|
|
667
|
-
processor = make_processor(double)
|
|
668
|
-
|
|
669
|
-
def main():
|
|
670
|
-
return processor(21)
|
|
671
|
-
`});
|
|
672
|
-
try {
|
|
673
|
-
const index = idx(d);
|
|
674
|
-
const callers = index.findCallers('double');
|
|
675
|
-
assert.ok(!callers.some(c => c.callerName === 'main'),
|
|
676
|
-
'main() calls processor() which calls double via closure — invisible');
|
|
677
|
-
} finally { rm(d); }
|
|
678
|
-
});
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
// ============================================================================
|
|
682
|
-
// 6. DEAD CODE — false positives (reported dead but actually used)
|
|
683
|
-
// ============================================================================
|
|
684
|
-
|
|
685
|
-
describe('6. Dead Code False Positives', () => {
|
|
686
|
-
|
|
687
|
-
it('LIMITATION: visitor pattern methods reported dead', () => {
|
|
688
|
-
const d = tmp({
|
|
689
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
690
|
-
'visitor.py': `
|
|
691
|
-
class NodeVisitor:
|
|
692
|
-
def visit(self, node_type):
|
|
693
|
-
method = getattr(self, f'visit_{node_type}', None)
|
|
694
|
-
if method:
|
|
695
|
-
return method()
|
|
696
|
-
return None
|
|
697
|
-
|
|
698
|
-
def visit_number(self):
|
|
699
|
-
return 42
|
|
700
|
-
|
|
701
|
-
def visit_string(self):
|
|
702
|
-
return 'hello'
|
|
703
|
-
|
|
704
|
-
def visit_bool(self):
|
|
705
|
-
return True
|
|
706
|
-
`});
|
|
707
|
-
try {
|
|
708
|
-
const index = idx(d);
|
|
709
|
-
const dead = index.deadcode({ includeExported: true });
|
|
710
|
-
const deadNames = dead.map(d => d.name);
|
|
711
|
-
// These are called via getattr with f-string — name never appears as identifier
|
|
712
|
-
const visitorDead = ['visit_number', 'visit_string', 'visit_bool']
|
|
713
|
-
.filter(n => deadNames.includes(n));
|
|
714
|
-
assert.ok(visitorDead.length > 0,
|
|
715
|
-
`Visitor methods false-positive dead: ${visitorDead.join(', ')}`);
|
|
716
|
-
} finally { rm(d); }
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it('PASS: registry-referenced functions NOT reported dead', () => {
|
|
720
|
-
const d = tmp({
|
|
721
|
-
'package.json': '{"name":"t"}',
|
|
722
|
-
'lib.js': `
|
|
723
|
-
function handleCreate(data) { return { action: 'create', data }; }
|
|
724
|
-
function handleUpdate(data) { return { action: 'update', data }; }
|
|
725
|
-
function handleDelete(data) { return { action: 'delete', data }; }
|
|
726
|
-
const handlers = {
|
|
727
|
-
create: handleCreate,
|
|
728
|
-
update: handleUpdate,
|
|
729
|
-
delete: handleDelete,
|
|
730
|
-
};
|
|
731
|
-
function dispatch(action, data) { return handlers[action](data); }
|
|
732
|
-
module.exports = { dispatch, handlers };
|
|
733
|
-
`});
|
|
734
|
-
try {
|
|
735
|
-
const index = idx(d);
|
|
736
|
-
const dead = index.deadcode({ includeExported: true });
|
|
737
|
-
const deadNames = dead.map(d => d.name);
|
|
738
|
-
// Identifier references in object literal ARE detected by buildUsageIndex
|
|
739
|
-
assert.ok(!deadNames.includes('handleCreate'),
|
|
740
|
-
'handleCreate referenced in object literal — not dead');
|
|
741
|
-
assert.ok(!deadNames.includes('handleUpdate'),
|
|
742
|
-
'handleUpdate referenced in object literal — not dead');
|
|
743
|
-
} finally { rm(d); }
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
it('LIMITATION: Rust macro-generated functions invisible', () => {
|
|
747
|
-
const d = tmp({
|
|
748
|
-
'Cargo.toml': '[package]\nname = "t"\nversion = "0.1.0"\nedition = "2021"',
|
|
749
|
-
'src/main.rs': `
|
|
750
|
-
macro_rules! create_handler {
|
|
751
|
-
($name:ident) => {
|
|
752
|
-
fn $name() -> String {
|
|
753
|
-
String::from(stringify!($name))
|
|
754
|
-
}
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
create_handler!(handle_get);
|
|
759
|
-
create_handler!(handle_post);
|
|
760
|
-
|
|
761
|
-
fn main() {
|
|
762
|
-
handle_get();
|
|
763
|
-
handle_post();
|
|
764
|
-
}
|
|
765
|
-
`});
|
|
766
|
-
try {
|
|
767
|
-
const index = idx(d);
|
|
768
|
-
// Macro-generated functions don't appear in AST
|
|
769
|
-
const symbols = index.symbols.get('handle_get');
|
|
770
|
-
assert.ok(!symbols || symbols.length === 0,
|
|
771
|
-
'Macro-generated functions invisible to tree-sitter');
|
|
772
|
-
} finally { rm(d); }
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
it('FIXED: deadcode and findCallers now agree — function arg detected', () => {
|
|
776
|
-
// Previously there was a gap: deadcode saw the identifier but findCallers didn't.
|
|
777
|
-
// Now function-argument detection bridges that gap.
|
|
778
|
-
const d = tmp({
|
|
779
|
-
'package.json': '{"name":"t"}',
|
|
780
|
-
'lib.js': `
|
|
781
|
-
function processItem(item) { return item * 2; }
|
|
782
|
-
function customExecute(fn, data) { return fn(data); }
|
|
783
|
-
function main() { customExecute(processItem, 42); }
|
|
784
|
-
module.exports = { processItem, customExecute, main };
|
|
785
|
-
`});
|
|
786
|
-
try {
|
|
787
|
-
const index = idx(d);
|
|
788
|
-
// deadcode sees processItem identifier — alive
|
|
789
|
-
const dead = index.deadcode({ includeExported: true });
|
|
790
|
-
assert.ok(!dead.some(d => d.name === 'processItem'),
|
|
791
|
-
'deadcode: processItem is alive (identifier found in code)');
|
|
792
|
-
// findCallers now also detects processItem via isPotentialCallback
|
|
793
|
-
const callers = index.findCallers('processItem');
|
|
794
|
-
assert.strictEqual(callers.length, 1,
|
|
795
|
-
'findCallers: processItem has 1 caller (detected as function arg)');
|
|
796
|
-
assert.strictEqual(callers[0].callerName, 'main',
|
|
797
|
-
'The caller is main()');
|
|
798
|
-
} finally { rm(d); }
|
|
799
|
-
});
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
// ============================================================================
|
|
803
|
-
// 7. SCOPE AND SHADOWING
|
|
804
|
-
// ============================================================================
|
|
805
|
-
|
|
806
|
-
describe('7. Scope and Shadowing', () => {
|
|
807
|
-
|
|
808
|
-
it('FIXED: inner function shadows outer — scope-based disambiguation finds callers', () => {
|
|
809
|
-
const d = tmp({
|
|
810
|
-
'package.json': '{"name":"t"}',
|
|
811
|
-
'lib.js': `
|
|
812
|
-
function helper() { return 'outer'; }
|
|
813
|
-
function outer() {
|
|
814
|
-
function helper() { return 'inner'; }
|
|
815
|
-
return helper();
|
|
816
|
-
}
|
|
817
|
-
function other() {
|
|
818
|
-
return helper();
|
|
819
|
-
}
|
|
820
|
-
module.exports = { helper, outer, other };
|
|
821
|
-
`});
|
|
822
|
-
try {
|
|
823
|
-
const index = idx(d);
|
|
824
|
-
const callers = index.findCallers('helper');
|
|
825
|
-
// Previously: 2 bindings → uncertain → 0 callers (both dropped)
|
|
826
|
-
// Now: scope-based disambiguation resolves bindings correctly:
|
|
827
|
-
// - other() calls outer helper (module scope) → found
|
|
828
|
-
// - outer() calls inner helper (shadowed scope) → also found (caller of inner helper)
|
|
829
|
-
assert.ok(callers.some(c => c.callerName === 'other'),
|
|
830
|
-
'Scope disambiguation: other() calls outer helper — no longer dropped');
|
|
831
|
-
// Both callers found (was 0 before)
|
|
832
|
-
assert.ok(callers.length >= 1,
|
|
833
|
-
'At least one caller found (was 0 before disambiguation)');
|
|
834
|
-
} finally { rm(d); }
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
it('LIMITATION: Go closure variable capture — invisible', () => {
|
|
838
|
-
const d = tmp({
|
|
839
|
-
'go.mod': 'module test\ngo 1.21',
|
|
840
|
-
'main.go': `package main
|
|
841
|
-
|
|
842
|
-
func createAdder(x int) func(int) int {
|
|
843
|
-
return func(y int) int {
|
|
844
|
-
return x + y
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
func compute(x int) int {
|
|
849
|
-
return x * 2
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
func main() {
|
|
853
|
-
adder := createAdder(5)
|
|
854
|
-
_ = adder(3)
|
|
855
|
-
}
|
|
856
|
-
`});
|
|
857
|
-
try {
|
|
858
|
-
const index = idx(d);
|
|
859
|
-
// adder(3) calls the closure — no named function to attribute
|
|
860
|
-
const def = index.symbols.get('main')?.[0];
|
|
861
|
-
const callees = index.findCallees(def);
|
|
862
|
-
assert.ok(callees.some(c => c.name === 'createAdder'),
|
|
863
|
-
'createAdder() call is detected');
|
|
864
|
-
// But adder(3) calls the returned closure — UCN sees call to 'adder' (a variable)
|
|
865
|
-
assert.ok(!callees.some(c => c.name === 'createAdder' && c.callCount >= 2),
|
|
866
|
-
'adder(3) not linked back to createAdder — it calls the return value');
|
|
867
|
-
} finally { rm(d); }
|
|
868
|
-
});
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
// ============================================================================
|
|
872
|
-
// 8. CROSS-MODULE NAME COLLISION
|
|
873
|
-
// ============================================================================
|
|
874
|
-
|
|
875
|
-
describe('8. Cross-Module Name Collision', () => {
|
|
876
|
-
|
|
877
|
-
it('PASS: require() binding disambiguates same-name functions', () => {
|
|
878
|
-
const d = tmp({
|
|
879
|
-
'package.json': '{"name":"t"}',
|
|
880
|
-
'csv.js': `
|
|
881
|
-
function parse(input) { return input.split(','); }
|
|
882
|
-
module.exports = { parse };
|
|
883
|
-
`,
|
|
884
|
-
'json.js': `
|
|
885
|
-
function parse(input) { return JSON.parse(input); }
|
|
886
|
-
module.exports = { parse };
|
|
887
|
-
`,
|
|
888
|
-
'app.js': `
|
|
889
|
-
const { parse } = require('./csv');
|
|
890
|
-
function process(input) { return parse(input); }
|
|
891
|
-
module.exports = { process };
|
|
892
|
-
`});
|
|
893
|
-
try {
|
|
894
|
-
const index = idx(d);
|
|
895
|
-
// require('./csv') creates a binding — parse resolves to csv.js
|
|
896
|
-
const callers = index.findCallers('parse');
|
|
897
|
-
// Should find process as caller; with binding, should distinguish which parse
|
|
898
|
-
assert.ok(callers.length >= 1, 'Finds at least one caller of parse');
|
|
899
|
-
} finally { rm(d); }
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
it('LIMITATION: resolveSymbol picks winner by heuristic for same-name functions', () => {
|
|
903
|
-
const d = tmp({
|
|
904
|
-
'package.json': '{"name":"t"}',
|
|
905
|
-
'src/parser-a.js': `
|
|
906
|
-
function parse(input) { return input.split(','); }
|
|
907
|
-
module.exports = { parse };
|
|
908
|
-
`,
|
|
909
|
-
'src/parser-b.js': `
|
|
910
|
-
function parse(input) { return JSON.parse(input); }
|
|
911
|
-
module.exports = { parse };
|
|
912
|
-
`});
|
|
913
|
-
try {
|
|
914
|
-
const index = idx(d);
|
|
915
|
-
const { def, warnings } = index.resolveSymbol('parse');
|
|
916
|
-
assert.ok(def, 'Resolves to some definition');
|
|
917
|
-
assert.ok(warnings.length > 0, 'Warns about ambiguity');
|
|
918
|
-
assert.strictEqual(warnings[0].type, 'ambiguous',
|
|
919
|
-
'Warning type is ambiguous — multiple same-name definitions');
|
|
920
|
-
} finally { rm(d); }
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
it('FIXED: Go same-name methods on different types — callers found (conflated)', () => {
|
|
924
|
-
const d = tmp({
|
|
925
|
-
'go.mod': 'module test\ngo 1.21',
|
|
926
|
-
'main.go': `package main
|
|
927
|
-
|
|
928
|
-
type Reader struct{ data string }
|
|
929
|
-
type Writer struct{ buf string }
|
|
930
|
-
func (r *Reader) String() string { return r.data }
|
|
931
|
-
func (w *Writer) String() string { return w.buf }
|
|
932
|
-
func printReader(r *Reader) { r.String() }
|
|
933
|
-
func printWriter(w *Writer) { w.String() }
|
|
934
|
-
func main() {
|
|
935
|
-
r := &Reader{data: "hi"}
|
|
936
|
-
w := &Writer{buf: "lo"}
|
|
937
|
-
printReader(r)
|
|
938
|
-
printWriter(w)
|
|
939
|
-
}
|
|
940
|
-
`});
|
|
941
|
-
try {
|
|
942
|
-
const index = idx(d);
|
|
943
|
-
const callers = index.findCallers('String');
|
|
944
|
-
// Multiple method bindings: now included (over-report rather than lose all)
|
|
945
|
-
// Both callers found, even though we can't distinguish Reader.String vs Writer.String
|
|
946
|
-
assert.ok(callers.length >= 2,
|
|
947
|
-
'Method calls with multiple bindings now included — both callers found');
|
|
948
|
-
} finally { rm(d); }
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
it('LIMITATION: Java same-name methods in different classes conflated', () => {
|
|
952
|
-
const d = tmp({
|
|
953
|
-
'pom.xml': '<project><modelVersion>4.0.0</modelVersion><groupId>t</groupId><artifactId>t</artifactId><version>1</version></project>',
|
|
954
|
-
'ServiceA.java': `
|
|
955
|
-
public class ServiceA {
|
|
956
|
-
public String process(String input) {
|
|
957
|
-
return input.toUpperCase();
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
`,
|
|
961
|
-
'ServiceB.java': `
|
|
962
|
-
public class ServiceB {
|
|
963
|
-
public String process(String input) {
|
|
964
|
-
return input.toLowerCase();
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
`,
|
|
968
|
-
'App.java': `
|
|
969
|
-
public class App {
|
|
970
|
-
public void run() {
|
|
971
|
-
ServiceA a = new ServiceA();
|
|
972
|
-
ServiceB b = new ServiceB();
|
|
973
|
-
a.process("hello");
|
|
974
|
-
b.process("world");
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
`});
|
|
978
|
-
try {
|
|
979
|
-
const index = idx(d);
|
|
980
|
-
const callers = index.findCallers('process');
|
|
981
|
-
// Both a.process() and b.process() match — can't distinguish
|
|
982
|
-
assert.ok(callers.length >= 2,
|
|
983
|
-
'Finds callers to both ServiceA.process and ServiceB.process — conflated');
|
|
984
|
-
} finally { rm(d); }
|
|
985
|
-
});
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
// ============================================================================
|
|
989
|
-
// 9. VERIFY EDGE CASES
|
|
990
|
-
// ============================================================================
|
|
991
|
-
|
|
992
|
-
describe('9. Verify Edge Cases', () => {
|
|
993
|
-
|
|
994
|
-
it('LIMITATION: spread args — parameter count ambiguous', () => {
|
|
995
|
-
const d = tmp({
|
|
996
|
-
'package.json': '{"name":"t"}',
|
|
997
|
-
'lib.js': `
|
|
998
|
-
function target(a, b, c) { return a + b + c; }
|
|
999
|
-
function caller() {
|
|
1000
|
-
const args = [1, 2, 3];
|
|
1001
|
-
return target(...args);
|
|
1002
|
-
}
|
|
1003
|
-
module.exports = { target, caller };
|
|
1004
|
-
`});
|
|
1005
|
-
try {
|
|
1006
|
-
const index = idx(d);
|
|
1007
|
-
const result = index.verify('target');
|
|
1008
|
-
// target(...args) — 1 apparent arg but 3 expected
|
|
1009
|
-
if (result && result.mismatches && result.mismatches.length > 0) {
|
|
1010
|
-
assert.ok(true,
|
|
1011
|
-
'Spread args causes false mismatch — 1 apparent arg vs 3 expected');
|
|
1012
|
-
} else {
|
|
1013
|
-
assert.ok(true, 'verify may count spread as uncertain');
|
|
1014
|
-
}
|
|
1015
|
-
} finally { rm(d); }
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
it('LIMITATION: Python **kwargs — parameter count ambiguous', () => {
|
|
1019
|
-
const d = tmp({
|
|
1020
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
1021
|
-
'lib.py': `
|
|
1022
|
-
def target(a, b, c=None):
|
|
1023
|
-
return a + b
|
|
1024
|
-
|
|
1025
|
-
def caller():
|
|
1026
|
-
kwargs = {'a': 1, 'b': 2, 'c': 3}
|
|
1027
|
-
return target(**kwargs)
|
|
1028
|
-
`});
|
|
1029
|
-
try {
|
|
1030
|
-
const index = idx(d);
|
|
1031
|
-
const result = index.verify('target');
|
|
1032
|
-
if (result && result.mismatches && result.mismatches.length > 0) {
|
|
1033
|
-
assert.ok(true,
|
|
1034
|
-
'**kwargs causes false mismatch — 1 apparent arg vs 2-3 expected');
|
|
1035
|
-
} else {
|
|
1036
|
-
assert.ok(true, 'verify may count **kwargs as uncertain');
|
|
1037
|
-
}
|
|
1038
|
-
} finally { rm(d); }
|
|
1039
|
-
});
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
// ============================================================================
|
|
1043
|
-
// 10. TYPESCRIPT-SPECIFIC
|
|
1044
|
-
// ============================================================================
|
|
1045
|
-
|
|
1046
|
-
describe('10. TypeScript-Specific', () => {
|
|
1047
|
-
|
|
1048
|
-
it('LIMITATION: TS method calls not auto-included — interface dispatch invisible', () => {
|
|
1049
|
-
const d = tmp({
|
|
1050
|
-
'package.json': '{"name":"t"}',
|
|
1051
|
-
'lib.ts': `
|
|
1052
|
-
interface Handler<T> {
|
|
1053
|
-
handle(input: T): void;
|
|
1054
|
-
}
|
|
1055
|
-
class StringHandler implements Handler<string> {
|
|
1056
|
-
handle(input: string) { console.log(input.toUpperCase()); }
|
|
1057
|
-
}
|
|
1058
|
-
class NumberHandler implements Handler<number> {
|
|
1059
|
-
handle(input: number) { console.log(input * 2); }
|
|
1060
|
-
}
|
|
1061
|
-
function process<T>(handler: Handler<T>, input: T) {
|
|
1062
|
-
handler.handle(input);
|
|
1063
|
-
}
|
|
1064
|
-
export { StringHandler, NumberHandler, process };
|
|
1065
|
-
`});
|
|
1066
|
-
try {
|
|
1067
|
-
const index = idx(d);
|
|
1068
|
-
// handler.handle() is a method call — TS doesn't auto-include methods
|
|
1069
|
-
// (only Go and Java auto-include method calls)
|
|
1070
|
-
const callers = index.findCallers('handle');
|
|
1071
|
-
assert.strictEqual(callers.length, 0,
|
|
1072
|
-
'TS method calls skipped by default — use --include-methods');
|
|
1073
|
-
|
|
1074
|
-
// With includeMethods, still uncertain (2 definitions)
|
|
1075
|
-
const callersWithMethods = index.findCallers('handle', { includeMethods: true });
|
|
1076
|
-
// Even with methods included, 2 bindings → uncertain → likely 0
|
|
1077
|
-
assert.ok(callersWithMethods.length <= 1,
|
|
1078
|
-
'Even with includeMethods, same-name method ambiguity may drop callers');
|
|
1079
|
-
} finally { rm(d); }
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
it('LIMITATION: type-narrowing doesn\'t help resolution', () => {
|
|
1083
|
-
const d = tmp({
|
|
1084
|
-
'package.json': '{"name":"t"}',
|
|
1085
|
-
'lib.ts': `
|
|
1086
|
-
type Shape = Circle | Square;
|
|
1087
|
-
interface Circle { kind: 'circle'; radius: number; }
|
|
1088
|
-
interface Square { kind: 'square'; size: number; }
|
|
1089
|
-
|
|
1090
|
-
function getCircleArea(c: Circle): number { return Math.PI * c.radius ** 2; }
|
|
1091
|
-
function getSquareArea(s: Square): number { return s.size ** 2; }
|
|
1092
|
-
|
|
1093
|
-
function getArea(shape: Shape): number {
|
|
1094
|
-
if (shape.kind === 'circle') {
|
|
1095
|
-
return getCircleArea(shape);
|
|
1096
|
-
}
|
|
1097
|
-
return getSquareArea(shape);
|
|
1098
|
-
}
|
|
1099
|
-
export { getArea, getCircleArea, getSquareArea };
|
|
1100
|
-
`});
|
|
1101
|
-
try {
|
|
1102
|
-
const index = idx(d);
|
|
1103
|
-
const def = index.symbols.get('getArea')?.[0];
|
|
1104
|
-
const callees = index.findCallees(def);
|
|
1105
|
-
// These are direct calls — UCN should find them
|
|
1106
|
-
assert.ok(callees.some(c => c.name === 'getCircleArea'),
|
|
1107
|
-
'Direct calls inside type-narrowed branches ARE detected');
|
|
1108
|
-
assert.ok(callees.some(c => c.name === 'getSquareArea'),
|
|
1109
|
-
'Direct calls inside type-narrowed branches ARE detected');
|
|
1110
|
-
} finally { rm(d); }
|
|
1111
|
-
});
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
// ============================================================================
|
|
1115
|
-
// 11. FIX C FALSE POSITIVES — function-argument detection edge cases
|
|
1116
|
-
// ============================================================================
|
|
1117
|
-
|
|
1118
|
-
describe('11. Fix C Edge Cases', () => {
|
|
1119
|
-
|
|
1120
|
-
it('LIMITATION: variable sharing name with function causes false caller', () => {
|
|
1121
|
-
const d = tmp({
|
|
1122
|
-
'package.json': '{"name":"t"}',
|
|
1123
|
-
'lib.js': `
|
|
1124
|
-
function count(arr) { return arr.length; }
|
|
1125
|
-
function process(items) {
|
|
1126
|
-
const count = items.length;
|
|
1127
|
-
return doSomething(count);
|
|
1128
|
-
}
|
|
1129
|
-
function doSomething(n) { return n * 2; }
|
|
1130
|
-
module.exports = { count, process, doSomething };
|
|
1131
|
-
`});
|
|
1132
|
-
try {
|
|
1133
|
-
const index = idx(d);
|
|
1134
|
-
const callers = index.findCallers('count');
|
|
1135
|
-
// doSomething(count) — 'count' is a variable, not the function, but name matches symbol table
|
|
1136
|
-
const hasProcess = callers.some(c => c.callerName === 'process');
|
|
1137
|
-
// This is a known false positive from Fix C
|
|
1138
|
-
assert.ok(hasProcess,
|
|
1139
|
-
'False positive: variable count passed to doSomething() triggers isPotentialCallback');
|
|
1140
|
-
} finally { rm(d); }
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
it('PASS: non-function identifier args correctly filtered', () => {
|
|
1144
|
-
const d = tmp({
|
|
1145
|
-
'package.json': '{"name":"t"}',
|
|
1146
|
-
'lib.js': `
|
|
1147
|
-
function transform(x) { return x * 2; }
|
|
1148
|
-
function main() {
|
|
1149
|
-
const data = [1, 2, 3];
|
|
1150
|
-
return doWork(data);
|
|
1151
|
-
}
|
|
1152
|
-
function doWork(arr) { return arr.map(x => x + 1); }
|
|
1153
|
-
module.exports = { transform, main, doWork };
|
|
1154
|
-
`});
|
|
1155
|
-
try {
|
|
1156
|
-
const index = idx(d);
|
|
1157
|
-
const callers = index.findCallers('transform');
|
|
1158
|
-
// 'data' doesn't match any function name → not a false positive
|
|
1159
|
-
assert.ok(!callers.some(c => c.callerName === 'main'),
|
|
1160
|
-
'No false positive: data is not a known function name');
|
|
1161
|
-
} finally { rm(d); }
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
it('FIXED: callback inside object literal detected', () => {
|
|
1165
|
-
const d = tmp({
|
|
1166
|
-
'package.json': '{"name":"t"}',
|
|
1167
|
-
'lib.js': `
|
|
1168
|
-
function handleSuccess(data) { return data; }
|
|
1169
|
-
function handleError(err) { throw err; }
|
|
1170
|
-
function main() {
|
|
1171
|
-
doRequest({
|
|
1172
|
-
onSuccess: handleSuccess,
|
|
1173
|
-
onError: handleError,
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
function doRequest(opts) { opts.onSuccess('done'); }
|
|
1177
|
-
module.exports = { handleSuccess, handleError, main, doRequest };
|
|
1178
|
-
`});
|
|
1179
|
-
try {
|
|
1180
|
-
const index = idx(d);
|
|
1181
|
-
const def = index.symbols.get('main')?.[0];
|
|
1182
|
-
const callees = index.findCallees(def);
|
|
1183
|
-
// Object literal property values scanned for function identifiers
|
|
1184
|
-
assert.ok(callees.some(c => c.name === 'handleSuccess'),
|
|
1185
|
-
'Function ref in object property — detected via object literal scanning');
|
|
1186
|
-
assert.ok(callees.some(c => c.name === 'handleError'),
|
|
1187
|
-
'Both callbacks in object literal detected');
|
|
1188
|
-
} finally { rm(d); }
|
|
1189
|
-
});
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
// ============================================================================
|
|
1193
|
-
// 12. ALIAS TRACKING EDGE CASES
|
|
1194
|
-
// ============================================================================
|
|
1195
|
-
|
|
1196
|
-
describe('12. Alias Tracking Edge Cases', () => {
|
|
1197
|
-
|
|
1198
|
-
it('LIMITATION: alias reassignment — only first assignment tracked', () => {
|
|
1199
|
-
const d = tmp({
|
|
1200
|
-
'package.json': '{"name":"t"}',
|
|
1201
|
-
'lib.js': `
|
|
1202
|
-
function parseCSV(input) { return input.split(','); }
|
|
1203
|
-
function parseJSON(input) { return JSON.parse(input); }
|
|
1204
|
-
let parser = parseCSV;
|
|
1205
|
-
parser = parseJSON;
|
|
1206
|
-
function process(input) { return parser(input); }
|
|
1207
|
-
module.exports = { parseCSV, parseJSON, process };
|
|
1208
|
-
`});
|
|
1209
|
-
try {
|
|
1210
|
-
const index = idx(d);
|
|
1211
|
-
const def = index.symbols.get('process')?.[0];
|
|
1212
|
-
const callees = index.findCallees(def);
|
|
1213
|
-
// Only first assignment tracked (let parser = parseCSV), reassignment ignored
|
|
1214
|
-
const hasCSV = callees.some(c => c.name === 'parseCSV');
|
|
1215
|
-
const hasJSON = callees.some(c => c.name === 'parseJSON');
|
|
1216
|
-
assert.ok(hasCSV, 'First assignment (parseCSV) IS tracked');
|
|
1217
|
-
assert.ok(!hasJSON, 'Reassignment to parseJSON is NOT tracked');
|
|
1218
|
-
} finally { rm(d); }
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
it('LIMITATION: cross-file alias not tracked', () => {
|
|
1222
|
-
const d = tmp({
|
|
1223
|
-
'package.json': '{"name":"t"}',
|
|
1224
|
-
'parser.js': `
|
|
1225
|
-
function parse(input) { return input.split(','); }
|
|
1226
|
-
module.exports = { parse };
|
|
1227
|
-
`,
|
|
1228
|
-
'alias.js': `
|
|
1229
|
-
const { parse } = require('./parser');
|
|
1230
|
-
const myParse = parse;
|
|
1231
|
-
module.exports = { myParse };
|
|
1232
|
-
`,
|
|
1233
|
-
'app.js': `
|
|
1234
|
-
const { myParse } = require('./alias');
|
|
1235
|
-
function process(input) { return myParse(input); }
|
|
1236
|
-
module.exports = { process };
|
|
1237
|
-
`});
|
|
1238
|
-
try {
|
|
1239
|
-
const index = idx(d);
|
|
1240
|
-
const callers = index.findCallers('parse');
|
|
1241
|
-
// Cross-file alias chain (parse → myParse → process) — alias tracking is file-local only
|
|
1242
|
-
assert.ok(!callers.some(c => c.callerName === 'process'),
|
|
1243
|
-
'Cross-file alias chain not tracked — process() not found as caller of parse');
|
|
1244
|
-
} finally { rm(d); }
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
it('LIMITATION: computed alias from function return value', () => {
|
|
1248
|
-
const d = tmp({
|
|
1249
|
-
'package.json': '{"name":"t"}',
|
|
1250
|
-
'lib.js': `
|
|
1251
|
-
function getParser() { return parse; }
|
|
1252
|
-
function parse(input) { return input.split(','); }
|
|
1253
|
-
function process(input) {
|
|
1254
|
-
const parser = getParser();
|
|
1255
|
-
return parser(input);
|
|
1256
|
-
}
|
|
1257
|
-
module.exports = { getParser, parse, process };
|
|
1258
|
-
`});
|
|
1259
|
-
try {
|
|
1260
|
-
const index = idx(d);
|
|
1261
|
-
const def = index.symbols.get('process')?.[0];
|
|
1262
|
-
const callees = index.findCallees(def);
|
|
1263
|
-
// const parser = getParser() — alias tracking only handles identifier assignments
|
|
1264
|
-
assert.ok(!callees.some(c => c.name === 'parse'),
|
|
1265
|
-
'Alias from function return value — cannot trace through getParser()');
|
|
1266
|
-
assert.ok(callees.some(c => c.name === 'getParser'),
|
|
1267
|
-
'Direct call to getParser() IS detected');
|
|
1268
|
-
} finally { rm(d); }
|
|
1269
|
-
});
|
|
1270
|
-
|
|
1271
|
-
it('PASS: alias inside function scope works correctly', () => {
|
|
1272
|
-
const d = tmp({
|
|
1273
|
-
'package.json': '{"name":"t"}',
|
|
1274
|
-
'lib.js': `
|
|
1275
|
-
function parse(input) { return input.split(','); }
|
|
1276
|
-
function processA() {
|
|
1277
|
-
const fn = parse;
|
|
1278
|
-
return fn('a,b');
|
|
1279
|
-
}
|
|
1280
|
-
function processB() {
|
|
1281
|
-
const fn = parse;
|
|
1282
|
-
return fn('x,y');
|
|
1283
|
-
}
|
|
1284
|
-
module.exports = { parse, processA, processB };
|
|
1285
|
-
`});
|
|
1286
|
-
try {
|
|
1287
|
-
const index = idx(d);
|
|
1288
|
-
const callers = index.findCallers('parse');
|
|
1289
|
-
assert.ok(callers.some(c => c.callerName === 'processA'),
|
|
1290
|
-
'Local alias in processA resolves to parse');
|
|
1291
|
-
assert.ok(callers.some(c => c.callerName === 'processB'),
|
|
1292
|
-
'Local alias in processB resolves to parse');
|
|
1293
|
-
} finally { rm(d); }
|
|
1294
|
-
});
|
|
1295
|
-
});
|
|
1296
|
-
|
|
1297
|
-
// ============================================================================
|
|
1298
|
-
// 13. INHERITANCE EDGE CASES
|
|
1299
|
-
// ============================================================================
|
|
1300
|
-
|
|
1301
|
-
describe('13. Inheritance Edge Cases', () => {
|
|
1302
|
-
|
|
1303
|
-
it('LIMITATION: cross-file inheritance — parent in different file', () => {
|
|
1304
|
-
const d = tmp({
|
|
1305
|
-
'package.json': '{"name":"t"}',
|
|
1306
|
-
'base.js': `
|
|
1307
|
-
class Base {
|
|
1308
|
-
helper() { return 42; }
|
|
1309
|
-
}
|
|
1310
|
-
module.exports = { Base };
|
|
1311
|
-
`,
|
|
1312
|
-
'child.js': `
|
|
1313
|
-
const { Base } = require('./base');
|
|
1314
|
-
class Child extends Base {
|
|
1315
|
-
process() { return this.helper(); }
|
|
1316
|
-
}
|
|
1317
|
-
module.exports = { Child };
|
|
1318
|
-
`});
|
|
1319
|
-
try {
|
|
1320
|
-
const index = idx(d);
|
|
1321
|
-
const def = index.symbols.get('process')?.[0];
|
|
1322
|
-
const callees = index.findCallees(def);
|
|
1323
|
-
// Cross-file: Child extends Base — extendsGraph is project-wide
|
|
1324
|
-
assert.ok(callees.some(c => c.name === 'helper'),
|
|
1325
|
-
'Cross-file inheritance: this.helper() in Child finds Base.helper');
|
|
1326
|
-
} finally { rm(d); }
|
|
1327
|
-
});
|
|
1328
|
-
|
|
1329
|
-
it('FIXED: extends from aliased import name', () => {
|
|
1330
|
-
const d = tmp({
|
|
1331
|
-
'package.json': '{"name":"t"}',
|
|
1332
|
-
'base.js': `
|
|
1333
|
-
class BaseHandler {
|
|
1334
|
-
handle() { return 'handled'; }
|
|
1335
|
-
}
|
|
1336
|
-
module.exports = { BaseHandler };
|
|
1337
|
-
`,
|
|
1338
|
-
'child.js': `
|
|
1339
|
-
const { BaseHandler: Handler } = require('./base');
|
|
1340
|
-
class MyHandler extends Handler {
|
|
1341
|
-
process() { return this.handle(); }
|
|
1342
|
-
}
|
|
1343
|
-
module.exports = { MyHandler };
|
|
1344
|
-
`});
|
|
1345
|
-
try {
|
|
1346
|
-
const index = idx(d);
|
|
1347
|
-
const def = index.symbols.get('process')?.[0];
|
|
1348
|
-
const callees = index.findCallees(def);
|
|
1349
|
-
// Import alias Handler → BaseHandler resolved in buildInheritanceGraph
|
|
1350
|
-
assert.ok(callees.some(c => c.name === 'handle'),
|
|
1351
|
-
'Aliased extends: Handler resolved to BaseHandler — this.handle() found');
|
|
1352
|
-
} finally { rm(d); }
|
|
1353
|
-
});
|
|
1354
|
-
|
|
1355
|
-
it('FIXED: Python super() call resolved to parent class method', () => {
|
|
1356
|
-
const d = tmp({
|
|
1357
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
1358
|
-
'classes.py': `
|
|
1359
|
-
class Base:
|
|
1360
|
-
def process(self, data):
|
|
1361
|
-
return data.upper()
|
|
1362
|
-
|
|
1363
|
-
class Child(Base):
|
|
1364
|
-
def process(self, data):
|
|
1365
|
-
result = super().process(data)
|
|
1366
|
-
return result + '!'
|
|
1367
|
-
`});
|
|
1368
|
-
try {
|
|
1369
|
-
const index = idx(d);
|
|
1370
|
-
const childProcess = index.symbols.get('process')?.find(
|
|
1371
|
-
s => s.className === 'Child');
|
|
1372
|
-
const callees = index.findCallees(childProcess);
|
|
1373
|
-
// super().process() — resolved to parent class via inheritance graph
|
|
1374
|
-
assert.ok(callees.some(c => c.name === 'process'),
|
|
1375
|
-
'super().process() resolved to Base.process via inheritance chain');
|
|
1376
|
-
} finally { rm(d); }
|
|
1377
|
-
});
|
|
1378
|
-
|
|
1379
|
-
it('PASS: deep inheritance chain (3+ levels)', () => {
|
|
1380
|
-
const d = tmp({
|
|
1381
|
-
'package.json': '{"name":"t"}',
|
|
1382
|
-
'lib.js': `
|
|
1383
|
-
class A {
|
|
1384
|
-
helper() { return 1; }
|
|
1385
|
-
}
|
|
1386
|
-
class B extends A {
|
|
1387
|
-
other() { return 2; }
|
|
1388
|
-
}
|
|
1389
|
-
class C extends B {
|
|
1390
|
-
process() { return this.helper(); }
|
|
1391
|
-
}
|
|
1392
|
-
module.exports = { A, B, C };
|
|
1393
|
-
`});
|
|
1394
|
-
try {
|
|
1395
|
-
const index = idx(d);
|
|
1396
|
-
const def = index.symbols.get('process')?.[0];
|
|
1397
|
-
const callees = index.findCallees(def);
|
|
1398
|
-
// C → B → A, helper is in A
|
|
1399
|
-
// Fix D walks: C → B (no helper), B → A (has helper)
|
|
1400
|
-
assert.ok(callees.some(c => c.name === 'helper'),
|
|
1401
|
-
'Deep chain: C → B → A, this.helper() resolves to A.helper');
|
|
1402
|
-
} finally { rm(d); }
|
|
1403
|
-
});
|
|
1404
|
-
});
|
|
1405
|
-
|
|
1406
|
-
// ============================================================================
|
|
1407
|
-
// 14. CROSS-FUNCTION VALUE FLOW — the biggest gap
|
|
1408
|
-
// ============================================================================
|
|
1409
|
-
|
|
1410
|
-
describe('14. Cross-Function Value Flow', () => {
|
|
1411
|
-
|
|
1412
|
-
it('LIMITATION: function return value used as caller', () => {
|
|
1413
|
-
const d = tmp({
|
|
1414
|
-
'package.json': '{"name":"t"}',
|
|
1415
|
-
'lib.js': `
|
|
1416
|
-
function createParser() { return { parse: parseCSV }; }
|
|
1417
|
-
function parseCSV(input) { return input.split(','); }
|
|
1418
|
-
function main() {
|
|
1419
|
-
const parser = createParser();
|
|
1420
|
-
return parser.parse('a,b');
|
|
1421
|
-
}
|
|
1422
|
-
module.exports = { createParser, parseCSV, main };
|
|
1423
|
-
`});
|
|
1424
|
-
try {
|
|
1425
|
-
const index = idx(d);
|
|
1426
|
-
const callers = index.findCallers('parseCSV');
|
|
1427
|
-
// parser.parse() — can't trace through createParser() return value
|
|
1428
|
-
assert.ok(!callers.some(c => c.callerName === 'main'),
|
|
1429
|
-
'Cannot trace parseCSV through createParser() return value');
|
|
1430
|
-
} finally { rm(d); }
|
|
1431
|
-
});
|
|
1432
|
-
|
|
1433
|
-
it('LIMITATION: method chaining hides callees', () => {
|
|
1434
|
-
const d = tmp({
|
|
1435
|
-
'package.json': '{"name":"t"}',
|
|
1436
|
-
'lib.js': `
|
|
1437
|
-
class Builder {
|
|
1438
|
-
setName(n) { this.name = n; return this; }
|
|
1439
|
-
setAge(a) { this.age = a; return this; }
|
|
1440
|
-
build() { return { name: this.name, age: this.age }; }
|
|
1441
|
-
}
|
|
1442
|
-
function create() {
|
|
1443
|
-
return new Builder().setName('test').setAge(25).build();
|
|
1444
|
-
}
|
|
1445
|
-
module.exports = { Builder, create };
|
|
1446
|
-
`});
|
|
1447
|
-
try {
|
|
1448
|
-
const index = idx(d);
|
|
1449
|
-
const def = index.symbols.get('create')?.[0];
|
|
1450
|
-
const callees = index.findCallees(def);
|
|
1451
|
-
// Chained method calls filtered without includeMethods
|
|
1452
|
-
const hasBuilder = callees.some(c => c.name === 'Builder');
|
|
1453
|
-
assert.ok(hasBuilder, 'Constructor call detected');
|
|
1454
|
-
// Method chain calls are method calls — filtered by default
|
|
1455
|
-
const hasSetName = callees.some(c => c.name === 'setName');
|
|
1456
|
-
assert.ok(!hasSetName,
|
|
1457
|
-
'Method chain calls filtered — setName not in callees without includeMethods');
|
|
1458
|
-
} finally { rm(d); }
|
|
1459
|
-
});
|
|
1460
|
-
|
|
1461
|
-
it('LIMITATION: Promise chain function refs across .then() boundaries', () => {
|
|
1462
|
-
const d = tmp({
|
|
1463
|
-
'package.json': '{"name":"t"}',
|
|
1464
|
-
'lib.js': `
|
|
1465
|
-
function fetchData() { return Promise.resolve({ raw: 'data' }); }
|
|
1466
|
-
function transform(response) { return response.raw.toUpperCase(); }
|
|
1467
|
-
function validate(data) { if (!data) throw new Error('empty'); return data; }
|
|
1468
|
-
function save(data) { console.log('saved:', data); }
|
|
1469
|
-
function pipeline() {
|
|
1470
|
-
return fetchData()
|
|
1471
|
-
.then(transform)
|
|
1472
|
-
.then(validate)
|
|
1473
|
-
.then(save);
|
|
1474
|
-
}
|
|
1475
|
-
module.exports = { fetchData, transform, validate, save, pipeline };
|
|
1476
|
-
`});
|
|
1477
|
-
try {
|
|
1478
|
-
const index = idx(d);
|
|
1479
|
-
const callers = index.findCallers('validate');
|
|
1480
|
-
// .then(validate) — HOF detection finds pipeline as caller
|
|
1481
|
-
assert.ok(callers.some(c => c.callerName === 'pipeline'),
|
|
1482
|
-
'HOF detection: .then(validate) — pipeline is caller of validate');
|
|
1483
|
-
} finally { rm(d); }
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
it('LIMITATION: Python decorator wrapping changes function identity', () => {
|
|
1487
|
-
const d = tmp({
|
|
1488
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
1489
|
-
'decorators.py': `
|
|
1490
|
-
def memoize(fn):
|
|
1491
|
-
cache = {}
|
|
1492
|
-
def wrapper(*args):
|
|
1493
|
-
if args not in cache:
|
|
1494
|
-
cache[args] = fn(*args)
|
|
1495
|
-
return cache[args]
|
|
1496
|
-
return wrapper
|
|
1497
|
-
|
|
1498
|
-
@memoize
|
|
1499
|
-
def expensive_compute(n):
|
|
1500
|
-
return sum(range(n))
|
|
1501
|
-
|
|
1502
|
-
def main():
|
|
1503
|
-
return expensive_compute(1000)
|
|
1504
|
-
`});
|
|
1505
|
-
try {
|
|
1506
|
-
const index = idx(d);
|
|
1507
|
-
const callers = index.findCallers('expensive_compute');
|
|
1508
|
-
// Name-based tracking works despite decorator wrapping
|
|
1509
|
-
assert.ok(callers.some(c => c.callerName === 'main'),
|
|
1510
|
-
'Decorated function found by name — name-based tracking works despite wrapper');
|
|
1511
|
-
} finally { rm(d); }
|
|
1512
|
-
});
|
|
1513
|
-
});
|
|
1514
|
-
|
|
1515
|
-
// ============================================================================
|
|
1516
|
-
// 15. REAL-WORLD PATTERNS — common patterns from production codebases
|
|
1517
|
-
// ============================================================================
|
|
1518
|
-
|
|
1519
|
-
describe('15. Real-World Patterns', () => {
|
|
1520
|
-
|
|
1521
|
-
it('PASS: Express-style route handler registration', () => {
|
|
1522
|
-
const d = tmp({
|
|
1523
|
-
'package.json': '{"name":"t"}',
|
|
1524
|
-
'app.js': `
|
|
1525
|
-
function getUsers(req, res) { res.json([]); }
|
|
1526
|
-
function createUser(req, res) { res.json({}); }
|
|
1527
|
-
function deleteUser(req, res) { res.json({}); }
|
|
1528
|
-
function setupRoutes(app) {
|
|
1529
|
-
app.get('/users', getUsers);
|
|
1530
|
-
app.post('/users', createUser);
|
|
1531
|
-
app.delete('/users/:id', deleteUser);
|
|
1532
|
-
}
|
|
1533
|
-
module.exports = { getUsers, createUser, deleteUser, setupRoutes };
|
|
1534
|
-
`});
|
|
1535
|
-
try {
|
|
1536
|
-
const index = idx(d);
|
|
1537
|
-
const def = index.symbols.get('setupRoutes')?.[0];
|
|
1538
|
-
const callees = index.findCallees(def);
|
|
1539
|
-
// app.get('/users', getUsers) — identifier arg detected as callback
|
|
1540
|
-
assert.ok(callees.some(c => c.name === 'getUsers'),
|
|
1541
|
-
'Express route: getUsers detected as callback arg to app.get()');
|
|
1542
|
-
assert.ok(callees.some(c => c.name === 'createUser'),
|
|
1543
|
-
'Express route: createUser detected as callback arg to app.post()');
|
|
1544
|
-
} finally { rm(d); }
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
|
-
it('LIMITATION: Python pytest fixture injection — invisible dispatch', () => {
|
|
1548
|
-
const d = tmp({
|
|
1549
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
1550
|
-
'conftest.py': `
|
|
1551
|
-
import pytest
|
|
1552
|
-
|
|
1553
|
-
def create_db():
|
|
1554
|
-
return {'users': []}
|
|
1555
|
-
|
|
1556
|
-
@pytest.fixture
|
|
1557
|
-
def db():
|
|
1558
|
-
return create_db()
|
|
1559
|
-
`,
|
|
1560
|
-
'test_app.py': `
|
|
1561
|
-
def test_get_users(db):
|
|
1562
|
-
assert db['users'] == []
|
|
1563
|
-
`});
|
|
1564
|
-
try {
|
|
1565
|
-
const index = idx(d);
|
|
1566
|
-
const callers = index.findCallers('db');
|
|
1567
|
-
// pytest fixtures injected by name — no call_expression exists
|
|
1568
|
-
assert.strictEqual(callers.length, 0,
|
|
1569
|
-
'pytest fixtures injected by name — no call expression exists');
|
|
1570
|
-
} finally { rm(d); }
|
|
1571
|
-
});
|
|
1572
|
-
|
|
1573
|
-
it('PASS: Java builder pattern with same-class this.method()', () => {
|
|
1574
|
-
const d = tmp({
|
|
1575
|
-
'pom.xml': '<project><modelVersion>4.0.0</modelVersion><groupId>t</groupId><artifactId>t</artifactId><version>1</version></project>',
|
|
1576
|
-
'Builder.java': `
|
|
1577
|
-
public class Builder {
|
|
1578
|
-
private String name;
|
|
1579
|
-
private int age;
|
|
1580
|
-
public Builder setName(String n) { this.name = n; return this; }
|
|
1581
|
-
public Builder setAge(int a) { this.age = a; return this; }
|
|
1582
|
-
public String build() {
|
|
1583
|
-
this.validate();
|
|
1584
|
-
return this.name + ":" + this.age;
|
|
1585
|
-
}
|
|
1586
|
-
private void validate() {
|
|
1587
|
-
if (this.name == null) throw new RuntimeException("no name");
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
`});
|
|
1591
|
-
try {
|
|
1592
|
-
const index = idx(d);
|
|
1593
|
-
const def = index.symbols.get('build')?.[0];
|
|
1594
|
-
const callees = index.findCallees(def);
|
|
1595
|
-
// this.validate() — same-class resolution should find it
|
|
1596
|
-
assert.ok(callees.some(c => c.name === 'validate'),
|
|
1597
|
-
'Java this.validate() resolved via same-class method resolution');
|
|
1598
|
-
} finally { rm(d); }
|
|
1599
|
-
});
|
|
1600
|
-
|
|
1601
|
-
it('LIMITATION: Go error handling pattern — multiple returns', () => {
|
|
1602
|
-
const d = tmp({
|
|
1603
|
-
'go.mod': 'module test\ngo 1.21',
|
|
1604
|
-
'main.go': `package main
|
|
1605
|
-
|
|
1606
|
-
import "errors"
|
|
1607
|
-
|
|
1608
|
-
func validate(input string) error {
|
|
1609
|
-
if input == "" {
|
|
1610
|
-
return errors.New("empty")
|
|
1611
|
-
}
|
|
1612
|
-
return nil
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
func process(input string) error {
|
|
1616
|
-
err := validate(input)
|
|
1617
|
-
if err != nil {
|
|
1618
|
-
return handleError(err)
|
|
1619
|
-
}
|
|
1620
|
-
return doWork(input)
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
func handleError(err error) error {
|
|
1624
|
-
return err
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
func doWork(input string) error {
|
|
1628
|
-
return nil
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
func main() {
|
|
1632
|
-
process("test")
|
|
1633
|
-
}
|
|
1634
|
-
`});
|
|
1635
|
-
try {
|
|
1636
|
-
const index = idx(d);
|
|
1637
|
-
const def = index.symbols.get('process')?.[0];
|
|
1638
|
-
const callees = index.findCallees(def);
|
|
1639
|
-
// All three are direct calls — should be detected
|
|
1640
|
-
assert.ok(callees.some(c => c.name === 'validate'), 'validate() detected');
|
|
1641
|
-
assert.ok(callees.some(c => c.name === 'handleError'), 'handleError() detected');
|
|
1642
|
-
assert.ok(callees.some(c => c.name === 'doWork'), 'doWork() detected');
|
|
1643
|
-
} finally { rm(d); }
|
|
1644
|
-
});
|
|
1645
|
-
|
|
1646
|
-
it('LIMITATION: Python dict dispatch pattern', () => {
|
|
1647
|
-
const d = tmp({
|
|
1648
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
1649
|
-
'dispatch.py': `
|
|
1650
|
-
def handle_get(req):
|
|
1651
|
-
return {'status': 200}
|
|
1652
|
-
|
|
1653
|
-
def handle_post(req):
|
|
1654
|
-
return {'status': 201}
|
|
1655
|
-
|
|
1656
|
-
def handle_delete(req):
|
|
1657
|
-
return {'status': 204}
|
|
1658
|
-
|
|
1659
|
-
HANDLERS = {
|
|
1660
|
-
'GET': handle_get,
|
|
1661
|
-
'POST': handle_post,
|
|
1662
|
-
'DELETE': handle_delete,
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
def dispatch(method, req):
|
|
1666
|
-
handler = HANDLERS.get(method)
|
|
1667
|
-
if handler:
|
|
1668
|
-
return handler(req)
|
|
1669
|
-
`});
|
|
1670
|
-
try {
|
|
1671
|
-
const index = idx(d);
|
|
1672
|
-
const def = index.symbols.get('dispatch')?.[0];
|
|
1673
|
-
const callees = index.findCallees(def);
|
|
1674
|
-
// handler(req) calls through dict lookup — can't trace
|
|
1675
|
-
assert.ok(!callees.some(c => c.name === 'handle_get'),
|
|
1676
|
-
'Dict dispatch: handler from HANDLERS.get() — cannot trace');
|
|
1677
|
-
// But deadcode correctly sees the identifiers in the dict literal
|
|
1678
|
-
const dead = index.deadcode({ includeExported: true });
|
|
1679
|
-
assert.ok(!dead.some(d => d.name === 'handle_get'),
|
|
1680
|
-
'deadcode: handle_get referenced in dict literal — not reported dead');
|
|
1681
|
-
} finally { rm(d); }
|
|
1682
|
-
});
|
|
1683
|
-
|
|
1684
|
-
it('LIMITATION: JS optional chaining on function call', () => {
|
|
1685
|
-
const d = tmp({
|
|
1686
|
-
'package.json': '{"name":"t"}',
|
|
1687
|
-
'lib.js': `
|
|
1688
|
-
function parse(input) { return input.split(','); }
|
|
1689
|
-
function process(obj) {
|
|
1690
|
-
return obj?.transform?.(obj.data);
|
|
1691
|
-
}
|
|
1692
|
-
module.exports = { parse, process };
|
|
1693
|
-
`});
|
|
1694
|
-
try {
|
|
1695
|
-
const index = idx(d);
|
|
1696
|
-
const def = index.symbols.get('process')?.[0];
|
|
1697
|
-
const callees = index.findCallees(def);
|
|
1698
|
-
// obj?.transform?.() — method call with optional chaining, filtered
|
|
1699
|
-
assert.strictEqual(callees.length, 0,
|
|
1700
|
-
'Optional chaining method call — uncertain and method filtered');
|
|
1701
|
-
} finally { rm(d); }
|
|
1702
|
-
});
|
|
1703
|
-
|
|
1704
|
-
it('LIMITATION: Rust closure passed to iterator', () => {
|
|
1705
|
-
const d = tmp({
|
|
1706
|
-
'Cargo.toml': '[package]\nname = "t"\nversion = "0.1.0"\nedition = "2021"',
|
|
1707
|
-
'src/main.rs': `
|
|
1708
|
-
fn transform(x: i32) -> i32 {
|
|
1709
|
-
x * 2
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
fn process(items: Vec<i32>) -> Vec<i32> {
|
|
1713
|
-
items.iter().map(|x| transform(*x)).collect()
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
fn main() {
|
|
1717
|
-
let items = vec![1, 2, 3];
|
|
1718
|
-
process(items);
|
|
1719
|
-
}
|
|
1720
|
-
`});
|
|
1721
|
-
try {
|
|
1722
|
-
const index = idx(d);
|
|
1723
|
-
const def = index.symbols.get('process')?.[0];
|
|
1724
|
-
const callees = index.findCallees(def);
|
|
1725
|
-
// transform() inside closure — detected via line-range containment
|
|
1726
|
-
assert.ok(callees.some(c => c.name === 'transform'),
|
|
1727
|
-
'Call inside closure detected via line-range containment');
|
|
1728
|
-
} finally { rm(d); }
|
|
1729
|
-
});
|
|
1730
|
-
});
|
|
1731
|
-
|
|
1732
|
-
// ============================================================================
|
|
1733
|
-
// 16. SCOPE AND BINDING EDGE CASES
|
|
1734
|
-
// ============================================================================
|
|
1735
|
-
|
|
1736
|
-
describe('16. Scope and Binding Edge Cases', () => {
|
|
1737
|
-
|
|
1738
|
-
it('LIMITATION: block-scoped redeclaration — not modeled', () => {
|
|
1739
|
-
const d = tmp({
|
|
1740
|
-
'package.json': '{"name":"t"}',
|
|
1741
|
-
'lib.js': `
|
|
1742
|
-
function helper() { return 'module'; }
|
|
1743
|
-
function main() {
|
|
1744
|
-
if (true) {
|
|
1745
|
-
function helper() { return 'block'; }
|
|
1746
|
-
helper();
|
|
1747
|
-
}
|
|
1748
|
-
helper();
|
|
1749
|
-
}
|
|
1750
|
-
module.exports = { helper, main };
|
|
1751
|
-
`});
|
|
1752
|
-
try {
|
|
1753
|
-
const index = idx(d);
|
|
1754
|
-
const callers = index.findCallers('helper');
|
|
1755
|
-
// Two bindings for 'helper' — scope disambiguation resolves calls
|
|
1756
|
-
assert.ok(callers.length >= 1,
|
|
1757
|
-
'At least some calls to helper are resolved despite block-scope complexity');
|
|
1758
|
-
} finally { rm(d); }
|
|
1759
|
-
});
|
|
1760
|
-
|
|
1761
|
-
it('LIMITATION: Python nonlocal/global — scope not modeled', () => {
|
|
1762
|
-
const d = tmp({
|
|
1763
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
1764
|
-
'lib.py': `
|
|
1765
|
-
counter = 0
|
|
1766
|
-
|
|
1767
|
-
def increment():
|
|
1768
|
-
global counter
|
|
1769
|
-
counter += 1
|
|
1770
|
-
|
|
1771
|
-
def get_count():
|
|
1772
|
-
return counter
|
|
1773
|
-
|
|
1774
|
-
def process():
|
|
1775
|
-
increment()
|
|
1776
|
-
return get_count()
|
|
1777
|
-
`});
|
|
1778
|
-
try {
|
|
1779
|
-
const index = idx(d);
|
|
1780
|
-
const def = index.symbols.get('process')?.[0];
|
|
1781
|
-
const callees = index.findCallees(def);
|
|
1782
|
-
// Direct calls detected, but no data flow tracking for shared state
|
|
1783
|
-
assert.ok(callees.some(c => c.name === 'increment'),
|
|
1784
|
-
'Direct call to increment() detected');
|
|
1785
|
-
assert.ok(callees.some(c => c.name === 'get_count'),
|
|
1786
|
-
'Direct call to get_count() detected');
|
|
1787
|
-
} finally { rm(d); }
|
|
1788
|
-
});
|
|
1789
|
-
|
|
1790
|
-
it('LIMITATION: Go goroutine launch — function ref not always detected', () => {
|
|
1791
|
-
const d = tmp({
|
|
1792
|
-
'go.mod': 'module test\ngo 1.21',
|
|
1793
|
-
'main.go': `package main
|
|
1794
|
-
|
|
1795
|
-
func worker(id int) {
|
|
1796
|
-
// do work
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
func main() {
|
|
1800
|
-
for i := 0; i < 10; i++ {
|
|
1801
|
-
go worker(i)
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
`});
|
|
1805
|
-
try {
|
|
1806
|
-
const index = idx(d);
|
|
1807
|
-
const def = index.symbols.get('main')?.[0];
|
|
1808
|
-
const callees = index.findCallees(def);
|
|
1809
|
-
// go worker(i) — go_statement wraps a call_expression
|
|
1810
|
-
assert.ok(callees.some(c => c.name === 'worker'),
|
|
1811
|
-
'go worker(i) — call detected inside go_statement');
|
|
1812
|
-
} finally { rm(d); }
|
|
1813
|
-
});
|
|
1814
|
-
|
|
1815
|
-
it('LIMITATION: Java lambda — call inside lambda body', () => {
|
|
1816
|
-
const d = tmp({
|
|
1817
|
-
'pom.xml': '<project><modelVersion>4.0.0</modelVersion><groupId>t</groupId><artifactId>t</artifactId><version>1</version></project>',
|
|
1818
|
-
'App.java': `
|
|
1819
|
-
import java.util.List;
|
|
1820
|
-
import java.util.stream.Collectors;
|
|
1821
|
-
|
|
1822
|
-
public class App {
|
|
1823
|
-
public static String transform(String s) {
|
|
1824
|
-
return s.toUpperCase();
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
public static List<String> process(List<String> items) {
|
|
1828
|
-
return items.stream()
|
|
1829
|
-
.map(s -> transform(s))
|
|
1830
|
-
.collect(Collectors.toList());
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
`});
|
|
1834
|
-
try {
|
|
1835
|
-
const index = idx(d);
|
|
1836
|
-
const def = index.symbols.get('process')?.[0];
|
|
1837
|
-
const callees = index.findCallees(def);
|
|
1838
|
-
// transform(s) inside lambda — detected via line-range containment
|
|
1839
|
-
assert.ok(callees.some(c => c.name === 'transform'),
|
|
1840
|
-
'Call inside Java lambda detected via line-range containment');
|
|
1841
|
-
} finally { rm(d); }
|
|
1842
|
-
});
|
|
1843
|
-
|
|
1844
|
-
it('PASS: Python list comprehension calls detected', () => {
|
|
1845
|
-
const d = tmp({
|
|
1846
|
-
'pyproject.toml': '[project]\nname = "t"',
|
|
1847
|
-
'lib.py': `
|
|
1848
|
-
def transform(x):
|
|
1849
|
-
return x * 2
|
|
1850
|
-
|
|
1851
|
-
def process(items):
|
|
1852
|
-
return [transform(x) for x in items]
|
|
1853
|
-
`});
|
|
1854
|
-
try {
|
|
1855
|
-
const index = idx(d);
|
|
1856
|
-
const def = index.symbols.get('process')?.[0];
|
|
1857
|
-
const callees = index.findCallees(def);
|
|
1858
|
-
// transform(x) inside list comprehension — valid call_expression
|
|
1859
|
-
assert.ok(callees.some(c => c.name === 'transform'),
|
|
1860
|
-
'Call inside list comprehension detected — it IS a call_expression');
|
|
1861
|
-
} finally { rm(d); }
|
|
1862
|
-
});
|
|
1863
|
-
});
|