fix(resolution): resolve module-qualified calls colliding with builtin methods (#1749)

isBuiltInOrExternal treated ledger.append as list.append unless the receiver
matched a known class, so real module exports never reached resolveViaImport.
Allow project-module receivers (verified via resolveImportPath) through while
keeping stdlib/PyPI silent. Completes #1681 after #1748 fixed the FP half.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 00:27:27 -05:00
committed by GitHub
co-authored by Colby McHenry
parent bb1d3093eb
commit edcd36e5f0
3 changed files with 102 additions and 3 deletions
+54
View File
@@ -1504,6 +1504,60 @@ def external_caller():
expect(externalCalls).toHaveLength(0);
});
it('resolves a module-qualified call to a function whose name collides with a builtin collection method, and does not fabricate one from an unrelated chained receiver (#1681)', async () => {
// `ledger.append(row)` (module imported, method name `append`) previously
// never reached resolution: isBuiltInOrExternal's Python built-in-method
// filter treated ANY `x.append(...)` as `list.append` unless `X` matched a
// known CLASS, so a real MODULE export named `append` was dropped before
// resolveViaImport ever ran. Separately, `d.setdefault(k, []).append(x)` —
// a non-identifier (call-chain) receiver — used to degrade at extraction
// to a BARE `append` ref and exact-match ledger.append (#1683/#1748 fixed
// that half; assert both directions here).
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
def add_outcome(row):
if not ledger.append(row):
return None
return ledger.path()
`
);
fs.writeFileSync(
path.join(tempDir, 'unrelated.py'),
`def build_map():
rows_by_file = {}
rows_by_file.setdefault("f", []).append({"x": 1})
return rows_by_file
`
);
cg = await CodeGraph.init(tempDir, { index: true });
const ledgerAppend = cg
.getNodesByKind('function')
.find((n) => n.name === 'append' && n.filePath.replace(/\\/g, '/') === 'ledger.py');
expect(ledgerAppend).toBeDefined();
// The real, import-qualified call must resolve.
const addOutcome = cg.getNodesByKind('function').find((n) => n.name === 'add_outcome');
expect(addOutcome).toBeDefined();
const addOutcomeCalls = cg.getOutgoingEdges(addOutcome!.id).filter((e) => e.kind === 'calls');
expect(addOutcomeCalls.map((e) => e.target)).toContain(ledgerAppend!.id);
// The unrelated dict/list `.append()` on a chained receiver must NOT
// fabricate an edge to ledger.py's append.
const buildMap = cg.getNodesByKind('function').find((n) => n.name === 'build_map');
expect(buildMap).toBeDefined();
const buildMapCalls = cg.getOutgoingEdges(buildMap!.id).filter((e) => e.kind === 'calls');
expect(buildMapCalls.map((e) => e.target)).not.toContain(ledgerAppend!.id);
});
it('attaches Go methods to their receiver type across files (#583, cross-file half)', async () => {
// In Go a type's methods are commonly declared in a different file from the
// `type` declaration (`type Box` in box.go, `func (b *Box) Get()` in