mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
214 lines
7.3 KiB
TypeScript
214 lines
7.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { ParsedFile } from 'gitnexus-shared';
|
|
import { pythonScopeResolver } from '../../../../src/core/ingestion/languages/python/scope-resolver.js';
|
|
import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js';
|
|
import { finalizeScopeModel } from '../../../../src/core/ingestion/finalize-orchestrator.js';
|
|
import { resolveCompoundReceiverClass } from '../../../../src/core/ingestion/scope-resolution/passes/compound-receiver.js';
|
|
import { collectNamespaceTargets } from '../../../../src/core/ingestion/scope-resolution/scope/namespace-targets.js';
|
|
import { buildWorkspaceResolutionIndex } from '../../../../src/core/ingestion/scope-resolution/workspace-index.js';
|
|
|
|
const SOURCES = new Map([
|
|
[
|
|
'pkg/app.py',
|
|
`from pkg import models
|
|
from decoy import Missing
|
|
|
|
def valid():
|
|
return models.User()
|
|
|
|
def missing():
|
|
return models.Missing()
|
|
|
|
def shadowed(models):
|
|
return models.User()
|
|
|
|
def locally_shadowed():
|
|
models = object()
|
|
return models.User()
|
|
`,
|
|
],
|
|
[
|
|
'pkg/dotted.py',
|
|
`import pkg.models
|
|
|
|
def dotted():
|
|
return pkg.models.User()
|
|
|
|
def reexported():
|
|
return pkg.User()
|
|
|
|
def missing_package_member():
|
|
return pkg.Hidden()
|
|
|
|
def dotted_root_shadowed():
|
|
pkg = object()
|
|
return pkg.models.User()
|
|
`,
|
|
],
|
|
[
|
|
'pkg/models.py',
|
|
`from decoy import Hidden
|
|
|
|
class User:
|
|
pass
|
|
`,
|
|
],
|
|
['pkg/__init__.py', 'from .models import User\n'],
|
|
[
|
|
'pkg/package_leaf.py',
|
|
`import pkg.subpkg
|
|
|
|
def from_root():
|
|
return pkg.User()
|
|
|
|
def from_leaf():
|
|
return pkg.subpkg.User()
|
|
`,
|
|
],
|
|
['pkg/subpkg/__init__.py', 'from .models import User\n'],
|
|
['pkg/subpkg/models.py', 'class User:\n pass\n'],
|
|
[
|
|
'decoy.py',
|
|
`class Missing:
|
|
pass
|
|
|
|
class Hidden:
|
|
pass
|
|
`,
|
|
],
|
|
]);
|
|
|
|
function build() {
|
|
const parsedFiles: ParsedFile[] = [];
|
|
for (const [filePath, source] of SOURCES) {
|
|
const parsed = extractParsedFile(pythonScopeResolver.languageProvider, source, filePath);
|
|
if (parsed === undefined) throw new Error(`scope extraction failed for ${filePath}`);
|
|
pythonScopeResolver.populateOwners(parsed);
|
|
parsedFiles.push(parsed);
|
|
}
|
|
|
|
const allFilePaths = new Set(parsedFiles.map((file) => file.filePath));
|
|
const scopes = finalizeScopeModel(parsedFiles, {
|
|
hooks: {
|
|
resolveImportTarget: (targetRaw, fromFile, _workspaceIndex, parsedImport) =>
|
|
pythonScopeResolver.resolveImportTarget(targetRaw, fromFile, allFilePaths, undefined, {
|
|
parsedFiles,
|
|
parsedImport,
|
|
}),
|
|
isNamespaceImport: (parsedImport, targetFile, fromFile) =>
|
|
pythonScopeResolver.isNamespaceImport?.(parsedImport, targetFile, fromFile) ?? false,
|
|
mergeBindings: (existing, incoming, scopeId) =>
|
|
pythonScopeResolver.mergeBindings(existing, incoming, scopeId),
|
|
},
|
|
});
|
|
const index = buildWorkspaceResolutionIndex(parsedFiles);
|
|
const namespaceTargetsFor = (file: ParsedFile) =>
|
|
collectNamespaceTargets(file, scopes, {
|
|
// Mirror the production wiring in receiver-bound-calls.ts: the extra
|
|
// receiver spellings come from the provider hook, not from a default.
|
|
receiverPaths: pythonScopeResolver.namespaceReceiverPaths,
|
|
moduleFileExists: (filePath) => index.moduleScopeByFile.has(filePath),
|
|
});
|
|
const app = parsedFiles.find((file) => file.filePath === 'pkg/app.py');
|
|
if (app === undefined) throw new Error('missing app fixture');
|
|
const namespaceTargets = namespaceTargetsFor(app);
|
|
|
|
const resolveIn = (functionName: string, expression: string) => {
|
|
const functionScope = app.scopes.find(
|
|
(scope) =>
|
|
scope.kind === 'Function' &&
|
|
scope.ownedDefs.some((def) => def.qualifiedName === functionName),
|
|
);
|
|
if (functionScope === undefined) throw new Error(`missing scope for ${functionName}`);
|
|
return resolveCompoundReceiverClass(expression, functionScope.id, scopes, index, {
|
|
constructionSyntax: { bare: true },
|
|
namespaceTargets,
|
|
namespaceExportsIncludeImportedNames:
|
|
pythonScopeResolver.namespaceExportsIncludeImportedNames,
|
|
});
|
|
};
|
|
|
|
const resolveDottedIn = (
|
|
functionName: string,
|
|
expression: string,
|
|
filePath = 'pkg/dotted.py',
|
|
) => {
|
|
const dotted = parsedFiles.find((file) => file.filePath === filePath);
|
|
if (dotted === undefined) throw new Error(`missing fixture ${filePath}`);
|
|
const functionScope = dotted.scopes.find(
|
|
(scope) =>
|
|
scope.kind === 'Function' &&
|
|
scope.ownedDefs.some((def) => def.qualifiedName === functionName),
|
|
);
|
|
if (functionScope === undefined) throw new Error(`missing scope for ${functionName}`);
|
|
return resolveCompoundReceiverClass(expression, functionScope.id, scopes, index, {
|
|
constructionSyntax: { bare: true },
|
|
namespaceTargets: namespaceTargetsFor(dotted),
|
|
namespaceExportsIncludeImportedNames:
|
|
pythonScopeResolver.namespaceExportsIncludeImportedNames,
|
|
});
|
|
};
|
|
|
|
return { resolveIn, resolveDottedIn };
|
|
}
|
|
|
|
describe('Python module namespace construction', () => {
|
|
const { resolveIn, resolveDottedIn } = build();
|
|
|
|
it('resolves an exported class from the verified module target', () => {
|
|
expect(resolveIn('valid', 'models.User()')).toMatchObject({
|
|
filePath: 'pkg/models.py',
|
|
qualifiedName: 'User',
|
|
});
|
|
});
|
|
|
|
it('fails closed when the module lacks the requested class', () => {
|
|
expect(resolveIn('missing', 'models.Missing()')).toBeUndefined();
|
|
});
|
|
|
|
it('does not reuse a file-level namespace when a parameter shadows it', () => {
|
|
expect(resolveIn('shadowed', 'models.User()')).toBeUndefined();
|
|
});
|
|
|
|
it('does not reuse a file-level namespace when a local shadows it', () => {
|
|
expect(resolveIn('locally_shadowed', 'models.User()')).toBeUndefined();
|
|
});
|
|
|
|
// #2826: `import pkg.models` binds only `pkg`, so the namespace key is the
|
|
// dotted path `pkg.models` while the shadowable name is the root `pkg`.
|
|
it('resolves construction through a dotted import-path namespace', () => {
|
|
expect(resolveDottedIn('dotted', 'pkg.models.User()')).toMatchObject({
|
|
filePath: 'pkg/models.py',
|
|
qualifiedName: 'User',
|
|
});
|
|
});
|
|
|
|
it('does not treat a class re-exported from the imported leaf as ambiguous', () => {
|
|
expect(resolveDottedIn('reexported', 'pkg.User()')).toMatchObject({
|
|
filePath: 'pkg/models.py',
|
|
qualifiedName: 'User',
|
|
});
|
|
});
|
|
|
|
it('does not expose an imported leaf member on the package', () => {
|
|
expect(resolveDottedIn('missing_package_member', 'pkg.Hidden()')).toBeUndefined();
|
|
});
|
|
|
|
it('resolves root and leaf re-exports from their own package namespaces', () => {
|
|
expect(resolveDottedIn('from_root', 'pkg.User()', 'pkg/package_leaf.py')).toMatchObject({
|
|
filePath: 'pkg/models.py',
|
|
qualifiedName: 'User',
|
|
});
|
|
expect(resolveDottedIn('from_leaf', 'pkg.subpkg.User()', 'pkg/package_leaf.py')).toMatchObject({
|
|
filePath: 'pkg/subpkg/models.py',
|
|
qualifiedName: 'User',
|
|
});
|
|
});
|
|
|
|
it('does not reuse a dotted namespace when a local shadows its ROOT segment', () => {
|
|
// Fails without the root-segment fix: testing the whole `pkg.models`
|
|
// string against scope bindings never matches, so the guard would pass a
|
|
// shadowed receiver straight through to the authoritative namespace branch.
|
|
expect(resolveDottedIn('dotted_root_shadowed', 'pkg.models.User()')).toBeUndefined();
|
|
});
|
|
});
|