mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-03 22:57:40 +08:00
Found by the README-wide sweep — Django/Flask/FastAPI apps' most-depended-on lists were test and serializer methods: netbox `UserConfig.all` (3,610 from `X.objects.all()`), `PortSerializer.create` (2,093 from `.objects.create`), a test-local `FakeQuerySet.count` (1,910); healthchecks `AuthTestCase.get` (880 from `Channel.objects.get()`), `ProfileAdmin.login` (702 from `self.client.login()`); mealie `_FakeHTTPResponse.json`. Python keeps a call through a receiver it cannot name (`User.objects.get`, `self.client.login`, `request.POST.get`) as the bare `get` / `login`, and exact-name matching then took any class's method by proximity. The call's shape is now read at its column (the call's start): - `bare` (`get(1)`): not a method — Python has no implicit self — and not a project symbol when the file takes that name `from` a module the project doesn't contain (`from django.shortcuts import render`); - `chained`: only a member of what the chain names last — a method of a class of that name (case/underscores ignored: `self.store` → `Store`, `self.user_service` → `UserService`) or a function/class in a module of that name (`app.helpers.slugify()` → helpers.py); - `self.x()` / `cls.x()`, and chains split across lines: unchanged. Fuzzy matching is case-exact for Python (`dir(…)` ≠ class `Dir`). Lexical reachability now walks every enclosing scope, so a method of a class declared inside a function is only reachable in there (memoized per candidate). A/B vs main (edge diffs, every change classified): netbox −18,091/+73 (indexes 8s → 5s), healthchecks −3,354/+361, pytest −2,950/+105, mealie −1,802/+6, allauth −1,086/+57, DRF −943/+50, flask −167/+17, httpx −147/+24. Gains are precise member links (`self.channel.notify` → Channel.notify, `item.stash.get` → Stash.get, `config.cache.get` → Cache.get, `self.provider.verify_token`); the one deliberate loss class is calls through an instance whose type needs inference (`flask.g.setdefault` in flask's own repo). excalidraw, typeorm, gson, okio, AutoMapper, gin and hono byte-identical. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
3.1 KiB
TypeScript
90 lines
3.1 KiB
TypeScript
/**
|
|
* A Python call whose receiver the extractor could not keep — `User.objects.get(…)`,
|
|
* `self.client.login(…)` reach the resolver as `get`, `login` — is not a project
|
|
* method picked by name. netbox bound 3,610 `.all()` calls to one `UserConfig.all`,
|
|
* healthchecks 880 `objects.get` to a test case's `get`. A bare `get(1)` cannot be a
|
|
* method either (no implicit self); `self.helper()` still resolves to the class's own.
|
|
*/
|
|
import { describe, it, expect, afterAll } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
|
|
const roots: string[] = [];
|
|
afterAll(() => {
|
|
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
|
|
});
|
|
|
|
const FILES: Record<string, string> = {
|
|
'app/tests.py': `class AuthTestCase:
|
|
def get(self, url):
|
|
return url
|
|
|
|
def login(self, name):
|
|
return name
|
|
`,
|
|
'app/curl.py': `def get(url):
|
|
return url
|
|
`,
|
|
'app/helpers.py': `def slugify(text):
|
|
return text
|
|
`,
|
|
'app/tags.py': `def render(context):
|
|
return context
|
|
`,
|
|
'app/pages.py': `import app.helpers
|
|
from django.shortcuts import render
|
|
|
|
def page(request):
|
|
kind = request.POST.get("kind")
|
|
slug = app.helpers.slugify(kind)
|
|
return render(request, "page.html", {"slug": slug})
|
|
`,
|
|
'app/views.py': `from django.contrib.auth.models import User
|
|
|
|
class Backend:
|
|
def authenticate(self, username):
|
|
user = User.objects.get(email=username)
|
|
self.client.login(username="a")
|
|
self.helper()
|
|
self.assertEqual(self.filterset(params, self.queryset).qs.get(), 2)
|
|
get(1)
|
|
return user
|
|
|
|
def helper(self):
|
|
return 1
|
|
`,
|
|
};
|
|
|
|
describe('Python: a method is reached through a receiver', () => {
|
|
it('not by its name alone', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-py-receiver-'));
|
|
roots.push(root);
|
|
for (const [rel, content] of Object.entries(FILES)) {
|
|
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, rel), content);
|
|
}
|
|
const cg = await CodeGraph.init(root, { index: true });
|
|
try {
|
|
const authenticate = cg.getNodesByName('authenticate')[0]!;
|
|
const targets = cg
|
|
.getOutgoingEdgesFrom([authenticate.id], ['calls'])
|
|
.map((e) => cg.getNode(e.target)!.qualifiedName);
|
|
expect(targets).not.toContain('AuthTestCase::get');
|
|
expect(targets).not.toContain('AuthTestCase::login');
|
|
expect(targets).toContain('Backend::helper');
|
|
const page = cg.getNodesByName('page')[0]!;
|
|
const fromPage = cg.getOutgoingEdgesFrom([page.id], ['calls']).map((e) => cg.getNode(e.target)!);
|
|
const pageTargets = fromPage.map((n) => `${n.filePath}:${n.name}`);
|
|
// \`request.POST.get\` names no project module; \`render\` is Django's.
|
|
expect(pageTargets).not.toContain('app/curl.py:get');
|
|
expect(pageTargets).not.toContain('app/tags.py:render');
|
|
// A module path names the module's function.
|
|
expect(pageTargets).toContain('app/helpers.py:slugify');
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
});
|