mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-22 09:35:22 +08:00
fix(resolution): resolve Python module members through an aliased from-import (#1626)
resolvePythonModuleMember rebuilt the submodule's dotted path by joining the import source with the LOCAL name. Under 'from pkg import mod as alias' that produces 'pkg.alias' — a module that does not exist — so the file lookup found nothing and the call fell through to unresolved_refs with status='failed'. codegraph_callers then reported the target as having fewer callers than it does, which is the same wrong 'is this dead code?' answer #578 produced for the unaliased form. Join with the exported name instead. For an unaliased import the two names are identical, so nothing changes there; '*' (the namespace form) keeps using the local name, which is what it already bound to. Scope note: the issue also reports 'import top as alias' failing. That form is a namespace import and binds at source, so it resolves on current main — a probe against the reverted resolver confirms it already produces its call edge. The regression test pins both halves so the working one cannot silently break. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -242,6 +242,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
- Fixed a long-running `codegraph ui` session serving a symbol that a sync had already deleted. The viewer keeps one connection to your index open, and its in-memory lookup didn't notice when another process — your agent's sync, or `codegraph sync` — rewrote the file underneath it, so a symbol screen could keep showing a body with no callers while search correctly reported it had moved. Because a symbol's identity includes the line it starts on, this happened after almost any edit above it.
|
||||
|
||||
- Python calls made through a renamed import are no longer missing from the graph. After `from package import module as alias`, a call like `alias.func()` produced no call edge, so `codegraph_callers` could report a function as uncalled while a live caller reached it under the alias — the same wrong answer to "is this dead code?" that the unaliased form used to give. Thanks @JoeyNPP. (#1626)
|
||||
|
||||
## [1.6.0] - 2026-08-26
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -1558,6 +1558,56 @@ def add_outcome(row):
|
||||
expect(buildMapCalls.map((e) => e.target)).not.toContain(ledgerAppend!.id);
|
||||
});
|
||||
|
||||
it('resolves Python module-attribute calls through an ALIASED import (#1626)', async () => {
|
||||
// #715 taught resolvePythonModuleMember to fall back to a dotted-module
|
||||
// file lookup, which fixed `from pkg import module` (#578). The aliased
|
||||
// form still missed: the module path was rebuilt from the LOCAL name, so
|
||||
// `from pkg import module as alias` looked for `pkg.alias` — a file that
|
||||
// does not exist — and the call landed in unresolved_refs. The plain
|
||||
// `import top as alias` form is a namespace import and binds at `source`,
|
||||
// so it was already correct; it is pinned here so the fix can't regress it.
|
||||
fs.mkdirSync(path.join(tempDir, 'pkg'));
|
||||
fs.writeFileSync(path.join(tempDir, 'pkg', '__init__.py'), '');
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'pkg', 'module.py'),
|
||||
'def func():\n return 1\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'top_level.py'),
|
||||
'def top_func():\n return 2\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.py'),
|
||||
`from pkg import module as mod_alias
|
||||
import top_level as tl
|
||||
|
||||
|
||||
def from_import_caller():
|
||||
return mod_alias.func()
|
||||
|
||||
|
||||
def plain_import_caller():
|
||||
return tl.top_func()
|
||||
`
|
||||
);
|
||||
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
|
||||
const fromImportCaller = cg.getNodesByKind('function').filter((n) => n.name === 'from_import_caller')[0];
|
||||
expect(fromImportCaller).toBeDefined();
|
||||
const aliasCalls = cg.getOutgoingEdges(fromImportCaller!.id).filter((e) => e.kind === 'calls');
|
||||
expect(aliasCalls).toHaveLength(1);
|
||||
const aliasTarget = cg.getNode(aliasCalls[0]!.target);
|
||||
expect(aliasTarget?.name).toBe('func');
|
||||
expect(aliasTarget?.filePath.replace(/\\/g, '/')).toBe('pkg/module.py');
|
||||
|
||||
const plainCaller = cg.getNodesByKind('function').filter((n) => n.name === 'plain_import_caller')[0];
|
||||
expect(plainCaller).toBeDefined();
|
||||
const plainCalls = cg.getOutgoingEdges(plainCaller!.id).filter((e) => e.kind === 'calls');
|
||||
expect(plainCalls).toHaveLength(1);
|
||||
expect(cg.getNode(plainCalls[0]!.target)?.name).toBe('top_func');
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
@@ -1692,11 +1692,19 @@ function resolvePythonModuleMember(
|
||||
// `import mod` / `import numpy as np` bind the module at `source` itself;
|
||||
// `from . import certs` / `from pkg import mod` bind a SUBMODULE whose
|
||||
// dotted path is the source joined with the imported name.
|
||||
//
|
||||
// Join with the EXPORTED name, not the local one: under
|
||||
// `from pkg import mod as alias` the receiver is `alias` but the module on
|
||||
// disk is `pkg.mod`, and building `pkg.alias` looked for a file that does
|
||||
// not exist — so the aliased form dropped its `calls` edge while the plain
|
||||
// form (where the two names coincide) worked (#1626). For an unaliased
|
||||
// import the two are identical, so this changes nothing there.
|
||||
const moduleName = imp.exportedName === '*' ? imp.localName : imp.exportedName;
|
||||
const modulePath = imp.isNamespace
|
||||
? imp.source
|
||||
: imp.source.endsWith('.')
|
||||
? imp.source + imp.localName
|
||||
: imp.source + '.' + imp.localName;
|
||||
? imp.source + moduleName
|
||||
: imp.source + '.' + moduleName;
|
||||
|
||||
// resolveImportPath only maps RELATIVE dotted paths (`.mod`, `..pkg.mod`); an
|
||||
// ABSOLUTE package path (`pkg.module` from `from pkg import module`, or a bare
|
||||
|
||||
Reference in New Issue
Block a user