fix(python): an aliased module import inside a function links its calls (#2607)

`import a.b as X` written inside a function binds X there, ahead of any
module-level binding of X, so X.f() reaches a/b.py's f, as the same
import at module level already did. An undotted alias is left alone,
since an optional dependency's name can match any project file.
mealie's lifespan_fn now links init_db.main; the other repos are
unchanged.

Fixes issue 2600.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Eric Minish
2026-10-20 17:04:29 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4f222a3d65
commit 23ff26be68
4 changed files with 129 additions and 0 deletions
+11
View File
@@ -26,6 +26,7 @@ import {
arrayElementType,
tsImportedTypeFile,
reExportSource,
pythonFunctionModuleAlias,
isRustAssociatedItem,
} from './name-matcher';
import { JS_ARRAY_METHODS } from './js-builtins';
@@ -2299,6 +2300,16 @@ function resolvePythonModuleMember(
const member = ref.referenceName.substring(dotIdx + 1).split('.')[0];
if (!member) return null;
// `import a.b as X` inside the calling function binds X there, ahead of any
// module-level binding of X, and the file's mappings don't hold it (#2600).
const local = pythonFunctionModuleAlias(receiver, ref, context);
if (local) {
const localPath = resolveImportPath(local, ref.filePath, ref.language, context) ??
findPythonModuleFile(local, context, ref.filePath)?.filePath ?? null;
const target = localPath && localPath !== ref.filePath ? pythonModuleSymbol(localPath, member, context, 0) : null;
return target ? { original: ref, targetNodeId: target.id, confidence: 0.85, resolvedBy: 'import' } : null;
}
for (const imp of imports) {
if (imp.localName !== receiver) continue;
+25
View File
@@ -618,6 +618,31 @@ function pythonBindsLocally(name: string, ref: UnresolvedRef, context: Resolutio
return false;
}
/**
* The module `name` is bound to by an `import a.b as name` written in the
* function around `ref` — the innermost one that binds `name` at all (#2600).
* Import mappings are per file and read only module-level `import` lines, so
* such a function-local alias has none. Null when that function binds `name`
* any other way, binds it to several modules, or imports an undotted module
* (an optional dependency's name can match any project file's basename).
*/
export function pythonFunctionModuleAlias(name: string, ref: UnresolvedRef, context: ResolutionContext): string | null {
if (ref.language !== 'python' || !/^\w+$/.test(name)) return null;
const lines = pythonMemberLines(ref.filePath, context);
const statements = pythonStatementStarts(ref.filePath, context);
const scopes = context.getNodesInFile(ref.filePath).filter(n =>
(n.kind === 'function' || n.kind === 'method') && n.startLine <= ref.line && n.endLine >= ref.line)
.sort((a, b) => b.startLine - a.startLine);
for (const scope of scopes) {
const bindings = pythonOwnLines(scope, ref.filePath, context).flatMap(i => pythonLineBindings(lines, i, statements, name));
if (bindings.length === 0) continue;
const modules = new Set(bindings.map(b => b.kind === 'import' && b.key.endsWith(':*') ? b.key.slice(0, -2) : ''));
const [module] = modules;
return modules.size === 1 && module && module.includes('.') && !pythonBindsLocally(name, ref, context, false) ? module : null;
}
return null;
}
/** Distinct sources a file imports `name` from, at any scope (`from a import x` → `a:x`). */
function pythonImportKeys(name: string, filePath: string, context: ResolutionContext): Set<string> {
return pythonNameScan(context, `imports\0${filePath}\0${name}`, () => scanPythonImportKeys(name, filePath, context));