Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion gitnexus/src/core/ingestion/call-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 19 additions & 9 deletions gitnexus/src/core/ingestion/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
]);
Comment thread
magyargergo marked this conversation as resolved.

/** 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>,
Expand Down Expand Up @@ -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);

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

synthesizeWildcardImportBindings() scans all graph nodes to rebuild exportedSymbolsByFile and iterates the entire ctx.importMap every time it runs. Calling it once per chunk in the worker path risks O(chunks × graph_size) work on large repos. Consider caching exported symbols (or maintaining them incrementally) and/or synthesizing only for the importing files affected by the current chunk’s newly-resolved imports, then keep the final pass as a safety net.

Suggested change
// 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);
// NOTE: Per-chunk wildcard-import synthesis has been removed from the worker path
// to avoid O(chunks × graph_size) behavior. A final/global synthesis pass should
// run after all imports are resolved but before any global call resolution.

Copilot uses AI. Check for mistakes.
// 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ──────────────────────
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import models
import auth

u = models.User()
a = auth.Admin()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class User:
def check(self):
pass

class Admin:
def login(self):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class User:
def save(self):
pass
52 changes: 52 additions & 0 deletions gitnexus/test/integration/resolvers/python.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Comment thread
magyargergo marked this conversation as resolved.
});
Loading