mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(python): resolve module-qualified constructor calls (Issue #337)
Python repos were producing 0 CALLS edges for module-qualified constructor calls like `models.User()` where `import models` is a bare module import. Root causes: 1. `SupportedLanguages.Python` was absent from `WILDCARD_IMPORT_LANGUAGES`, so `synthesizeWildcardImportBindings` never ran for Python files — bare module imports never received per-symbol namedImportMap bindings. 2. Synthesis only ran in the Phase 14 pre-pass, after all chunks had already been call-resolved. When `models.User()` was processed in Phase 3+4, `namedImportMap` was empty for Python → Tier 2a-named fell through to Tier 2a which found both `models.py:User` and `auth.py:User` (ambiguous). 3. `filterCallableCandidates` with `callForm='member'` excluded `Class` nodes (only `CALLABLE_SYMBOL_TYPES` = Function/Method/Constructor/…). With 2 ambiguous Class candidates both were dropped, producing 0 CALLS edges. Fixes: - Add `SupportedLanguages.Python` to `WILDCARD_IMPORT_LANGUAGES` so that `import models` expands to per-symbol namedImportMap entries (first-seen semantics: `User→models.py:User`, `Admin→auth.py:Admin`). - Call `synthesizeWildcardImportBindings` inline in the chunk loop, after `processImportsFromExtracted` but BEFORE `processCallsFromExtracted`. This ensures Tier 2a-named can disambiguate `module.ClassName()` at initial call-resolution time. The Phase 14 pre-pass remains as a final safety net. - Add a fallback in `resolveCallTarget`: if `callForm='member'` yields 0 filtered candidates, retry with `callForm='constructor'`. This handles the case where a module-qualified class instantiation (e.g. `models.User()`) is syntactically an attribute-access call but semantically a constructor call. The fallback only triggers for 0-candidate member calls, so it cannot over-eagerly promote normal member calls. Tests: add `python-module-import` fixture (models.py/auth.py/app.py) with 4 regression tests covering IMPORTS edges, name-collision disambiguation for `models.User()`, and `auth.Admin()`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
907440cf0b
commit
cd1c0ff7dc
6 changed files with 95 additions and 10 deletions
|
|
@ -827,7 +827,15 @@ const resolveCallTarget = (
|
|||
const tiered = ctx.resolve(call.calledName, currentFile);
|
||||
if (!tiered) return null;
|
||||
|
||||
const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
|
||||
let filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
|
||||
|
||||
// Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
|
||||
// The attribute access gives callForm='member', but the callee may be a Class — a valid
|
||||
// constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
|
||||
// emits a CALLS edge to the class node.
|
||||
if (filteredCandidates.length === 0 && call.callForm === 'member') {
|
||||
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
||||
}
|
||||
|
||||
// D. Receiver-type filtering: for member calls with a known receiver type,
|
||||
// resolve the type through the same tiered import infrastructure, then
|
||||
|
|
|
|||
|
|
@ -118,13 +118,14 @@ const WILDCARD_IMPORT_LANGUAGES = new Set([
|
|||
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.
|
||||
* These languages (Go, Ruby, C/C++, Swift) import all exported symbols from a file,
|
||||
* not specific named symbols. After parsing, we know which symbols each file exports
|
||||
* (via graph isExported), so we can expand ImportMap edges into per-symbol bindings
|
||||
* that Phase 14 can use for cross-file type propagation. */
|
||||
* These languages (Go, Ruby, C/C++, Swift, Python) import all exported symbols from a
|
||||
* file, not specific named symbols. After parsing, we know which symbols each file
|
||||
* exports (via graph isExported), so we can expand ImportMap edges into per-symbol
|
||||
* bindings that Phase 14 can use for cross-file type propagation. */
|
||||
function synthesizeWildcardImportBindings(
|
||||
graph: ReturnType<typeof createKnowledgeGraph>,
|
||||
ctx: ReturnType<typeof createResolutionContext>,
|
||||
|
|
@ -576,6 +577,12 @@ export const runPipelineFromRepo = async (
|
|||
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
}, repoPath, importCtx);
|
||||
// ── Wildcard-import synthesis (Python / Ruby / C/C++ / Swift / Go) ──────────────
|
||||
// Synthesize namedImportMap entries for module-qualified calls like Python's
|
||||
// `models.User()`. Must run after imports are resolved (importMap is populated)
|
||||
// but BEFORE call resolution so Tier 2a-named can disambiguate `module.Name()`.
|
||||
// Idempotent: first-seen semantics prevents double-counting across chunks.
|
||||
synthesizeWildcardImportBindings(graph, ctx);
|
||||
// Phase 14 E1: Seed cross-file receiver types from ExportedTypeMap
|
||||
// before call resolution — eliminates re-parse for single-hop imported receivers.
|
||||
// NOTE: In the worker path, exportedTypeMap is empty during chunk processing
|
||||
|
|
@ -661,6 +668,9 @@ export const runPipelineFromRepo = async (
|
|||
}
|
||||
|
||||
// Sequential fallback chunks: re-read source for call/heritage resolution
|
||||
// Synthesize wildcard import bindings once after ALL imports are processed,
|
||||
// before any call resolution — same rationale as the worker-path inline synthesis.
|
||||
if (sequentialChunkPaths.length > 0) synthesizeWildcardImportBindings(graph, ctx);
|
||||
for (const chunkPaths of sequentialChunkPaths) {
|
||||
const chunkContents = await readFileContents(repoPath, chunkPaths);
|
||||
const chunkFiles = chunkPaths
|
||||
|
|
@ -712,13 +722,13 @@ export const runPipelineFromRepo = async (
|
|||
}
|
||||
}
|
||||
|
||||
// ── Phase 14 pre-pass: Synthesize namedImportMap for whole-module-import languages ──
|
||||
// Go, Ruby, C/C++, Swift import all exported symbols from a file.
|
||||
// Expand ImportMap edges into per-symbol namedImportMap entries so Phase 14 can
|
||||
// propagate types cross-file for these languages.
|
||||
// ── Phase 14 pre-pass: Final synthesis pass for whole-module-import languages ──
|
||||
// Per-chunk synthesis (above) already ran incrementally. This final pass ensures
|
||||
// any remaining files whose imports were not covered inline are also synthesized,
|
||||
// and that Phase 14 type propagation has complete namedImportMap data.
|
||||
const synthesized = synthesizeWildcardImportBindings(graph, ctx);
|
||||
if (isDev && synthesized > 0) {
|
||||
console.log(`🔗 Synthesized ${synthesized} wildcard import bindings (Go/Ruby/C++/Swift)`);
|
||||
console.log(`🔗 Synthesized ${synthesized} additional wildcard import bindings (Go/Ruby/C++/Swift/Python)`);
|
||||
}
|
||||
|
||||
// ── Phase 14: Cross-file binding propagation ──────────────────────
|
||||
|
|
|
|||
5
gitnexus/test/fixtures/lang-resolution/python-module-import/app.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-module-import/app.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import models
|
||||
import auth
|
||||
|
||||
u = models.User()
|
||||
a = auth.Admin()
|
||||
7
gitnexus/test/fixtures/lang-resolution/python-module-import/auth.py
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/python-module-import/auth.py
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class User:
|
||||
def check(self):
|
||||
pass
|
||||
|
||||
class Admin:
|
||||
def login(self):
|
||||
pass
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-module-import/models.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-module-import/models.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class User:
|
||||
def save(self):
|
||||
pass
|
||||
|
|
@ -1564,3 +1564,55 @@ describe('Python cross-file binding propagation', () => {
|
|||
expect(getNameEdge).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module import: `import models; models.User()` should produce CALLS edges
|
||||
// even when multiple imported modules export a class with the same name.
|
||||
// Without wildcard synthesis, Tier 2a returns candidates from both imported
|
||||
// files (models.User + auth.User) → resolveCallTarget returns null → 0 CALLS.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Python module import CALLS resolution (Issue #337)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'python-module-import'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User (×2) and Admin classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes.filter(c => c === 'User').length).toBe(2);
|
||||
expect(classes).toContain('Admin');
|
||||
});
|
||||
|
||||
it('resolves `import models` and `import auth` IMPORTS edges from app.py', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const toModels = imports.find(e =>
|
||||
e.sourceFilePath === 'app.py' && e.targetFilePath === 'models.py',
|
||||
);
|
||||
const toAuth = imports.find(e =>
|
||||
e.sourceFilePath === 'app.py' && e.targetFilePath === 'auth.py',
|
||||
);
|
||||
expect(toModels).toBeDefined();
|
||||
expect(toAuth).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves models.User() CALLS edge to models.py:User (not 0 edges despite name collision)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userCall = calls.find(c =>
|
||||
c.target === 'User' && c.targetFilePath === 'models.py',
|
||||
);
|
||||
expect(userCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves auth.Admin() CALLS edge to auth.py:Admin', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const adminCall = calls.find(c =>
|
||||
c.target === 'Admin' && c.targetFilePath === 'auth.py',
|
||||
);
|
||||
expect(adminCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue