Use same-file module variable initializers to filter builtin collection calls before class-name heuristics, and require a Python type node for the class escape. Preserve imported project module calls. Add callers/callees regressions for dict, list, set, tuple, and frozenset, with a real instance control and same-name bindings across files. Validation on Linux with Node 22.19.0: the new suite had 14 failures and one passing control on main at 8733c288; all 216 tests in the new suite, call-receiver-no-fabrication, and resolution now pass. TypeScript and copy-assets pass. Re-indexed /tmp/cg-1652-repro: callers get is empty and read_setting no longer calls a cache.py method. Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
|
|
const collections = [
|
|
{ name: 'dict_literal', value: '{"answer": "42"}', method: 'get' },
|
|
{ name: 'empty_dict', value: '{}', method: 'get' },
|
|
{ name: 'dict_constructor', value: 'dict()', method: 'get' },
|
|
{ name: 'list_literal', value: '[1]', method: 'append' },
|
|
{ name: 'empty_list', value: '[]', method: 'append' },
|
|
{ name: 'list_constructor', value: 'list()', method: 'append' },
|
|
{ name: 'set_literal', value: '{1}', method: 'add' },
|
|
{ name: 'set_constructor', value: 'set()', method: 'add' },
|
|
{ name: 'tuple_literal', value: '(1,)', method: 'index' },
|
|
{ name: 'empty_tuple', value: '()', method: 'index' },
|
|
{ name: 'tuple_constructor', value: 'tuple()', method: 'index' },
|
|
{ name: 'frozenset_constructor', value: 'frozenset()', method: 'union' },
|
|
];
|
|
|
|
let dir: string;
|
|
let cg: CodeGraph;
|
|
|
|
beforeAll(async () => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1652-'));
|
|
fs.writeFileSync(path.join(dir, 'settings.py'), `DEFAULTS = {"answer": "42"}
|
|
|
|
def read_setting(name):
|
|
return DEFAULTS.get(name, None)
|
|
`);
|
|
fs.writeFileSync(path.join(dir, 'cache.py'), `class LRUCache:
|
|
def __init__(self):
|
|
self._store = {}
|
|
|
|
def get(self, key):
|
|
return self._store.get(key)
|
|
|
|
class ProjectCollection:
|
|
def append(self, item):
|
|
pass
|
|
def add(self, item):
|
|
pass
|
|
def index(self, item):
|
|
return 0
|
|
def union(self, item):
|
|
return self
|
|
`);
|
|
for (const { name, value, method } of collections) {
|
|
// Capitalizing lRUCache matches a real class. Its name must not override
|
|
// the same-file binding's collection initializer (#1652).
|
|
fs.writeFileSync(path.join(dir, `${name}.py`), `lRUCache = ${value}
|
|
|
|
def use_${name}(item):
|
|
return lRUCache.${method}(item)
|
|
`);
|
|
}
|
|
fs.writeFileSync(path.join(dir, 'unknown.py'), `UNKNOWN = load_defaults()
|
|
|
|
def read_unknown(name):
|
|
return UNKNOWN.get(name)
|
|
`);
|
|
fs.writeFileSync(path.join(dir, 'client.py'), `from cache import LRUCache
|
|
|
|
def read_cache(lRUCache: LRUCache, name):
|
|
return lRUCache.get(name)
|
|
`);
|
|
cg = await CodeGraph.init(dir, { index: true });
|
|
});
|
|
|
|
afterAll(() => {
|
|
cg?.destroy();
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
function expectNoMethodCall(callerName: string, file: string, methodName: string) {
|
|
const caller = cg.getNodesByName(callerName).find((n) => n.kind === 'function' && n.filePath === file);
|
|
const method = cg.getNodesByName(methodName).find((n) => n.kind === 'method' && n.filePath === 'cache.py');
|
|
expect(caller).toBeDefined();
|
|
expect(method).toBeDefined();
|
|
expect(cg.getCallers(method!.id).map(({ node }) => node.id)).not.toContain(caller!.id);
|
|
expect(cg.getCallees(caller!.id).map(({ node }) => node.id)).not.toContain(method!.id);
|
|
}
|
|
|
|
describe('Python module-scope collection methods (#1652)', () => {
|
|
it('does not connect DEFAULTS.get to the unrelated LRUCache.get method', () => {
|
|
expectNoMethodCall('read_setting', 'settings.py', 'get');
|
|
});
|
|
|
|
it.each(collections)('keeps $name ($method) external even when the receiver resembles a class', ({ name, method }) => {
|
|
expectNoMethodCall(`use_${name}`, `${name}.py`, method);
|
|
});
|
|
|
|
it('does not treat an unrelated variable as evidence of a project class', () => {
|
|
expectNoMethodCall('read_unknown', 'unknown.py', 'get');
|
|
});
|
|
|
|
it('preserves real instance calls despite same-named collections in other files', () => {
|
|
const caller = cg.getNodesByName('read_cache').find((n) => n.kind === 'function')!;
|
|
const method = cg.getNodesByName('get').find((n) => n.kind === 'method' && n.filePath === 'cache.py')!;
|
|
expect(caller).toBeDefined();
|
|
expect(method).toBeDefined();
|
|
expect(cg.getCallees(caller.id).map(({ node }) => node.id)).toContain(method.id);
|
|
expect(cg.getCallers(method.id).map(({ node }) => node.id)).toContain(caller.id);
|
|
});
|
|
});
|