Files
codegraph/__tests__/python-self-call-not-import.test.ts
Colby Mchenry a4bd890ec6 fix(python): a call on self is never resolved through a same-named import (#2290)
The Python extractor records `self.get_ip(request)` by its bare name, so the
import strategy matched it to a function the file imports as `get_ip` and
the call never reached the class's own (or inherited) method. A bare-named
Python call written on `self` now skips import resolution; the method
strategies then find it. A plain `get_ip(request)` still binds to the import.

Across allauth, DRF, netbox and mealie this retargets 16 calls, all from an
imported helper to the class's own method; no edge is added or lost.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:42:23 +00:00

95 lines
3.3 KiB
TypeScript

/**
* A Python call written on the instance, `self.get_ip(request)`, is a method
* call even when the file also imports a function named `get_ip`. The
* extractor records it by its bare name, and the import strategy claimed it:
* django-allauth's `DefaultAccountAdapter.send_notification_mail` calling
* `self.get_client_ip(self.request)` was linked to `httpkit.get_client_ip`,
* which `adapter.py` imports, instead of the adapter's own method.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
describe('Python self-calls do not resolve through a same-named import', () => {
const dirs: string[] = [];
let cg: CodeGraph | undefined;
afterEach(() => {
cg?.close();
cg = undefined;
for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
});
async function callsIn(files: Record<string, string>): Promise<string[]> {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-py-self-'));
dirs.push(dir);
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
fs.writeFileSync(path.join(dir, rel), content);
}
cg = await CodeGraph.init(dir, { index: true });
const rows = (cg as any).db.db
.prepare(
`SELECT s.qualified_name s, t.qualified_name t, t.file_path f FROM edges e
JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target WHERE e.kind = 'calls'`,
)
.all() as { s: string; t: string; f: string }[];
return rows.map((r) => `${r.s} -> ${r.t} (${r.f})`).sort();
}
const httpkit = 'def get_ip(request):\n return "1.2.3.4"\n';
it('a method calling its own same-named method through self', async () => {
expect(
await callsIn({
'pkg/__init__.py': '',
'pkg/httpkit.py': httpkit,
'pkg/adapter.py': [
'from pkg.httpkit import get_ip',
'',
'',
'class Adapter:',
' def get_ip(self, request):',
' return get_ip(request)',
'',
' def notify(self, request):',
' return self.get_ip(request)',
'',
].join('\n'),
}),
).toEqual([
// The bare call is still the imported function.
'Adapter::get_ip -> get_ip (pkg/httpkit.py)',
'Adapter::notify -> Adapter::get_ip (pkg/adapter.py)',
]);
});
it('a method a base class in another file declares', async () => {
expect(
await callsIn({
'pkg/__init__.py': '',
'pkg/httpkit.py': httpkit,
'pkg/base.py': 'class Base:\n def get_ip(self, request):\n return "base"\n',
'pkg/adapter.py': [
'from pkg.base import Base',
'from pkg.httpkit import get_ip',
'',
'',
'class Adapter(Base):',
' def notify(self, request):',
' return self.get_ip(request)',
'',
' def raw(self, request):',
' return get_ip(request)',
'',
].join('\n'),
}),
).toEqual([
'Adapter::notify -> Base::get_ip (pkg/base.py)',
'Adapter::raw -> get_ip (pkg/httpkit.py)',
]);
});
});