mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
fix(python): resolve module-qualified calls via moduleAliasMap
Previously, Python was added to WILDCARD_IMPORT_LANGUAGES which expanded all exported symbols into namedImportMap using first-seen wins. This caused `auth.User()` to incorrectly resolve to `models.py:User` when both modules exported a class named User. Root cause: Python `import models` is a namespace import, not wildcard symbol expansion. Expanding all symbols produces ambiguous bindings that cannot be disambiguated later. Fix: - Remove Python from WILDCARD_IMPORT_LANGUAGES - Add ModuleAliasMap (callerFile → alias → sourceFile) to ResolutionContext - In synthesizeWildcardImportBindings, build moduleAliasMap for Python using the filename stem as the module alias - In resolveCallTarget, add module-alias disambiguation step: when multiple candidates survive filtering and the receiver name matches a module alias, narrow candidates to the aliased file Result: `models.User()` → models.py:User, `auth.User()` → auth.py:User even when both modules export a class named User. Adds regression test for the ambiguity case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cd1c0ff7dc
commit
f90aabf9a8
5 changed files with 66 additions and 3 deletions
|
|
@ -600,6 +600,7 @@ export const processCalls = async (
|
|||
argCount: countCallArguments(callNode),
|
||||
callForm,
|
||||
receiverTypeName,
|
||||
receiverName,
|
||||
}, file.path, ctx, hints);
|
||||
|
||||
if (!resolved) return;
|
||||
|
|
@ -819,7 +820,7 @@ const tryOverloadDisambiguation = (
|
|||
* If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
|
||||
*/
|
||||
const resolveCallTarget = (
|
||||
call: Pick<ExtractedCall, 'calledName' | 'argCount' | 'callForm' | 'receiverTypeName'>,
|
||||
call: Pick<ExtractedCall, 'calledName' | 'argCount' | 'callForm' | 'receiverTypeName' | 'receiverName'>,
|
||||
currentFile: string,
|
||||
ctx: ResolutionContext,
|
||||
overloadHints?: OverloadHints,
|
||||
|
|
@ -837,6 +838,20 @@ const resolveCallTarget = (
|
|||
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
||||
}
|
||||
|
||||
// Module-alias disambiguation: Python `import auth; auth.User()` — when both models.py and
|
||||
// auth.py export User, receiverName='auth' selects auth.py via moduleAliasMap.
|
||||
// Runs when multiple candidates survive filtering and the receiver is a known module alias.
|
||||
if (filteredCandidates.length > 1 && call.callForm === 'member' && call.receiverName) {
|
||||
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
||||
if (aliasMap) {
|
||||
const moduleFile = aliasMap.get(call.receiverName);
|
||||
if (moduleFile) {
|
||||
const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
|
||||
if (aliasFiltered.length > 0) filteredCandidates = aliasFiltered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// D. Receiver-type filtering: for member calls with a known receiver type,
|
||||
// resolve the type through the same tiered import infrastructure, then
|
||||
// filter method candidates to the type's defining file. Fall back to
|
||||
|
|
|
|||
|
|
@ -111,14 +111,18 @@ const MAX_SYNTHETIC_BINDINGS_PER_FILE = 1000;
|
|||
|
||||
/** Languages with whole-module import semantics (no per-symbol named imports).
|
||||
* For these languages, namedImportMap entries are synthesized from graph-exported
|
||||
* symbols after parsing, enabling Phase 14 cross-file binding propagation. */
|
||||
* symbols after parsing, enabling Phase 14 cross-file binding propagation.
|
||||
*
|
||||
* Note: Python is intentionally excluded here. `import models` is a namespace import
|
||||
* (not wildcard symbol expansion) — expanding all exported symbols produces ambiguous
|
||||
* bindings when multiple modules export the same name (e.g. models.User vs auth.User).
|
||||
* Python module aliases are built in synthesizeWildcardImportBindings via moduleAliasMap. */
|
||||
const WILDCARD_IMPORT_LANGUAGES = new Set([
|
||||
SupportedLanguages.Go,
|
||||
SupportedLanguages.Ruby,
|
||||
SupportedLanguages.C,
|
||||
SupportedLanguages.CPlusPlus,
|
||||
SupportedLanguages.Swift,
|
||||
SupportedLanguages.Python, // `import models` imports all exported symbols from modules
|
||||
]);
|
||||
|
||||
/** Synthesize namedImportMap entries for languages with whole-module imports.
|
||||
|
|
@ -205,6 +209,31 @@ function synthesizeWildcardImportBindings(
|
|||
synthesizeForFile(filePath, importedFiles);
|
||||
}
|
||||
|
||||
// Build module alias map for Python namespace imports.
|
||||
// `import models` in app.py → ctx.moduleAliasMap['app.py']['models'] = 'models.py'
|
||||
// Enables `models.User()` to resolve to models.py:User without ambiguous symbol expansion.
|
||||
const buildPythonModuleAliasForFile = (callerFile: string, importedFiles: Iterable<string>) => {
|
||||
let aliasMap = ctx.moduleAliasMap.get(callerFile);
|
||||
for (const importedFile of importedFiles) {
|
||||
// Derive the module alias from the imported filename stem (e.g. "models.py" → "models")
|
||||
const lastSlash = importedFile.lastIndexOf('/');
|
||||
const base = lastSlash >= 0 ? importedFile.slice(lastSlash + 1) : importedFile;
|
||||
const dot = base.lastIndexOf('.');
|
||||
const stem = dot >= 0 ? base.slice(0, dot) : base;
|
||||
if (!stem) continue;
|
||||
if (!aliasMap) {
|
||||
aliasMap = new Map();
|
||||
ctx.moduleAliasMap.set(callerFile, aliasMap);
|
||||
}
|
||||
aliasMap.set(stem, importedFile);
|
||||
}
|
||||
};
|
||||
|
||||
for (const [filePath, importedFiles] of ctx.importMap) {
|
||||
if (getLanguageFromFilename(filePath) !== SupportedLanguages.Python) continue;
|
||||
buildPythonModuleAliasForFile(filePath, importedFiles);
|
||||
}
|
||||
|
||||
return totalSynthesized;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ export const TIER_CONFIDENCE: Record<ResolutionTier, number> = {
|
|||
export type ImportMap = Map<string, Set<string>>;
|
||||
export type PackageMap = Map<string, Set<string>>;
|
||||
export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
|
||||
/** Maps callerFile → (moduleAlias → sourceFilePath) for Python namespace imports.
|
||||
* e.g. `import models` in app.py → moduleAliasMap.get('app.py')?.get('models') === 'models.py' */
|
||||
export type ModuleAliasMap = Map<string, Map<string, string>>;
|
||||
|
||||
export interface ResolutionContext {
|
||||
/**
|
||||
|
|
@ -56,6 +59,8 @@ export interface ResolutionContext {
|
|||
readonly importMap: ImportMap;
|
||||
readonly packageMap: PackageMap;
|
||||
readonly namedImportMap: NamedImportMap;
|
||||
/** Module-alias map for Python namespace imports: callerFile → (alias → sourceFile). */
|
||||
readonly moduleAliasMap: ModuleAliasMap;
|
||||
|
||||
// --- Per-file cache lifecycle ---
|
||||
enableCache(filePath: string): void;
|
||||
|
|
@ -71,6 +76,7 @@ export const createResolutionContext = (): ResolutionContext => {
|
|||
const importMap: ImportMap = new Map();
|
||||
const packageMap: PackageMap = new Map();
|
||||
const namedImportMap: NamedImportMap = new Map();
|
||||
const moduleAliasMap: ModuleAliasMap = new Map();
|
||||
|
||||
// Per-file cache state
|
||||
let cacheFile: string | null = null;
|
||||
|
|
@ -173,6 +179,7 @@ export const createResolutionContext = (): ResolutionContext => {
|
|||
importMap.clear();
|
||||
packageMap.clear();
|
||||
namedImportMap.clear();
|
||||
moduleAliasMap.clear();
|
||||
clearCache();
|
||||
cacheHits = 0;
|
||||
cacheMisses = 0;
|
||||
|
|
@ -184,6 +191,7 @@ export const createResolutionContext = (): ResolutionContext => {
|
|||
importMap,
|
||||
packageMap,
|
||||
namedImportMap,
|
||||
moduleAliasMap,
|
||||
enableCache,
|
||||
clearCache,
|
||||
getStats,
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ import auth
|
|||
|
||||
u = models.User()
|
||||
a = auth.Admin()
|
||||
au = auth.User()
|
||||
|
|
|
|||
|
|
@ -1615,4 +1615,14 @@ describe('Python module import CALLS resolution (Issue #337)', () => {
|
|||
);
|
||||
expect(adminCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves auth.User() CALLS edge to auth.py:User (not models.py:User) when both modules export User', () => {
|
||||
// Both models.py and auth.py export a class named User.
|
||||
// auth.User() must resolve to auth.py:User — the receiver module disambiguates.
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const authUserCall = calls.find(c =>
|
||||
c.target === 'User' && c.targetFilePath === 'auth.py',
|
||||
);
|
||||
expect(authUserCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue