mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-18 04:43:36 +00:00
Some checks failed
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Scorecard / Scorecard analysis (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled
* test(accuracy): retire repaired Python accessor gaps * fix(python): connect module accessor resolution * fix(python): resolve calls through module-returning accessors Bind `_pc().target()` and `m = _pc(); m.target()` to the module's function when a local accessor returns an imported module, and count capped-out dropped names in the impact lower bound. Closes #3497 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(python): share module-accessor position key (#3542) - Reuse the capture-side positionKey in the receiver namespace resolver instead of a second line:col helper plus an inline template. - Note why the synthetic { moduleScope, scopes: [] } input is sufficient for createNamespaceTargetCache (sealed disk-backed ParsedFile shape). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(python): reuse subtype-dispatch positionKey (#3542) - Export the existing line:col positionKey from subtype-dispatch instead of defining an identical copy in module-accessors, so both Python capture side channels key positions through one helper. - Apply prettier to the Function-scope indexing line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ci): unique test titles and rebaseline Python benches (#3542) - impact-epistemic-lower-bound: `%j` rendered null, NaN and Infinity all as `null`, so the test-completeness audit rejected the duplicate identities. Use `%s` so every parameterized case has a distinct title. - python-scope: capture fingerprint changes with the new module-accessor capture facts (golden captures were already updated). - receiver-resolution: +4 Python call drops (125 -> 129). The two-step `x = f(); x.m()` form is now counted as an unresolved call instead of vanishing: fastapi-depends `router.get/post` (unknown, x3) and python-method-chain-binding `user.get_city()` (in-program; `User` has no `get_city`, so the site is genuinely unresolvable). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
263 lines
7 KiB
TypeScript
263 lines
7 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 { summarizeUnresolvedReceivers } from '../../../src/core/ingestion/scope-resolution/unresolved-receivers.js';
|
|
import {
|
|
getRelationships,
|
|
runPipelineFromRepo,
|
|
writeFixtureRepo,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
|
|
describe('Python module accessor receivers', () => {
|
|
let repoDir: string;
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-module-accessors-'));
|
|
writeFixtureRepo(repoDir, {
|
|
'pkg/__init__.py': '',
|
|
'pkg/facade.py': 'def target(): return 1\n',
|
|
'pkg/decoy.py': 'def target(): return 2\n',
|
|
'pkg/values.py': 'facade = object()\n',
|
|
'pkg/user.py': `def _pc():
|
|
from pkg import facade
|
|
return facade
|
|
def caller():
|
|
_pc().target()
|
|
def caller_via_local():
|
|
m = _pc()
|
|
m.target()
|
|
def spaced_caller():
|
|
_pc (
|
|
).target()
|
|
`,
|
|
'pkg/control.py': 'from pkg import facade\ndef direct_caller():\n facade.target()\n',
|
|
'pkg/accessors.py': `def lazy():
|
|
from . import facade as module
|
|
return module
|
|
`,
|
|
'pkg/imported.py': `from pkg.accessors import lazy as renamed
|
|
from pkg import decoy as facade
|
|
def imported_direct():
|
|
renamed().target()
|
|
def imported_local():
|
|
m = renamed()
|
|
m.target()
|
|
`,
|
|
'pkg/shadows.py': `from pkg.user import _pc
|
|
def parameter(_pc):
|
|
_pc().target()
|
|
def before_assignment():
|
|
m.target()
|
|
m = _pc()
|
|
def reassigned():
|
|
m = _pc()
|
|
m = object()
|
|
m.target()
|
|
def deleted():
|
|
m = _pc()
|
|
del m
|
|
m.target()
|
|
def sibling():
|
|
m.target()
|
|
def local_name():
|
|
def _pc():
|
|
from pkg import decoy
|
|
return decoy
|
|
_pc().target()
|
|
def later_local():
|
|
_pc().target()
|
|
_pc = object()
|
|
def two_assignments():
|
|
m = _pc()
|
|
m.target()
|
|
m = object()
|
|
m.target()
|
|
def only_sibling_accessor():
|
|
def unrelated():
|
|
def missing():
|
|
from pkg import facade
|
|
return facade
|
|
missing().target()
|
|
`,
|
|
'pkg/rejected.py': `def rebound():
|
|
from pkg import facade
|
|
facade = object()
|
|
return facade
|
|
def ordinary_export():
|
|
from pkg.values import facade
|
|
return facade
|
|
def parameterized(facade):
|
|
from pkg import facade
|
|
return facade
|
|
def asynchronous():
|
|
async def lazy():
|
|
from pkg import facade
|
|
return facade
|
|
lazy().target()
|
|
def rejected_callers():
|
|
rebound().target()
|
|
ordinary_export().target()
|
|
parameterized().target()
|
|
m = rebound()
|
|
m.target()
|
|
def positive_external():
|
|
m = list()
|
|
m.target()
|
|
`,
|
|
'pkg/unsupported.py': `def mixed():
|
|
from pkg import facade
|
|
if flag:
|
|
return facade
|
|
return None
|
|
def unsupported_direct():
|
|
mixed().target()
|
|
def unsupported_local():
|
|
m = mixed()
|
|
m.target()
|
|
def property_only():
|
|
m = mixed()
|
|
value = m.target
|
|
m.target = value
|
|
`,
|
|
'pkg/conditional.py': `def first():
|
|
from pkg import facade
|
|
return facade
|
|
def second():
|
|
from pkg import decoy
|
|
return decoy
|
|
def conditional_modules(flag):
|
|
if flag:
|
|
m = first()
|
|
else:
|
|
m = second()
|
|
m.target()
|
|
def conditional_builtin(flag):
|
|
if flag:
|
|
m = first()
|
|
else:
|
|
m = list()
|
|
m.target()
|
|
def suppressed_assignment():
|
|
from contextlib import suppress
|
|
m = first()
|
|
with suppress(Exception):
|
|
raise RuntimeError()
|
|
m = second()
|
|
m.target()
|
|
def sequential():
|
|
m = first()
|
|
m = second()
|
|
m.target()
|
|
`,
|
|
'pkg/classes.py': `class Service:
|
|
def work(self): return 1
|
|
def factory() -> Service:
|
|
return Service()
|
|
def existing():
|
|
Service().work()
|
|
factory().work()
|
|
item = factory()
|
|
item.work()
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(repoDir, () => {}, { skipGraphPhases: true });
|
|
}, 120_000);
|
|
|
|
afterAll(() => fs.rmSync(repoDir, { recursive: true, force: true }));
|
|
|
|
const calls = () => getRelationships(result, 'CALLS');
|
|
const targetCalls = () => calls().filter((edge) => edge.target === 'target');
|
|
|
|
it('resolves direct, assigned, imported and aliased module accessors by lexical identity', () => {
|
|
expect(
|
|
targetCalls()
|
|
.map((edge) => `${edge.source} -> ${edge.targetFilePath}`)
|
|
.sort(),
|
|
).toEqual([
|
|
'caller -> pkg/facade.py',
|
|
'caller_via_local -> pkg/facade.py',
|
|
'direct_caller -> pkg/facade.py',
|
|
'imported_direct -> pkg/facade.py',
|
|
'imported_local -> pkg/facade.py',
|
|
'local_name -> pkg/decoy.py',
|
|
'sequential -> pkg/decoy.py',
|
|
'spaced_caller -> pkg/facade.py',
|
|
'two_assignments -> pkg/facade.py',
|
|
]);
|
|
expect(
|
|
calls()
|
|
.filter((edge) => edge.target === '_pc' && edge.sourceFilePath === 'pkg/user.py')
|
|
.map((edge) => edge.source)
|
|
.sort(),
|
|
).toEqual(['caller', 'caller_via_local', 'spaced_caller']);
|
|
});
|
|
|
|
it('records each unsupported direct and assigned call once, excluding property accesses', () => {
|
|
const outcomes = result.resolutionOutcomes.filter(
|
|
(outcome) => outcome.filePath === 'pkg/unsupported.py',
|
|
);
|
|
expect(summarizeUnresolvedReceivers(outcomes)).toMatchObject({
|
|
counts: { target: 2 },
|
|
totalSites: 2,
|
|
});
|
|
});
|
|
|
|
it('declines conditional assignment proofs without treating a last-branch built-in as external', () => {
|
|
expect(
|
|
targetCalls()
|
|
.filter((edge) => edge.sourceFilePath === 'pkg/conditional.py')
|
|
.map((edge) => `${edge.source} -> ${edge.targetFilePath}`),
|
|
).toEqual(['sequential -> pkg/decoy.py']);
|
|
const outcomes = result.resolutionOutcomes.filter(
|
|
(outcome) => outcome.filePath === 'pkg/conditional.py',
|
|
);
|
|
expect(
|
|
outcomes.map((outcome) =>
|
|
outcome.kind === 'suppressed'
|
|
? [outcome.range.startLine, outcome.receiverOrigin]
|
|
: undefined,
|
|
),
|
|
).toEqual([
|
|
[12, 'unknown'],
|
|
[18, 'unknown'],
|
|
[25, 'unknown'],
|
|
]);
|
|
expect(summarizeUnresolvedReceivers(outcomes)).toEqual({
|
|
counts: { target: 3 },
|
|
totalSites: 3,
|
|
});
|
|
});
|
|
|
|
it('preserves constructor and class-returning factory dispatch without dropped calls', () => {
|
|
expect(
|
|
calls().filter((edge) => edge.source === 'existing' && edge.target === 'work'),
|
|
).toHaveLength(3);
|
|
expect(
|
|
summarizeUnresolvedReceivers(
|
|
result.resolutionOutcomes.filter((outcome) => outcome.filePath === 'pkg/classes.py'),
|
|
),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('keeps rejected namespace proofs observable and positively external calls separate', () => {
|
|
const summary = summarizeUnresolvedReceivers(
|
|
result.resolutionOutcomes.filter((outcome) => outcome.filePath === 'pkg/rejected.py'),
|
|
);
|
|
expect(summary).toMatchObject({
|
|
counts: { target: 5 },
|
|
totalSites: 5,
|
|
externalCounts: { target: 1 },
|
|
externalSites: 1,
|
|
});
|
|
expect(
|
|
summarizeUnresolvedReceivers(
|
|
result.resolutionOutcomes.filter((outcome) =>
|
|
['pkg/user.py', 'pkg/imported.py'].includes(outcome.filePath),
|
|
),
|
|
),
|
|
).toBeUndefined();
|
|
});
|
|
});
|