mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-13 08:51:28 +00:00
Merge pull request #463 from ShunsukeHayashi/fix/python-calls-zero-337
fix(python): resolve module-qualified constructor calls — 0 CALLS edges (Issue #337)
This commit is contained in:
commit
a57550815f
7 changed files with 296 additions and 33 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,
|
||||
|
|
@ -827,7 +828,29 @@ 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');
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -111,7 +111,12 @@ 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,
|
||||
|
|
@ -120,11 +125,15 @@ const WILDCARD_IMPORT_LANGUAGES = new Set([
|
|||
SupportedLanguages.Swift,
|
||||
]);
|
||||
|
||||
/** Languages that require synthesizeWildcardImportBindings to run before call resolution.
|
||||
* Superset of WILDCARD_IMPORT_LANGUAGES — includes Python for moduleAliasMap building. */
|
||||
const SYNTHESIS_LANGUAGES = new Set([...WILDCARD_IMPORT_LANGUAGES, SupportedLanguages.Python]);
|
||||
|
||||
/** 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>,
|
||||
|
|
@ -199,11 +208,36 @@ function synthesizeWildcardImportBindings(
|
|||
synthesizeForFile(filePath, importedFiles);
|
||||
}
|
||||
|
||||
// Process files from graph IMPORTS edges (Go package imports)
|
||||
// Process files from graph IMPORTS edges (Go and other wildcard-import languages)
|
||||
for (const [filePath, importedFiles] of graphImports) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -531,6 +565,13 @@ export const runPipelineFromRepo = async (
|
|||
// are already registered). This trades ~5% cross-chunk resolution accuracy for
|
||||
// 200-400MB less memory — critical for Linux-kernel-scale repos.
|
||||
const sequentialChunkPaths: string[][] = [];
|
||||
// Pre-compute which chunks need synthesis — O(1) lookup per chunk.
|
||||
const chunkNeedsSynthesis = chunks.map(paths =>
|
||||
paths.some(p => {
|
||||
const lang = getLanguageFromFilename(p);
|
||||
return lang != null && SYNTHESIS_LANGUAGES.has(lang);
|
||||
}),
|
||||
);
|
||||
// Phase 14: Collect exported type bindings for cross-file propagation
|
||||
const exportedTypeMap: ExportedTypeMap = new Map();
|
||||
// Accumulate file-scope TypeEnv bindings from workers (closes worker/sequential quality gap)
|
||||
|
|
@ -576,6 +617,11 @@ export const runPipelineFromRepo = async (
|
|||
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
}, repoPath, importCtx);
|
||||
// ── Wildcard-import synthesis (Ruby / C/C++ / Swift / Go) + Python module aliases ─
|
||||
// Synthesize namedImportMap entries for wildcard-import languages and build
|
||||
// moduleAliasMap for Python namespace imports. Must run after imports are resolved
|
||||
// (importMap is populated) but BEFORE call resolution.
|
||||
if (chunkNeedsSynthesis[chunkIdx]) 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 +707,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 +761,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 ──────────────────────
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
14
gitnexus/test/fixtures/lang-resolution/python-module-import/app.py
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/python-module-import/app.py
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import models
|
||||
import auth
|
||||
|
||||
u = models.User()
|
||||
u.save()
|
||||
|
||||
a = auth.Admin()
|
||||
a.login()
|
||||
|
||||
# Same-name cross-module disambiguation: both models and auth export User.
|
||||
# moduleAliasMap maps receiverName='auth' → auth.py, enabling resolveCallTarget
|
||||
# to narrow candidates to the correct file.
|
||||
v = auth.User()
|
||||
v.verify()
|
||||
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 verify(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
|
||||
|
|
@ -1006,25 +1006,7 @@ describe('Python match/case as-pattern type binding', () => {
|
|||
expect(saveFns.length).toBe(2);
|
||||
});
|
||||
|
||||
it('DEBUG: shows pipeline result details', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
console.log('ALL CALLS:', JSON.stringify(calls.map(c => ({ source: c.source, target: c.target, targetFilePath: c.targetFilePath }))));
|
||||
// Check all relationships
|
||||
const allRels: string[] = [];
|
||||
result.graph.iterRelationships && [...result.graph.iterRelationships()].forEach(r => {
|
||||
const src = result.graph.getNode(r.sourceId);
|
||||
const tgt = result.graph.getNode(r.targetId);
|
||||
allRels.push(r.type + ': ' + src?.properties.name + ' -> ' + tgt?.properties.name);
|
||||
});
|
||||
console.log('ALL RELATIONSHIPS:', allRels.join(', '));
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
// Skip: call extraction issue, NOT a type-env limitation.
|
||||
// Type-env binding works correctly (unit test passes). The root cause is likely
|
||||
// in call-processor's findEnclosingFunction scope resolution within match_statement
|
||||
// blocks, not the tree-sitter query patterns (which descend recursively by default).
|
||||
it.skip('resolves u.save() to User#save via match/case as-pattern binding', () => {
|
||||
it('resolves u.save() to User#save via match/case as-pattern binding', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('user.py'),
|
||||
|
|
@ -1032,7 +1014,7 @@ describe('Python match/case as-pattern type binding', () => {
|
|||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it.skip('does NOT resolve u.save() to Repo#save (negative disambiguation)', () => {
|
||||
it('does NOT resolve u.save() to Repo#save (negative disambiguation)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('repo.py'),
|
||||
|
|
@ -1564,3 +1546,180 @@ 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.
|
||||
// Python's `import models` is a namespace import — moduleAliasMap maps the
|
||||
// module alias to its source file, enabling resolveCallTarget to disambiguate
|
||||
// `models.User()` from `auth.User()` when both modules export `User`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Python module import CALLS resolution (Issue #337)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'python-module-import'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
// ── Node detection ──────────────────────────────────────────────────
|
||||
|
||||
it('detects exactly 3 Class nodes: User (×2) and Admin (×1)', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes.length).toBe(3);
|
||||
expect(classes.filter(c => c === 'User').length).toBe(2);
|
||||
expect(classes.filter(c => c === 'Admin').length).toBe(1);
|
||||
});
|
||||
|
||||
it('detects exactly 3 Function nodes: save, verify, login', () => {
|
||||
const fns = getNodesByLabel(result, 'Function');
|
||||
expect(fns.length).toBe(3);
|
||||
expect(fns).toContain('save');
|
||||
expect(fns).toContain('verify');
|
||||
expect(fns).toContain('login');
|
||||
});
|
||||
|
||||
// ── IMPORTS edges ───────────────────────────────────────────────────
|
||||
|
||||
it('emits exactly 2 IMPORTS edges from app.py', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const appImports = imports.filter(e => e.sourceFilePath === 'app.py');
|
||||
expect(appImports.length).toBe(2);
|
||||
});
|
||||
|
||||
it('resolves `import models` IMPORTS edge: app.py → models.py', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const toModels = imports.find(e =>
|
||||
e.sourceFilePath === 'app.py' && e.targetFilePath === 'models.py',
|
||||
);
|
||||
expect(toModels).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves `import auth` IMPORTS edge: app.py → auth.py', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const toAuth = imports.find(e =>
|
||||
e.sourceFilePath === 'app.py' && e.targetFilePath === 'auth.py',
|
||||
);
|
||||
expect(toAuth).toBeDefined();
|
||||
});
|
||||
|
||||
it('no IMPORTS edge from models.py or auth.py (they import nothing)', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const fromModels = imports.filter(e => e.sourceFilePath === 'models.py');
|
||||
const fromAuth = imports.filter(e => e.sourceFilePath === 'auth.py');
|
||||
expect(fromModels.length).toBe(0);
|
||||
expect(fromAuth.length).toBe(0);
|
||||
});
|
||||
|
||||
// ── CALLS edges: key regression test (Issue #337) ───────────────────
|
||||
|
||||
it('resolves models.User() CALLS edge from app.py to models.py:User', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userCall = calls.find(c =>
|
||||
c.target === 'User' && c.targetFilePath === 'models.py' && c.sourceFilePath === 'app.py',
|
||||
);
|
||||
expect(userCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves auth.Admin() CALLS edge from app.py to auth.py:Admin', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const adminCall = calls.find(c =>
|
||||
c.target === 'Admin' && c.targetFilePath === 'auth.py' && c.sourceFilePath === 'app.py',
|
||||
);
|
||||
expect(adminCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves u.save() method call from app.py to models.py:save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath === 'models.py' && c.sourceFilePath === 'app.py',
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves a.login() method call from app.py to auth.py:login', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const loginCall = calls.find(c =>
|
||||
c.target === 'login' && c.targetFilePath === 'auth.py' && c.sourceFilePath === 'app.py',
|
||||
);
|
||||
expect(loginCall).toBeDefined();
|
||||
});
|
||||
|
||||
// ── Negative tests ──────────────────────────────────────────────────
|
||||
|
||||
it('no CALLS edges originate from models.py or auth.py (they have no callers)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const fromModels = calls.filter(c => c.sourceFilePath === 'models.py');
|
||||
const fromAuth = calls.filter(c => c.sourceFilePath === 'auth.py');
|
||||
expect(fromModels.length).toBe(0);
|
||||
expect(fromAuth.length).toBe(0);
|
||||
});
|
||||
|
||||
it('Admin() does NOT resolve to models.py (Admin only exists in auth.py)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongAdmin = calls.find(c =>
|
||||
c.target === 'Admin' && c.targetFilePath === 'models.py',
|
||||
);
|
||||
expect(wrongAdmin).toBeUndefined();
|
||||
});
|
||||
|
||||
it('no EXTENDS edges (no inheritance in this fixture)', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(extends_.length).toBe(0);
|
||||
});
|
||||
|
||||
// ── Same-name cross-module disambiguation ───────────────────────────
|
||||
|
||||
it('resolves auth.User() CALLS edge to auth.py:User (not models.py:User)', () => {
|
||||
// Both models.py and auth.py export User. moduleAliasMap maps
|
||||
// receiverName='auth' → auth.py for correct disambiguation.
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const authUserCall = calls.find(c =>
|
||||
c.target === 'User' && c.targetFilePath === 'auth.py' && c.sourceFilePath === 'app.py',
|
||||
);
|
||||
expect(authUserCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('models.User() and auth.User() resolve to DIFFERENT files', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userCalls = calls.filter(c =>
|
||||
c.target === 'User' && c.sourceFilePath === 'app.py',
|
||||
);
|
||||
expect(userCalls.length).toBe(2);
|
||||
const targetFiles = new Set(userCalls.map(c => c.targetFilePath));
|
||||
expect(targetFiles.size).toBe(2);
|
||||
expect(targetFiles).toContain('models.py');
|
||||
expect(targetFiles).toContain('auth.py');
|
||||
});
|
||||
|
||||
it('v.verify() resolves to auth.py:verify (via auth.User() constructor inference)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const verifyCall = calls.find(c =>
|
||||
c.target === 'verify' && c.targetFilePath === 'auth.py' && c.sourceFilePath === 'app.py',
|
||||
);
|
||||
expect(verifyCall).toBeDefined();
|
||||
});
|
||||
|
||||
// ── HAS_METHOD edges ────────────────────────────────────────────────
|
||||
|
||||
it('emits HAS_METHOD edges linking methods to their classes', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
// models.py: User → save
|
||||
const modelsUserSave = hasMethod.find(e =>
|
||||
e.source === 'User' && e.target === 'save' && e.sourceFilePath === 'models.py',
|
||||
);
|
||||
expect(modelsUserSave).toBeDefined();
|
||||
// auth.py: User → verify, Admin → login
|
||||
const authUserVerify = hasMethod.find(e =>
|
||||
e.source === 'User' && e.target === 'verify' && e.sourceFilePath === 'auth.py',
|
||||
);
|
||||
const authAdminLogin = hasMethod.find(e =>
|
||||
e.source === 'Admin' && e.target === 'login' && e.sourceFilePath === 'auth.py',
|
||||
);
|
||||
expect(authUserVerify).toBeDefined();
|
||||
expect(authAdminLogin).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue