mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
parent
b20c8b6ef2
commit
0ef3f28d0e
6 changed files with 81 additions and 8 deletions
|
|
@ -45,6 +45,13 @@ function hasDecorator(fnNode: SyntaxNode, decoratorName: string): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
/** Static-like descriptors do not inject an instance on attribute access. */
|
||||
export function isPythonStaticLikeMethod(fnNode: SyntaxNode): boolean {
|
||||
return (
|
||||
fnNode.childForFieldName('name')?.text === '__new__' || hasDecorator(fnNode, 'staticmethod')
|
||||
);
|
||||
}
|
||||
|
||||
function firstBoundReceiverParameter(parameters: SyntaxNode): SyntaxNode | null {
|
||||
for (let i = 0; i < parameters.namedChildCount; i++) {
|
||||
const child = parameters.namedChild(i);
|
||||
|
|
@ -93,14 +100,12 @@ export interface PythonBoundReceiver {
|
|||
*/
|
||||
export function classifyPythonBoundReceiver(fnNode: SyntaxNode): PythonBoundReceiver | null {
|
||||
const enclosingClass = findEnclosingClassDefinition(fnNode);
|
||||
if (enclosingClass === null || hasDecorator(fnNode, 'staticmethod')) return null;
|
||||
if (enclosingClass === null || isPythonStaticLikeMethod(fnNode)) return null;
|
||||
|
||||
const functionName = fnNode.childForFieldName('name')?.text;
|
||||
// Python applies these descriptor kinds implicitly even without decorators.
|
||||
// __new__ is static-like (its class argument is explicit), while
|
||||
// __init_subclass__ and __class_getitem__ receive the class implicitly.
|
||||
if (functionName === '__new__') return null;
|
||||
|
||||
const params = fnNode.childForFieldName('parameters');
|
||||
if (params === null) return null;
|
||||
const parameter = firstBoundReceiverParameter(params);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { definitionIdPosition } from '../../scope-resolution/utils/definition-id.js';
|
||||
import { classifyPythonBoundReceiver } from './receiver-binding.js';
|
||||
import { classifyPythonBoundReceiver, isPythonStaticLikeMethod } from './receiver-binding.js';
|
||||
|
||||
type PositionTuple = readonly [line: number, column: number];
|
||||
type CallShapeTuple = readonly [line: number, column: number, positionalCount: number];
|
||||
|
|
@ -81,6 +81,9 @@ function positionalCapacity(fnNode: SyntaxNode): number | undefined {
|
|||
const parameters = fnNode.childForFieldName('parameters');
|
||||
if (parameters === null) return undefined;
|
||||
const receiver = classifyPythonBoundReceiver(fnNode)?.parameter;
|
||||
// Plain class functions still receive the instance even with no declared
|
||||
// receiver slot. Without that slot, a zero-argument call is not proven safe.
|
||||
if (receiver === undefined && !isPythonStaticLikeMethod(fnNode)) return undefined;
|
||||
let capacity = 0;
|
||||
let keywordOnly = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -803,7 +803,9 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|||
// v118 (#3398): decorated Python methods with unproven decorator identity no
|
||||
// longer publish subtype positional capacity. Warm v117 side-channel snapshots
|
||||
// would retain that capacity and could emit a false concrete call target.
|
||||
const SCHEMA_BUMP = 118;
|
||||
// v119 (#3396): Python subtype method capacities now omit receiverless ordinary
|
||||
// methods. Warm v118 ParsedFiles would replay a false compatible target.
|
||||
const SCHEMA_BUMP = 119;
|
||||
const GITNEXUS_PKG_VERSION = (() => {
|
||||
try {
|
||||
// package.json sits at gitnexus/package.json — two levels up from
|
||||
|
|
|
|||
|
|
@ -1386,6 +1386,41 @@ describe('Python aliased abstract subtype method', () => {
|
|||
}, 60000);
|
||||
});
|
||||
|
||||
describe('Python receiverless subtype method', () => {
|
||||
it('does not emit a CALLS edge to a method that rejects the injected instance', async () => {
|
||||
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-python-receiverless-subtype-'));
|
||||
try {
|
||||
writeFixtureRepo(repoDir, {
|
||||
'worker.py': [
|
||||
'class Mixin:',
|
||||
' def dispatch(self):',
|
||||
' return self.hook()',
|
||||
'class Worker(Mixin):',
|
||||
' def hook():',
|
||||
' pass',
|
||||
].join('\n'),
|
||||
});
|
||||
const result = await runPipelineFromRepo(repoDir, () => {});
|
||||
expect(
|
||||
getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'dispatch' && call.target === 'hook',
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
getResolutionOutcomes(result).some(
|
||||
(outcome) =>
|
||||
outcome.kind === 'suppressed' &&
|
||||
outcome.filePath === 'worker.py' &&
|
||||
outcome.name === 'hook' &&
|
||||
outcome.reason === 'receiver-unresolved',
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(repoDir, { recursive: true, force: true });
|
||||
}
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Incomplete Python inheritance must not invent an MRO binding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -297,8 +297,9 @@ describe('PARSE_CACHE_VERSION', () => {
|
|||
// Moved 114 -> 115 for #3390: statically known Python call arity.
|
||||
// Moved 115 -> 116 for #3390's Python subtype-dispatch shape side-channel.
|
||||
// Moved 116 -> 117 for #3390's private positional-count side-channel.
|
||||
it('pins SCHEMA_BUMP to 118 so concurrent bumps cannot silently collide (#2766, #3015, #3088, #2885, #3128, #2865, #3130, #1432, #3161, #3179, #3219, #3190, #3253, #3273, #3339, #3354, #3371, #2965, #3390, #3398)', () => {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(118);
|
||||
// Moved 117 -> 118 for #3398 and 118 -> 119 for #3396's subtype capacity correction.
|
||||
it('pins SCHEMA_BUMP to 119 so concurrent bumps cannot silently collide (#2766, #3015, #3088, #2885, #3128, #2865, #3130, #1432, #3161, #3179, #3219, #3190, #3253, #3273, #3339, #3354, #3371, #2965, #3390, #3398, #3396)', () => {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(119);
|
||||
expect(PARSE_CACHE_BUCKET_COUNT).toBe(128);
|
||||
// The PREVIOUS version must fail the reuse gate, not merely differ from the
|
||||
// current one — a hardcoded number outside the conflict hunk rebases cleanly
|
||||
|
|
@ -307,7 +308,7 @@ describe('PARSE_CACHE_VERSION', () => {
|
|||
for (const taken of [
|
||||
59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
|
||||
82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103,
|
||||
104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117,
|
||||
104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118,
|
||||
]) {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(taken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,4 +174,31 @@ describe('Python missing-member subtype argument shapes', () => {
|
|||
),
|
||||
).toBe('compatible');
|
||||
});
|
||||
|
||||
it('does not accept a receiverless ordinary method as a zero-argument instance target', () => {
|
||||
emitPythonScopeCaptures(
|
||||
'class Caller:\n def dispatch(self):\n return self.target()',
|
||||
'caller.py',
|
||||
);
|
||||
emitPythonScopeCaptures(
|
||||
'class Worker:\n def target():\n pass\n @staticmethod\n def static_target():\n pass',
|
||||
'targets.py',
|
||||
);
|
||||
|
||||
const site = { atRange: { startLine: 3, startCol: 15, endLine: 3, endCol: 28 } };
|
||||
const receiverless = { ...candidate(2), parameterCount: 0, requiredParameterCount: 0 };
|
||||
const staticTarget = {
|
||||
...candidate(5),
|
||||
nodeId: 'def:targets.py#5:4:Method:static_target',
|
||||
parameterCount: 0,
|
||||
requiredParameterCount: 0,
|
||||
};
|
||||
|
||||
expect(
|
||||
pythonMissingReceiverSubtypeCandidateCompatibility('caller.py', site, receiverless),
|
||||
).toBe('unknown');
|
||||
expect(
|
||||
pythonMissingReceiverSubtypeCandidateCompatibility('caller.py', site, staticTarget),
|
||||
).toBe('compatible');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue