mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
522 lines
17 KiB
TypeScript
522 lines
17 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 { pythonProvider } from '../../../../src/core/ingestion/languages/python.js';
|
|
import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js';
|
|
import {
|
|
getRelationships,
|
|
runPipelineFromRepo,
|
|
writeFixtureRepo,
|
|
type PipelineResult,
|
|
} from '../../../integration/resolvers/helpers.js';
|
|
|
|
describe('Python compiler-local import ownership', () => {
|
|
let repoDir: string;
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-python-local-imports-'));
|
|
writeFixtureRepo(repoDir, {
|
|
'module_target.py': 'def helper(): return 1\n',
|
|
'closure_target.py': 'def helper(): return 2\n',
|
|
'class_target.py': 'def helper(): return 3\n',
|
|
'empty.py': 'value = 1\n',
|
|
'model.py': 'class User:\n def save(self): pass\n',
|
|
'comprehension_source.py': 'def items(): return []\n',
|
|
'type_checking.py': `from typing import TYPE_CHECKING
|
|
if TYPE_CHECKING:
|
|
from model import User
|
|
|
|
def type_checking_receiver(user: User):
|
|
user.save()
|
|
`,
|
|
'conditional_imports.py': `def conditional_named(flag):
|
|
if flag:
|
|
from module_target import helper
|
|
return helper()
|
|
|
|
def conditional_namespace(flag):
|
|
if flag:
|
|
import module_target as mod
|
|
return mod.helper()
|
|
|
|
def try_named():
|
|
try:
|
|
from module_target import helper
|
|
except ImportError:
|
|
pass
|
|
return helper()
|
|
|
|
def try_identical_imports():
|
|
try:
|
|
import module_target as mod
|
|
except ImportError:
|
|
import module_target as mod
|
|
return mod.helper()
|
|
|
|
def conflicting_conditional_imports(flag):
|
|
if flag:
|
|
from module_target import helper
|
|
else:
|
|
from closure_target import helper
|
|
return helper()
|
|
|
|
def mixed_conditional_import(flag, callback):
|
|
if flag:
|
|
from module_target import helper
|
|
else:
|
|
helper = callback
|
|
return helper()
|
|
|
|
def conflicting_try_imports():
|
|
try:
|
|
from module_target import helper
|
|
except ImportError:
|
|
from closure_target import helper
|
|
return helper()
|
|
`,
|
|
'comprehension_iterables.py': `import comprehension_source as y
|
|
|
|
def comprehension_later_iterable(xs):
|
|
return [y for x in xs for y in y.items()]
|
|
|
|
def comprehension_first_iterable():
|
|
return [y for y in y.items()]
|
|
`,
|
|
'receivers.py': `from model import User
|
|
class Holder:
|
|
value = User()
|
|
def bare_class_value(self):
|
|
return value.save()
|
|
def explicit_class_value(self):
|
|
return self.value.save()
|
|
`,
|
|
'parameter_default.py': `from module_target import helper
|
|
def outer_parameter_default(helper):
|
|
def inner_parameter_default(value=helper()):
|
|
from closure_target import helper
|
|
return inner_parameter_default
|
|
`,
|
|
'parameter_annotation.py': `from module_target import helper
|
|
def annotated_parameter(value: helper()):
|
|
from closure_target import helper
|
|
return helper()
|
|
|
|
def outer_annotation(helper):
|
|
def inner_annotation(value: helper()):
|
|
from closure_target import helper
|
|
return inner_annotation
|
|
`,
|
|
'import_default.py': `from module_target import helper
|
|
def outer_import_default():
|
|
from closure_target import helper
|
|
def inner_import_default(value=helper()):
|
|
from class_target import helper
|
|
return inner_import_default
|
|
`,
|
|
'module_default.py': `from module_target import helper
|
|
def module_default(value=helper()):
|
|
from closure_target import helper
|
|
return helper()
|
|
`,
|
|
'class_default.py': `from module_target import helper
|
|
class Defaults:
|
|
from class_target import helper
|
|
def method_default(self, value=helper()):
|
|
from closure_target import helper
|
|
return helper()
|
|
`,
|
|
'receiver_defaults.py': `from model import User
|
|
def outer_receiver_parameter_default(value):
|
|
def inner_receiver_parameter_default(result=value.save()):
|
|
value = User()
|
|
return inner_receiver_parameter_default
|
|
def outer_receiver_import_default():
|
|
value = User()
|
|
def inner_receiver_import_default(result=value.save()):
|
|
value = None
|
|
return inner_receiver_import_default
|
|
`,
|
|
'annotated.py': `from module_target import helper
|
|
helper: object
|
|
def annotation_keeps_module_import():
|
|
return helper()
|
|
def annotation_is_function_local():
|
|
helper()
|
|
helper: object
|
|
`,
|
|
'type_alias.py': `from module_target import helper
|
|
def generic_type_alias():
|
|
helper()
|
|
type helper[T] = T
|
|
`,
|
|
'app.py': `from module_target import helper
|
|
|
|
def before_import():
|
|
return helper()
|
|
from closure_target import helper
|
|
|
|
def after_import():
|
|
from closure_target import helper
|
|
return helper()
|
|
|
|
def unresolved_import():
|
|
from unavailable import helper
|
|
return helper()
|
|
|
|
def missing_export():
|
|
from empty import helper
|
|
return helper()
|
|
|
|
def parameter(helper):
|
|
return helper()
|
|
|
|
def tuple_target():
|
|
helper()
|
|
helper, *rest = values
|
|
|
|
def loop_target():
|
|
helper()
|
|
for helper, other in values: pass
|
|
|
|
def with_target():
|
|
helper()
|
|
with resource as helper: pass
|
|
|
|
def exception_target():
|
|
helper()
|
|
try: pass
|
|
except Exception as helper: pass
|
|
|
|
def match_target(value):
|
|
helper()
|
|
match value:
|
|
case {'name': helper}: pass
|
|
|
|
def augmented_target():
|
|
helper()
|
|
helper += 1
|
|
|
|
def deleted_target():
|
|
helper()
|
|
del helper
|
|
|
|
def walrus_target():
|
|
helper()
|
|
[(helper := item) for item in values]
|
|
|
|
def comprehension_target(values):
|
|
return [helper() for helper in values]
|
|
|
|
def comprehension_does_not_leak(values):
|
|
[helper for helper in values]
|
|
return helper()
|
|
|
|
def comprehension_iterable():
|
|
return [helper for helper in helper()]
|
|
|
|
def install_global():
|
|
global deferred_helper
|
|
from closure_target import helper as deferred_helper
|
|
return deferred_helper()
|
|
|
|
def without_installing():
|
|
return deferred_helper()
|
|
|
|
callback = lambda helper: helper()
|
|
|
|
def enclosing():
|
|
from closure_target import helper
|
|
class Ordered:
|
|
before = helper()
|
|
from class_target import helper
|
|
after = helper()
|
|
def method(self):
|
|
return helper()
|
|
class ClosureOnly:
|
|
value = helper()
|
|
class Deleted:
|
|
from class_target import helper
|
|
before = helper()
|
|
del helper
|
|
after = helper()
|
|
class AnnotationOnly:
|
|
helper: object
|
|
value = helper()
|
|
class AnnotationKeepsImport:
|
|
from class_target import helper
|
|
helper: object
|
|
value = helper()
|
|
return Ordered
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(repoDir, () => {}, { skipGraphPhases: true });
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
if (repoDir) fs.rmSync(repoDir, { recursive: true, force: true });
|
|
});
|
|
|
|
const targets = (caller: string) =>
|
|
getRelationships(result, 'CALLS')
|
|
.filter((edge) => edge.source === caller && edge.target === 'helper')
|
|
.map((edge) => edge.rel.targetId)
|
|
.sort();
|
|
|
|
it('resolves a local import only after it has initialized', () => {
|
|
expect(targets('before_import')).toEqual([]);
|
|
expect(targets('after_import')).toEqual(['Function:closure_target.py:helper']);
|
|
});
|
|
|
|
it('uses a TYPE_CHECKING import to resolve an annotated receiver', () => {
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter((edge) => edge.source === 'type_checking_receiver')
|
|
.map((edge) => edge.rel.targetId),
|
|
).toEqual(['Method:model.py:User.save#0']);
|
|
});
|
|
|
|
it.each(['conditional_named', 'conditional_namespace', 'try_named', 'try_identical_imports'])(
|
|
'resolves the sole import identity in %s',
|
|
(caller) => {
|
|
expect(targets(caller)).toEqual(['Function:module_target.py:helper']);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
'conflicting_conditional_imports',
|
|
'mixed_conditional_import',
|
|
'conflicting_try_imports',
|
|
])('preserves the barrier for competing bindings in %s', (caller) => {
|
|
expect(targets(caller)).toEqual([]);
|
|
});
|
|
|
|
it.each(['unresolved_import', 'missing_export'])(
|
|
'does not fall through an unresolved owned import: %s',
|
|
(caller) => expect(targets(caller)).toEqual([]),
|
|
);
|
|
|
|
it.each([
|
|
'parameter',
|
|
'tuple_target',
|
|
'loop_target',
|
|
'with_target',
|
|
'exception_target',
|
|
'match_target',
|
|
'augmented_target',
|
|
'deleted_target',
|
|
'walrus_target',
|
|
'comprehension_target',
|
|
'callback',
|
|
])('respects the compiler-local binder in %s', (caller) => {
|
|
expect(targets(caller)).toEqual([]);
|
|
});
|
|
|
|
it('isolates comprehension targets and evaluates the first iterable outside their scope', () => {
|
|
expect(targets('comprehension_does_not_leak')).toEqual(['Function:module_target.py:helper']);
|
|
expect(targets('comprehension_iterable')).toEqual(['Function:module_target.py:helper']);
|
|
});
|
|
|
|
it('owns later comprehension targets before evaluating their iterable', () => {
|
|
// CPython raises UnboundLocalError for y.items() in the later for clause:
|
|
// its y is already a comprehension local, not the imported namespace.
|
|
const calls = getRelationships(result, 'CALLS').filter((edge) => edge.target === 'items');
|
|
expect(calls.filter((edge) => edge.source === 'comprehension_later_iterable')).toEqual([]);
|
|
expect(
|
|
calls
|
|
.filter((edge) => edge.source === 'comprehension_first_iterable')
|
|
.map((edge) => edge.rel.targetId),
|
|
).toEqual(['Function:comprehension_source.py:items']);
|
|
});
|
|
|
|
it('does not assume an installer executed for an unrelated global caller', () => {
|
|
expect(targets('without_installing')).toEqual([]);
|
|
expect(targets('install_global')).toEqual(['Function:closure_target.py:helper']);
|
|
});
|
|
|
|
it('uses ordered class locals, module fallback, and class-skipping closure lookup', () => {
|
|
expect(targets('Ordered')).toEqual([
|
|
'Function:class_target.py:helper',
|
|
'Function:module_target.py:helper',
|
|
]);
|
|
expect(targets('method')).toEqual(['Function:closure_target.py:helper']);
|
|
expect(targets('ClosureOnly')).toEqual(['Function:closure_target.py:helper']);
|
|
});
|
|
|
|
it('resumes module fallback after deleting a class binding', () => {
|
|
expect(targets('Deleted')).toEqual([
|
|
'Function:class_target.py:helper',
|
|
'Function:module_target.py:helper',
|
|
]);
|
|
});
|
|
|
|
it('distinguishes annotation-only compiler locals from runtime stores', () => {
|
|
expect(targets('annotation_keeps_module_import')).toEqual(['Function:module_target.py:helper']);
|
|
expect(targets('annotation_is_function_local')).toEqual([]);
|
|
expect(targets('AnnotationOnly')).toEqual(['Function:module_target.py:helper']);
|
|
expect(targets('AnnotationKeepsImport')).toEqual(['Function:class_target.py:helper']);
|
|
});
|
|
|
|
it('treats a generic type alias name as a compiler-local binding', () => {
|
|
expect(targets('generic_type_alias')).toEqual([]);
|
|
});
|
|
|
|
it('skips class locals for bare method names while retaining explicit instance field lookup', () => {
|
|
const calls = getRelationships(result, 'CALLS').filter((edge) => edge.target === 'save');
|
|
expect(calls.filter((edge) => edge.source === 'bare_class_value')).toEqual([]);
|
|
expect(
|
|
calls
|
|
.filter((edge) => edge.source === 'explicit_class_value')
|
|
.map((edge) => edge.rel.targetId),
|
|
).toEqual(['Method:model.py:User.save#0']);
|
|
});
|
|
|
|
it('keeps an outer parameter barrier for a default expression despite an inner import', () => {
|
|
expect(
|
|
getRelationships(result, 'CALLS').filter(
|
|
(edge) => edge.sourceFilePath === 'parameter_default.py' && edge.target === 'helper',
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('resolves a default expression through the outer import before the inner import executes', () => {
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter((edge) => edge.sourceFilePath === 'import_default.py' && edge.target === 'helper')
|
|
.map((edge) => [edge.source, edge.rel.targetId]),
|
|
).toEqual([['outer_import_default', 'Function:closure_target.py:helper']]);
|
|
});
|
|
|
|
it('resolves parameter annotations outside the function body and retains outer parameter barriers', () => {
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter(
|
|
(edge) => edge.sourceFilePath === 'parameter_annotation.py' && edge.target === 'helper',
|
|
)
|
|
.map((edge) => `${edge.rel.sourceId} -> ${edge.rel.targetId}`)
|
|
.sort(),
|
|
).toEqual([
|
|
'File:parameter_annotation.py -> Function:module_target.py:helper',
|
|
'Function:parameter_annotation.py:annotated_parameter -> Function:closure_target.py:helper',
|
|
]);
|
|
});
|
|
|
|
it('resolves default-expression receivers in the enclosing environment', () => {
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter((edge) => edge.sourceFilePath === 'receiver_defaults.py' && edge.target === 'save')
|
|
.map((edge) => [edge.source, edge.rel.targetId]),
|
|
).toEqual([['outer_receiver_import_default', 'Method:model.py:User.save#0']]);
|
|
});
|
|
|
|
it('attributes a module-level default to the file and its body call to the function', () => {
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter((edge) => edge.sourceFilePath === 'module_default.py' && edge.target === 'helper')
|
|
.map((edge) => `${edge.rel.sourceId} -> ${edge.rel.targetId}`)
|
|
.sort(),
|
|
).toEqual([
|
|
'File:module_default.py -> Function:module_target.py:helper',
|
|
'Function:module_default.py:module_default -> Function:closure_target.py:helper',
|
|
]);
|
|
});
|
|
|
|
it('attributes a method default to the class and its body call to the method', () => {
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter((edge) => edge.sourceFilePath === 'class_default.py' && edge.target === 'helper')
|
|
.map((edge) => `${edge.rel.sourceId} -> ${edge.rel.targetId}`)
|
|
.sort(),
|
|
).toEqual([
|
|
'Class:class_default.py:Defaults -> Function:class_target.py:helper',
|
|
'Method:class_default.py:Defaults.method_default#1 -> Function:closure_target.py:helper',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Python binding directives', () => {
|
|
it('separates synthesized dependency callers from default lookup ownership', () => {
|
|
const parsed = extractParsedFile(
|
|
pythonProvider,
|
|
`from fastapi import Depends
|
|
from module_target import helper
|
|
def handler(value=Depends(helper)):
|
|
from closure_target import helper
|
|
return helper()
|
|
`,
|
|
'dependency.py',
|
|
)!;
|
|
const module = parsed.scopes.find((scope) => scope.kind === 'Module')!;
|
|
const handler = parsed.scopes.find((scope) => scope.kind === 'Function')!;
|
|
const dependency = parsed.referenceSites.find(
|
|
(site) => site.name === 'helper' && site.callerScope !== undefined,
|
|
)!;
|
|
expect(dependency.inScope).toBe(module.id);
|
|
expect(dependency.callerScope).toBe(handler.id);
|
|
const body = parsed.referenceSites.find(
|
|
(site) => site.name === 'helper' && site.kind === 'call' && site.atRange.startLine === 5,
|
|
)!;
|
|
expect(body.inScope).toBe(handler.id);
|
|
expect(body.callerScope).toBeUndefined();
|
|
const factory = parsed.referenceSites.find(
|
|
(site) => site.name === 'Depends' && site.kind === 'call',
|
|
)!;
|
|
expect(factory.inScope).toBe(module.id);
|
|
expect(factory.callerScope).toBeUndefined();
|
|
});
|
|
|
|
it.each(['def f():', 'class C:'])(
|
|
'keeps module and child ownership distinct when the file is exactly one %s',
|
|
(header) => {
|
|
const parsed = extractParsedFile(
|
|
pythonProvider,
|
|
`${header}\n from closure_target import helper\n value = helper()`,
|
|
'single.py',
|
|
)!;
|
|
const module = parsed.scopes.find((scope) => scope.kind === 'Module')!;
|
|
const child = parsed.scopes.find((scope) => scope.kind !== 'Module')!;
|
|
expect(module.range).toEqual(child.range);
|
|
expect(module.nameClaims?.map((claim) => claim.name)).toEqual([
|
|
header.startsWith('def') ? 'f' : 'C',
|
|
]);
|
|
expect(module.lookupPolicy?.parentScope).toBeUndefined();
|
|
expect(child.nameClaims?.find((claim) => claim.name === 'helper')?.kind).toBe('import');
|
|
expect(parsed.parsedImports[0]?.declaredAtScope).toBe(child.id);
|
|
},
|
|
);
|
|
|
|
it('routes a global import to the module while preserving its deferred execution', () => {
|
|
const parsed = extractParsedFile(
|
|
pythonProvider,
|
|
`def boot():
|
|
global helper
|
|
from module_target import helper
|
|
return helper()
|
|
`,
|
|
'global.py',
|
|
)!;
|
|
const module = parsed.scopes.find((scope) => scope.kind === 'Module')!;
|
|
expect(parsed.parsedImports[0]?.declaredAtScope).toBe(module.id);
|
|
expect(parsed.parsedImports[0]?.runsOnlyWhenCalled).toBe(true);
|
|
});
|
|
|
|
it('routes nonlocal imports through an intervening function and class to the owning function', () => {
|
|
const parsed = extractParsedFile(
|
|
pythonProvider,
|
|
`def outer():
|
|
helper = None
|
|
def middle():
|
|
class Inner:
|
|
def update(self):
|
|
nonlocal helper
|
|
from module_target import helper
|
|
`,
|
|
'nonlocal.py',
|
|
)!;
|
|
const outer = parsed.scopes.find(
|
|
(scope) => scope.kind === 'Function' && scope.range.startLine === 1,
|
|
)!;
|
|
expect(parsed.parsedImports[0]?.declaredAtScope).toBe(outer.id);
|
|
});
|
|
});
|