mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(python): route module aliases directly to moduleAliasMap in import processor
`import models as m` aliases were stored in namedImportMap (a symbol-binding map) then cross-referenced in pipeline.ts — semantic misuse and inefficient. Refactored: NamedBinding gains `isModuleAlias` flag. applyImportResult routes tagged bindings directly to moduleAliasMap at import time. Removes the pipeline.ts post-processing loop entirely. Added test fixture and 5 integration tests for `import X as Y` with multi-module disambiguation (both models.py and auth.py export User).
This commit is contained in:
parent
9f2d1780d5
commit
cb1293b718
7 changed files with 108 additions and 21 deletions
|
|
@ -10,7 +10,7 @@ import { getTreeSitterBufferSize } from './constants.js';
|
|||
import { loadImportConfigs } from './language-config.js';
|
||||
import { buildSuffixIndex } from './resolvers/index.js';
|
||||
import { callRouters } from './call-routing.js';
|
||||
import type { ResolutionContext } from './resolution-context.js';
|
||||
import type { ResolutionContext, ModuleAliasMap } from './resolution-context.js';
|
||||
import type { SuffixIndex } from './resolvers/index.js';
|
||||
import { importResolvers, namedBindingExtractors, preprocessImportPath } from './import-resolution.js';
|
||||
import type { ImportResult, ResolveCtx, NamedBinding } from './import-resolution.js';
|
||||
|
|
@ -101,6 +101,7 @@ function createImportEdgeHelpers(graph: KnowledgeGraph, importMap: ImportMap) {
|
|||
* Apply an ImportResult: emit graph edges and update ImportMap/PackageMap.
|
||||
* If namedBindings are provided and the import resolves to a single file,
|
||||
* also populate the NamedImportMap for precise Tier 2a resolution.
|
||||
* Bindings tagged with `isModuleAlias` are routed to moduleAliasMap instead.
|
||||
*/
|
||||
function applyImportResult(
|
||||
result: ImportResult,
|
||||
|
|
@ -111,6 +112,7 @@ function applyImportResult(
|
|||
addImportGraphEdge: (from: string, to: string) => void,
|
||||
namedBindings?: NamedBinding[],
|
||||
namedImportMap?: NamedImportMap,
|
||||
moduleAliasMap?: ModuleAliasMap,
|
||||
): void {
|
||||
if (!result) return;
|
||||
|
||||
|
|
@ -128,6 +130,21 @@ function applyImportResult(
|
|||
addImportEdge(filePath, resolvedFile);
|
||||
}
|
||||
|
||||
// Route module aliases (import X as Y) directly to moduleAliasMap.
|
||||
// These are module-level aliases, not symbol bindings — they don't belong in namedImportMap.
|
||||
if (namedBindings && moduleAliasMap && files.length === 1) {
|
||||
const resolvedFile = files[0];
|
||||
for (const binding of namedBindings) {
|
||||
if (!binding.isModuleAlias) continue;
|
||||
let aliasMap = moduleAliasMap.get(filePath);
|
||||
if (!aliasMap) {
|
||||
aliasMap = new Map();
|
||||
moduleAliasMap.set(filePath, aliasMap);
|
||||
}
|
||||
aliasMap.set(binding.local, resolvedFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Record named bindings for precise Tier 2a resolution.
|
||||
// If the same local name is imported from multiple files (e.g., Java static imports
|
||||
// of overloaded methods), remove the entry so resolution falls through to Tier 2a
|
||||
|
|
@ -139,6 +156,7 @@ function applyImportResult(
|
|||
if (files.length === 1) {
|
||||
const resolvedFile = files[0];
|
||||
for (const binding of namedBindings) {
|
||||
if (binding.isModuleAlias) continue; // already routed to moduleAliasMap
|
||||
const existing = fileBindings.get(binding.local);
|
||||
if (existing && existing.sourcePath !== resolvedFile) {
|
||||
fileBindings.delete(binding.local);
|
||||
|
|
@ -151,6 +169,7 @@ function applyImportResult(
|
|||
// Match each binding to a resolved file by comparing the lowercase binding name
|
||||
// to the file's basename (without extension). If no match, skip the binding.
|
||||
for (const binding of namedBindings) {
|
||||
if (binding.isModuleAlias) continue;
|
||||
const lowerName = binding.exported.toLowerCase();
|
||||
const matchedFile = files.find(f => {
|
||||
const base = f.replace(/\\/g, '/').split('/').pop() ?? '';
|
||||
|
|
@ -187,6 +206,7 @@ export const processImports = async (
|
|||
const importMap = ctx.importMap;
|
||||
const packageMap = ctx.packageMap;
|
||||
const namedImportMap = ctx.namedImportMap;
|
||||
const moduleAliasMap = ctx.moduleAliasMap;
|
||||
// Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
|
||||
const allFileList = allPaths ?? files.map(f => f.path);
|
||||
const allFilePaths = new Set(allFileList);
|
||||
|
|
@ -285,7 +305,7 @@ export const processImports = async (
|
|||
const result = importResolvers[language](rawImportPath, file.path, resolveCtx);
|
||||
const extractor = namedBindingExtractors[language];
|
||||
const bindings = namedImportMap && extractor ? extractor(captureMap['import']) : undefined;
|
||||
applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge, bindings, namedImportMap);
|
||||
applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge, bindings, namedImportMap, moduleAliasMap);
|
||||
}
|
||||
|
||||
// ---- Language-specific call-as-import routing (Ruby require, etc.) ----
|
||||
|
|
@ -335,6 +355,7 @@ export const processImportsFromExtracted = async (
|
|||
const importMap = ctx.importMap;
|
||||
const packageMap = ctx.packageMap;
|
||||
const namedImportMap = ctx.namedImportMap;
|
||||
const moduleAliasMap = ctx.moduleAliasMap;
|
||||
const importCtx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
|
||||
const { allFilePaths, allFileList, normalizedFileList, index, resolveCache } = importCtx;
|
||||
|
||||
|
|
@ -369,7 +390,7 @@ export const processImportsFromExtracted = async (
|
|||
totalImportsFound++;
|
||||
|
||||
const result = importResolvers[imp.language](imp.rawImportPath, filePath, resolveCtx);
|
||||
applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, imp.namedBindings, namedImportMap);
|
||||
applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, imp.namedBindings, namedImportMap, moduleAliasMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,8 +83,10 @@ export type ImportResolverFn = (
|
|||
resolveCtx: ResolveCtx,
|
||||
) => ImportResult;
|
||||
|
||||
/** A single named import binding: local name in the importing file and exported name from the source. */
|
||||
export interface NamedBinding { local: string; exported: string }
|
||||
/** A single named import binding: local name in the importing file and exported name from the source.
|
||||
* When `isModuleAlias` is true, the binding represents a Python `import X as Y` module alias
|
||||
* and is routed to moduleAliasMap instead of namedImportMap during import processing. */
|
||||
export interface NamedBinding { local: string; exported: string; isModuleAlias?: boolean }
|
||||
|
||||
/** Per-language named binding extractor -- optional (returns undefined if language has no named imports). */
|
||||
type NamedBindingExtractorFn = (importNode: SyntaxNode) => NamedBinding[] | undefined;
|
||||
|
|
|
|||
|
|
@ -146,27 +146,18 @@ export function extractPythonNamedBindings(importNode: SyntaxNode): NamedBinding
|
|||
}
|
||||
|
||||
// Handle: import numpy as np (import_statement with aliased_import child)
|
||||
// Records local alias "np" → exported module name "numpy" so that call sites
|
||||
// like np.array() resolve correctly via namedImportMap.
|
||||
// Tagged with isModuleAlias so applyImportResult routes these directly to
|
||||
// moduleAliasMap (e.g. "np" → "numpy.py") instead of namedImportMap.
|
||||
if (importNode.type === 'import_statement') {
|
||||
const bindings: NamedBinding[] = [];
|
||||
for (let i = 0; i < importNode.namedChildCount; i++) {
|
||||
const child = importNode.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (!child || child.type !== 'aliased_import') continue;
|
||||
|
||||
if (child.type === 'aliased_import') {
|
||||
// import numpy as np → name=(dotted_name "numpy"), alias=(identifier "np")
|
||||
const dottedName = findChild(child, 'dotted_name');
|
||||
const aliasIdent = findChild(child, 'identifier');
|
||||
if (dottedName && aliasIdent) {
|
||||
// exported = last segment of dotted module path (e.g. "numpy" from "numpy")
|
||||
// local = alias used at call sites (e.g. "np")
|
||||
const moduleName = dottedName.text;
|
||||
const lastSegment = moduleName.includes('.')
|
||||
? moduleName.split('.').pop()!
|
||||
: moduleName;
|
||||
bindings.push({ local: aliasIdent.text, exported: lastSegment });
|
||||
}
|
||||
const dottedName = findChild(child, 'dotted_name');
|
||||
const aliasIdent = findChild(child, 'identifier');
|
||||
if (dottedName && aliasIdent) {
|
||||
bindings.push({ local: aliasIdent.text, exported: dottedName.text, isModuleAlias: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
12
gitnexus/test/fixtures/lang-resolution/python-plain-import-alias/app.py
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/python-plain-import-alias/app.py
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import models as m
|
||||
import auth as a
|
||||
|
||||
def main():
|
||||
u = m.User()
|
||||
u.save()
|
||||
|
||||
r = m.Repo()
|
||||
r.persist()
|
||||
|
||||
v = a.User()
|
||||
v.login()
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-plain-import-alias/auth.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-plain-import-alias/auth.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class User:
|
||||
def login(self):
|
||||
pass
|
||||
7
gitnexus/test/fixtures/lang-resolution/python-plain-import-alias/models.py
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/python-plain-import-alias/models.py
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class User:
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
class Repo:
|
||||
def persist(self):
|
||||
pass
|
||||
|
|
@ -285,6 +285,57 @@ describe('Python alias import resolution', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plain import alias: import models as m → m.User() resolves to models.py
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Python plain import alias resolution (import X as Y)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'python-plain-import-alias'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User classes in both models.py and auth.py', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('User');
|
||||
expect(classes).toContain('Repo');
|
||||
});
|
||||
|
||||
it('emits IMPORTS edges: app.py → models.py and app.py → auth.py', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const importFiles = imports
|
||||
.filter(i => i.sourceFilePath === 'app.py')
|
||||
.map(i => i.targetFilePath)
|
||||
.sort();
|
||||
expect(importFiles).toEqual(['auth.py', 'models.py']);
|
||||
});
|
||||
|
||||
it('resolves m.User() and u.save() to models.py via alias', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c => c.target === 'save' && c.source === 'main');
|
||||
expect(saveCall).toBeDefined();
|
||||
expect(saveCall!.targetFilePath).toBe('models.py');
|
||||
});
|
||||
|
||||
it('resolves m.Repo() and r.persist() to models.py via alias', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const persistCall = calls.find(c => c.target === 'persist' && c.source === 'main');
|
||||
expect(persistCall).toBeDefined();
|
||||
expect(persistCall!.targetFilePath).toBe('models.py');
|
||||
});
|
||||
|
||||
it('resolves a.User() and v.login() to auth.py via alias (disambiguation)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const loginCall = calls.find(c => c.target === 'login' && c.source === 'main');
|
||||
expect(loginCall).toBeDefined();
|
||||
expect(loginCall!.targetFilePath).toBe('auth.py');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Re-export chain: from .base import X barrel pattern via __init__.py
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue