mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
Python calls through an attribute reach the method the code gives that attribute: `self.sink.stop()` follows `self.sink = Sink()`, a `sink: Sink` annotation, a typed `__init__` parameter, a factory's return type or what every `return` in it builds, and a base class's `__init__` in another file. Calls straight on a factory's result (`Client.from_env().send()`) resolve the same way, and a sync re-opens these answers when the factory or base they were read from changes. The native Python extractor now keeps a call's receiver text as the wasm one does, so `self.data.append(1)` is never a bare `append` that matches an unrelated project function. This includes all of PR 1704. Reworked from evidence-or-silence, which dropped about 3,000 correct Django links (`self.client.get()`, `self.engine.render_to_string()`). The reader now has three answers. A project class that has the method: that method. Evidence that rules project methods out: no edge, for a builtin, a class from outside the project built in a method or named by an annotation, an attribute only an outside base can give (Django's `TestCase` in an app), or a class without the method whose bases are all known. Nothing typed: the call is guessed by the name of its last link, read as written, as any receiver is, but only for an owner whose name holds the link's head noun (`self.session` for `SessionBase`, not `database.group_shopping_lists` for `GroupRepositoryAIProvider`) and never as the one method of a name; the guess records the files the reader looked at, so a sync re-opens it too. A class-body field an outside class builds (`author = models.ForeignKey(Author)`) may be a descriptor, so it is no evidence. A chain through an imported module (`os.path.exists()`) is the module's. In the shared scoring, a receiver that spells an owner's whole name (`client` for `Client`) outscores one that only shares its word (`RedisCacheClient`). A call through an imported value's attribute chain (`app.jinja_env.filters.keys()`) no longer links to the value's `jinja_env`. A/B against main with fresh indexes, every changed edge triaged, sites matched by position since the extractor now names these calls by their whole chain. Django: 3,317 calls linked that were not (`self.client.force_login()` to `ClientMixin`, `User.objects.create_user()` to `UserManager`, `self.stdout.write()` to `OutputWrapper`), 127 moved, nearly all to a more precise target (`self.site.register()` to `AdminSite.register` rather than the `register` decorator), and 134 removed: main's bare-name links on builtin values (`value.lower()` to the template filter, `.encode()` to a test password hasher) and calls on expressions such as `(q1 | q2).order_by()`, which now go unresolved as they do in the wasm engine. flask: 32 linked, 4 moved to `JSONProvider`. fastapi: 58 linked. mealie: 740 linked. Guesses still wrong, as main makes them for a plain receiver of the same name: fastapi's `request.headers.get()` to a telemetry `_HeadersGetter`, and about 20 of mealie's repository attributes to a controller. Go, C#, Ruby, Swift, Kotlin, Java and TS repositories are byte-identical, except excalidraw's 5 `props.onChange()` calls, which move between two unrelated `Props` interfaces. Indexing CPU: Django +7%, mealie +5%, fastapi flat; wall time unchanged within noise. Full suite on the merged tree: 7,999 passed, 0 failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
/**
|
|
* Python non-identifier call receivers must not fabricate edges — ON THE ARM
|
|
* THAT SHIPS.
|
|
*
|
|
* `resolution.test.ts` already covers this end-to-end, but python is in the
|
|
* kernel's DEFAULT_ROUTED set, so in a from-source checkout (no staged .node)
|
|
* that test exercises the *wasm* TreeSitterExtractor while every published
|
|
* bundle extracts python with codegraph-kernel/src/python.rs. A fix applied to
|
|
* only one arm is a green test over code that never runs.
|
|
*
|
|
* This suite forces the kernel arm and asserts the same two properties:
|
|
* - `ledger.append(row)`, where `ledger` is an imported project module that
|
|
* exports `append`, RESOLVES (the false-negative half, #66).
|
|
* - `self.data.append(...)` / `rows[k].append(...)` — attribute-chain and
|
|
* subscript receivers — do NOT fabricate an edge to that same `append`
|
|
* (the false-positive half; `python.rs` used to emit a BARE `append`,
|
|
* which exact-matched the only project-wide symbol of that name).
|
|
*
|
|
* Skips when no kernel binary is staged, like the parity suites.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src/index';
|
|
|
|
const KERNEL_PATH = path.join(
|
|
__dirname,
|
|
'..',
|
|
'codegraph-kernel',
|
|
'prebuilds',
|
|
`${process.platform}-${process.arch}`,
|
|
'codegraph-kernel.node'
|
|
);
|
|
const kernelBuilt = fs.existsSync(KERNEL_PATH);
|
|
|
|
describe.skipIf(!kernelBuilt)('python call-receiver fabrication (kernel arm)', () => {
|
|
let tempDir: string;
|
|
let cg: CodeGraph | undefined;
|
|
let savedLangs: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
savedLangs = process.env.CODEGRAPH_KERNEL_LANGS;
|
|
process.env.CODEGRAPH_KERNEL_LANGS = 'python';
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-py-fab-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
cg?.close();
|
|
cg = undefined;
|
|
if (savedLangs === undefined) delete process.env.CODEGRAPH_KERNEL_LANGS;
|
|
else process.env.CODEGRAPH_KERNEL_LANGS = savedLangs;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('resolves a module-qualified call and fabricates none from chained receivers (#66)', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tempDir, 'ledger.py'),
|
|
'def append(row):\n return True\n\n\ndef path():\n return "ledger.jsonl"\n'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tempDir, 'record.py'),
|
|
'from . import ledger\n\n\ndef add_outcome(row):\n if not ledger.append(row):\n return None\n return ledger.path()\n'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tempDir, 'unrelated.py'),
|
|
'class Box:\n def __init__(self):\n self.data = []\n\n def build_map(self):\n self.data.append({"x": 1})\n rows = {}\n rows["k"] = []\n rows["k"].append(2)\n return rows\n'
|
|
);
|
|
|
|
cg = await CodeGraph.init(tempDir, { index: true });
|
|
|
|
const ledgerAppend = cg
|
|
.getNodesByKind('function')
|
|
.find((n) => n.name === 'append' && n.filePath.replace(/\\/g, '/').endsWith('ledger.py'));
|
|
expect(ledgerAppend).toBeDefined();
|
|
|
|
const addOutcome = cg.getNodesByKind('function').find((n) => n.name === 'add_outcome');
|
|
expect(addOutcome).toBeDefined();
|
|
expect(
|
|
cg
|
|
.getOutgoingEdges(addOutcome!.id)
|
|
.filter((e) => e.kind === 'calls')
|
|
.map((e) => e.target)
|
|
).toContain(ledgerAppend!.id);
|
|
|
|
// Both non-identifier receivers live in build_map; neither may reach
|
|
// ledger.py's append.
|
|
const buildMap = cg.getNodesByKind('method').find((n) => n.name === 'build_map');
|
|
expect(buildMap).toBeDefined();
|
|
expect(
|
|
cg
|
|
.getOutgoingEdges(buildMap!.id)
|
|
.filter((e) => e.kind === 'calls')
|
|
.map((e) => e.target)
|
|
).not.toContain(ledgerAppend!.id);
|
|
});
|
|
});
|