GitNexus/gitnexus/test/integration/resolvers/python-namespace-semantics.test.ts

314 lines
10 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
getRelationships,
runPipelineFromRepo,
writeFixtureRepo,
type PipelineResult,
} from './helpers.js';
describe('Python wildcard definitions and lexical namespace visibility', () => {
let repoDir: string;
let result: PipelineResult;
beforeAll(async () => {
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-python-namespace-semantics-'));
writeFixtureRepo(repoDir, {
'definitions.py':
'class Worker:\n def run(self):\n return 1\ndef run():\n return 2\n',
'values.py': 'run = 42\nclass Worker:\n def run(self):\n return 1\n',
'definitions_star.py': 'from definitions import *\n',
'values_star.py': 'from values import *\n',
'origin.py':
'def helper():\n return 1\ndef _hidden():\n return 2\nclass Maker:\n def work(self):\n return 3\n',
'pkg/__init__.py': 'from origin import helper as exported, Maker, _hidden\n',
'pkg/a.py': 'def run():\n return 3\n',
'pkg/b.py': 'def other():\n return 4\n',
'pkg/nested/__init__.py': 'def other():\n return 11\n',
'namespace_pkg/a.py': 'def run():\n return 5\n',
'namespace_pkg/b.py': 'def other():\n return 6\n',
'star.py': 'from pkg import *\n',
'multi_star.py': 'from star import *\n',
'exports.py': '__all__ = ["exported"]\n',
'assigned_all.py': 'from pkg import exported\n__all__ = ["exported"]\n',
'imported_all.py': 'from pkg import exported\nfrom exports import __all__\n',
'local_all.py':
'from pkg import exported\ndef configure():\n from exports import __all__\n',
'assigned_star.py': 'from assigned_all import *\n',
'imported_star.py': 'from imported_all import *\n',
'blocked_multi.py': 'from assigned_star import *\n',
'local_star.py': 'from local_all import *\n',
'cycle_a.py': 'from cycle_b import *\ndef cyclic():\n return 7\n',
'cycle_b.py': 'from cycle_a import *\n',
'module_target.py': 'def helper():\n return 8\n',
'class_target.py': 'def helper():\n return 9\n',
'closure_target.py': 'def helper():\n return 10\n',
'decoy.py':
'def run():\n return 0\ndef exported():\n return 0\ndef _hidden():\n return 0\n',
'module_rebinding.py': `import module_target as mod
from values import run as mod
def module_rebound():
return mod.helper()
`,
'module_rebinding_reverse.py': `from values import run as mod
import module_target as mod
def module_rebound_reverse():
return mod.helper()
`,
'app.py': `import definitions_star
import values_star
import star
import multi_star
import assigned_star
import imported_star
import blocked_multi
import local_star
import cycle_b
import pkg.a
import namespace_pkg.a
import module_target as mod
def module_definition():
return definitions_star.run()
def module_value():
return values_star.run()
def imported_export():
return star.exported()
def transitive_export():
return multi_star.exported()
def imported_class():
return multi_star.Maker().work()
def private_export():
return multi_star._hidden()
def assigned_exports():
return assigned_star.exported()
def imported_exports():
return imported_star.exported()
def transitive_blocked_exports():
return blocked_multi.exported()
def local_exports():
return local_star.exported()
def cyclic_export():
return cycle_b.cyclic()
def sibling_package():
import pkg.b
return pkg.a.run()
def sibling_namespace_package():
import namespace_pkg.b
return namespace_pkg.a.run()
def named_shadow():
from origin import Maker as pkg
return pkg.a.run()
def unresolved_shadow():
import unavailable as pkg
return pkg.a.run()
def different_package():
import namespace_pkg.b as pkg
return pkg.a.run()
def root_named_module_alias():
import pkg.b as pkg
return pkg.other()
def root_named_package_alias():
import pkg.nested as pkg
return pkg.other()
def root_named_module_shadow():
import pkg.b as pkg
return pkg.a.run()
def root_named_package_shadow():
import pkg.nested as pkg
return pkg.a.run()
def root_named_module_path():
import pkg.b as pkg
return pkg.b.other()
def root_named_package_path():
import pkg.nested as pkg
return pkg.nested.other()
class ClassOnly:
import class_target as class_mod
value = class_mod.helper()
def class_only(self):
return class_mod.helper()
class WithModule:
import class_target as mod
value = mod.helper()
def module_visible(self):
return mod.helper()
def parameter_shadow(self, mod):
return mod.helper()
def namespace_rebound():
import module_target as mod
import class_target as mod
return mod.helper()
def named_rebound():
import module_target as mod
from values import run as mod
return mod.helper()
def named_rebound_reverse():
from values import run as mod
import module_target as mod
return mod.helper()
def conditional_rebound(flag):
if flag:
import module_target as mod
else:
from values import run as mod
return mod.helper()
def conditional_namespaces(flag):
if flag:
import module_target as mod
else:
import class_target as mod
return mod.helper()
def compatible_imports():
import pkg.a
import pkg.b
import pkg.a
pkg.a.run()
return pkg.b.other()
def duplicate_imports():
import module_target as mod
import module_target as mod
return mod.helper()
def enclosing():
import closure_target as enclosed_mod
class Nested:
import class_target as enclosed_mod
def closure_visible(self):
return enclosed_mod.helper()
return Nested
`,
});
result = await runPipelineFromRepo(repoDir, () => {});
}, 60000);
afterAll(() => {
if (repoDir) fs.rmSync(repoDir, { recursive: true, force: true });
});
const callsFrom = (name: string) =>
getRelationships(result, 'CALLS').filter((edge) => edge.source === name);
it('keeps the exact module definition when a class method has the same name', () => {
expect(callsFrom('module_definition').map((edge) => edge.rel.targetId)).toEqual([
'Function:definitions.py:run',
]);
expect(callsFrom('module_value').map((edge) => edge.rel.targetId)).toEqual([
'Variable:values.py:run',
]);
});
it('publishes imported aliases through one or more wildcard barrels', () => {
for (const name of ['imported_export', 'transitive_export', 'local_exports']) {
expect(callsFrom(name).map((edge) => edge.rel.targetId)).toEqual([
'Function:origin.py:helper',
]);
}
expect(
callsFrom('imported_class').map(
(edge) => `${edge.targetLabel}:${edge.targetFilePath}:${edge.target}`,
),
).toEqual(['Class:origin.py:Maker', 'Method:origin.py:work']);
});
it('filters private names and declines explicit module-level __all__ through every hop', () => {
for (const name of [
'private_export',
'assigned_exports',
'imported_exports',
'transitive_blocked_exports',
]) {
expect(callsFrom(name), name).toEqual([]);
}
});
it('propagates wildcard exports through a bounded cycle', () => {
expect(callsFrom('cyclic_export').map((edge) => edge.rel.targetId)).toEqual([
'Function:cycle_a.py:cyclic',
]);
});
it('retains compatible outer submodule paths for regular and namespace packages', () => {
expect(callsFrom('sibling_package').map((edge) => edge.rel.targetId)).toEqual([
'Function:pkg/a.py:run',
]);
expect(callsFrom('sibling_namespace_package').map((edge) => edge.rel.targetId)).toEqual([
'Function:namespace_pkg/a.py:run',
]);
});
it('still suppresses outer paths under named, unresolved, and different namespace imports', () => {
for (const name of ['named_shadow', 'unresolved_shadow', 'different_package']) {
expect(callsFrom(name), name).toEqual([]);
}
});
it('binds root-spelled aliases to the imported module or package', () => {
expect(callsFrom('root_named_module_alias').map((edge) => edge.rel.targetId)).toEqual([
'Function:pkg/b.py:other',
]);
expect(callsFrom('root_named_package_alias').map((edge) => edge.rel.targetId)).toEqual([
'Function:pkg/nested/__init__.py:other',
]);
});
it('does not expose original or outer package paths through root-spelled aliases', () => {
for (const name of [
'root_named_module_shadow',
'root_named_package_shadow',
'root_named_module_path',
'root_named_package_path',
]) {
expect(callsFrom(name), name).toEqual([]);
}
});
it('suppresses conflicting imports in module and function scopes regardless of order', () => {
for (const name of [
'module_rebound',
'module_rebound_reverse',
'namespace_rebound',
'named_rebound',
'named_rebound_reverse',
'conditional_rebound',
'conditional_namespaces',
]) {
expect(callsFrom(name), name).toEqual([]);
}
});
it('preserves compatible dotted imports and repeated identical aliases in one scope', () => {
expect(
callsFrom('compatible_imports')
.map((edge) => edge.rel.targetId)
.sort(),
).toEqual(['Function:pkg/a.py:run', 'Function:pkg/b.py:other']);
expect(callsFrom('duplicate_imports').map((edge) => edge.rel.targetId)).toEqual([
'Function:module_target.py:helper',
]);
});
it('skips class imports when looking outward from methods', () => {
expect(callsFrom('class_only')).toEqual([]);
expect(callsFrom('module_visible').map((edge) => edge.rel.targetId)).toEqual([
'Function:module_target.py:helper',
]);
expect(callsFrom('closure_visible').map((edge) => edge.rel.targetId)).toEqual([
'Function:closure_target.py:helper',
]);
expect(callsFrom('parameter_shadow')).toEqual([]);
});
it('keeps class imports visible within their own class bodies', () => {
const classCalls = getRelationships(result, 'CALLS').filter(
(edge) => edge.rel.targetId === 'Function:class_target.py:helper',
);
expect(classCalls).toHaveLength(2);
expect(classCalls.map((edge) => edge.source).sort()).toEqual(['ClassOnly', 'WithModule']);
});
});