gitnexus 1.6.13-rc.45 → 1.6.13-rc.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,12 +5,13 @@
5
5
  *
6
6
  * Mirrors the legacy `buildMethodProps` conversion so scope-extracted
7
7
  * defs carry the same arity semantics as the parse-worker path:
8
- * - `self` / `cls` are stripped (consumed by `extractPythonParameters`).
8
+ * - A bound method's first positional receiver is stripped by class and
9
+ * decorator context, independent of spelling; static/free functions keep it.
9
10
  * - Defaulted params contribute to `optionalCount`, flipping
10
11
  * `requiredParameterCount = total − optionalCount`.
11
- * - Variadic (`*args` / `**kwargs`) collapses `parameterCount` to
12
- * `undefined`, which `pythonArityCompatibility` then treats as
13
- * `'unknown'` — keeping the candidate in the registry's lookup set.
12
+ * - Variadic (`*args` / `**kwargs`) leaves both count-only bounds unknown:
13
+ * parameter kinds are not retained, so a required keyword-only argument
14
+ * cannot safely be treated as a positional minimum.
14
15
  * - `parameterTypes` is populated only with real type text, matching
15
16
  * legacy behavior.
16
17
  */
@@ -5,12 +5,13 @@
5
5
  *
6
6
  * Mirrors the legacy `buildMethodProps` conversion so scope-extracted
7
7
  * defs carry the same arity semantics as the parse-worker path:
8
- * - `self` / `cls` are stripped (consumed by `extractPythonParameters`).
8
+ * - A bound method's first positional receiver is stripped by class and
9
+ * decorator context, independent of spelling; static/free functions keep it.
9
10
  * - Defaulted params contribute to `optionalCount`, flipping
10
11
  * `requiredParameterCount = total − optionalCount`.
11
- * - Variadic (`*args` / `**kwargs`) collapses `parameterCount` to
12
- * `undefined`, which `pythonArityCompatibility` then treats as
13
- * `'unknown'` — keeping the candidate in the registry's lookup set.
12
+ * - Variadic (`*args` / `**kwargs`) leaves both count-only bounds unknown:
13
+ * parameter kinds are not retained, so a required keyword-only argument
14
+ * cannot safely be treated as a positional minimum.
14
15
  * - `parameterTypes` is populated only with real type text, matching
15
16
  * legacy behavior.
16
17
  */
@@ -27,6 +27,7 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
27
27
  import { pythonFunctionDefinitionLabel } from './simple-hooks.js';
28
28
  import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
29
29
  import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
30
+ import { beginPythonSubtypeDispatchCapture, recordPythonSimplePositionalCall, recordPythonSubtypeMethodShape, } from './subtype-dispatch.js';
30
31
  const PYTHON_CALLABLE_CAPTURE_OPTIONS = {
31
32
  functionNodeTypes: new Set(['function_definition', 'lambda']),
32
33
  callNodeTypes: new Set(['call']),
@@ -58,6 +59,7 @@ const PYTHON_CALLABLE_CAPTURE_OPTIONS = {
58
59
  },
59
60
  };
60
61
  export function emitPythonScopeCaptures(sourceText, filePath, cachedTree, sourceMeta) {
62
+ beginPythonSubtypeDispatchCapture(filePath);
61
63
  let parseText = sourceText;
62
64
  let tree = cachedTree;
63
65
  let notebookSegments;
@@ -69,6 +71,9 @@ export function emitPythonScopeCaptures(sourceText, filePath, cachedTree, source
69
71
  tree = resolved.tree;
70
72
  notebookSegments = resolved.notebookSegments;
71
73
  }
74
+ const subtypeLineMapper = notebookSegments === undefined
75
+ ? undefined
76
+ : (line) => mapExtractLine(line - 1, notebookSegments) + 1;
72
77
  // Skip the parse when the caller (the scope-resolution orchestrator's
73
78
  // `treeCache`) already produced a Tree for this source — empty under
74
79
  // worker-pool runs, so cache miss = re-parse. The cachedTree parameter
@@ -116,6 +121,7 @@ export function emitPythonScopeCaptures(sourceText, filePath, cachedTree, source
116
121
  }
117
122
  if (Object.keys(grouped).length === 0)
118
123
  continue;
124
+ recordPythonSubtypeCallShape(grouped, nodeMap, filePath, subtypeLineMapper);
119
125
  if (grouped['@import.statement'] !== undefined) {
120
126
  // `@import.statement` is captured directly ON the `import_statement` /
121
127
  // `import_from_statement` node (query: `(import_statement) @import.statement`
@@ -183,6 +189,7 @@ export function emitPythonScopeCaptures(sourceText, filePath, cachedTree, source
183
189
  if (pythonFunctionDefinitionLabel(fnNode, 'Function') === 'Method') {
184
190
  delete grouped['@declaration.function'];
185
191
  grouped['@declaration.method'] = { ...anchorCap, name: '@declaration.method' };
192
+ recordPythonSubtypeMethodShape(filePath, fnNode, subtypeLineMapper);
186
193
  }
187
194
  const arity = computePythonArityMetadata(fnNode);
188
195
  if (arity.parameterCount !== undefined) {
@@ -257,6 +264,31 @@ function remapCaptureMatch(match, segments) {
257
264
  }
258
265
  return next;
259
266
  }
267
+ /**
268
+ * Record fixed positional argument counts only for Python's conservative
269
+ * missing-member subtype fallback. Ordinary reference arity stays unchanged:
270
+ * count-only metadata cannot model Python keyword binding or definition order.
271
+ */
272
+ function recordPythonSubtypeCallShape(grouped, nodeMap, filePath, mapLine) {
273
+ const callTag = ['@reference.call.free', '@reference.call.member'].find((tag) => grouped[tag] !== undefined);
274
+ if (callTag === undefined)
275
+ return;
276
+ // Decorator references use the same call tags but are anchored on a
277
+ // `decorator`, not a `call`, so they intentionally retain their old shape.
278
+ const callNode = nodeMap[callTag];
279
+ if (callNode === undefined || callNode.type !== 'call')
280
+ return;
281
+ const argumentList = callNode.childForFieldName('arguments');
282
+ if (argumentList === null || argumentList.type !== 'argument_list')
283
+ return;
284
+ const args = argumentList.namedChildren.filter((child) => child !== null && child.type !== 'comment');
285
+ if (args.some((arg) => arg.type === 'list_splat' ||
286
+ arg.type === 'dictionary_splat' ||
287
+ arg.type === 'keyword_argument')) {
288
+ return;
289
+ }
290
+ recordPythonSimplePositionalCall(filePath, callNode, args.length, mapLine);
291
+ }
260
292
  /**
261
293
  * Synthesize `@reference.inherits` captures from Python class superclass
262
294
  * lists so the registry-primary scope-resolution path emits EXTENDS edges
@@ -3,13 +3,27 @@
3
3
  * for methods.
4
4
  *
5
5
  * Tree-sitter can't easily express "the first parameter of a function
6
- * defined directly inside a class body" via a single static query.
6
+ * defined in a class suite, including conditional suite branches" via a
7
+ * single static query.
7
8
  * Doing this in code keeps the embedded scope query declarative and
8
9
  * lets us encode the `@classmethod` / `@staticmethod` decorator
9
10
  * awareness that Python's runtime depends on.
10
11
  */
11
12
  import type { CaptureMatch } from '../../../../_shared/index.js';
12
13
  import { type SyntaxNode } from '../../utils/ast-helpers.js';
14
+ export interface PythonBoundReceiver {
15
+ readonly kind: 'instance' | 'class';
16
+ readonly parameter: SyntaxNode;
17
+ readonly name: string;
18
+ readonly className: string;
19
+ }
20
+ /**
21
+ * Classify the parameter that Python's descriptor protocol binds implicitly.
22
+ * Class-suite control flow does not change descriptor ownership, while an
23
+ * intervening function does. Splat and keyword-only parameters cannot name
24
+ * the injected receiver directly and therefore fail closed.
25
+ */
26
+ export declare function classifyPythonBoundReceiver(fnNode: SyntaxNode): PythonBoundReceiver | null;
13
27
  /**
14
28
  * Build a `@type-binding.self` (instance method) or `@type-binding.cls`
15
29
  * (`@classmethod`) match for `fnNode`, or `null` if `fnNode` is not a
@@ -3,7 +3,8 @@
3
3
  * for methods.
4
4
  *
5
5
  * Tree-sitter can't easily express "the first parameter of a function
6
- * defined directly inside a class body" via a single static query.
6
+ * defined in a class suite, including conditional suite branches" via a
7
+ * single static query.
7
8
  * Doing this in code keeps the embedded scope query declarative and
8
9
  * lets us encode the `@classmethod` / `@staticmethod` decorator
9
10
  * awareness that Python's runtime depends on.
@@ -43,75 +44,120 @@ function hasDecorator(fnNode, decoratorName) {
43
44
  }
44
45
  return false;
45
46
  }
46
- function firstNamedParameter(parameters) {
47
+ function firstBoundReceiverParameter(parameters) {
47
48
  for (let i = 0; i < parameters.namedChildCount; i++) {
48
49
  const child = parameters.namedChild(i);
49
50
  if (child === null)
50
51
  continue;
51
- // Skip `*` / `/` markers.
52
- if (child.type === 'positional_separator' || child.type === 'keyword_separator')
52
+ if (child.type === 'comment')
53
53
  continue;
54
- return child;
54
+ // A positional-only separator follows at least one real positional
55
+ // parameter, so encountering it before a candidate is malformed input.
56
+ // A keyword-only separator, *args, or **kwargs means there is no variable
57
+ // that directly receives Python's descriptor-injected instance.
58
+ if (child.type === 'positional_separator' ||
59
+ child.type === 'keyword_separator' ||
60
+ child.type === 'list_splat_pattern' ||
61
+ child.type === 'dictionary_splat_pattern') {
62
+ return null;
63
+ }
64
+ return firstParameterName(child) === null ? null : child;
55
65
  }
56
66
  return null;
57
67
  }
58
68
  function firstParameterName(param) {
59
69
  if (param.type === 'identifier')
60
70
  return param.text;
61
- // typed_parameter / default_parameter / typed_default_parameter:
62
- // first child holds the identifier / pattern.
63
- const ident = param.childForFieldName('name') ?? findIdentifierChild(param);
64
- return ident?.text ?? null;
65
- }
66
- function findIdentifierChild(node) {
67
- for (let i = 0; i < node.namedChildCount; i++) {
68
- const child = node.namedChild(i);
69
- if (child !== null && child.type === 'identifier')
70
- return child;
71
- }
72
- return null;
71
+ // typed_parameter / default_parameter / typed_default_parameter must name a
72
+ // real positional variable. In particular, do not look through a typed
73
+ // list_splat_pattern or dictionary_splat_pattern for its nested identifier.
74
+ const named = param.childForFieldName('name') ?? param.firstNamedChild;
75
+ return named?.type === 'identifier' ? named.text : null;
73
76
  }
74
77
  /**
75
- * Build a `@type-binding.self` (instance method) or `@type-binding.cls`
76
- * (`@classmethod`) match for `fnNode`, or `null` if `fnNode` is not a
77
- * method, is `@staticmethod`, or has no parameters.
78
- *
79
- * The caller is responsible for guaranteeing `fnNode.type ===
80
- * 'function_definition'`.
78
+ * Classify the parameter that Python's descriptor protocol binds implicitly.
79
+ * Class-suite control flow does not change descriptor ownership, while an
80
+ * intervening function does. Splat and keyword-only parameters cannot name
81
+ * the injected receiver directly and therefore fail closed.
81
82
  */
82
- export function synthesizeReceiverTypeBinding(fnNode) {
83
+ export function classifyPythonBoundReceiver(fnNode) {
83
84
  const enclosingClass = findEnclosingClassDefinition(fnNode);
84
- if (enclosingClass === null)
85
+ if (enclosingClass === null || hasDecorator(fnNode, 'staticmethod'))
85
86
  return null;
86
- // Skip @staticmethod-decorated methods (no implicit receiver).
87
- if (hasDecorator(fnNode, 'staticmethod'))
87
+ const functionName = fnNode.childForFieldName('name')?.text;
88
+ // Python applies these descriptor kinds implicitly even without decorators.
89
+ // __new__ is static-like (its class argument is explicit), while
90
+ // __init_subclass__ and __class_getitem__ receive the class implicitly.
91
+ if (functionName === '__new__')
88
92
  return null;
89
- const isClassmethod = hasDecorator(fnNode, 'classmethod');
90
93
  const params = fnNode.childForFieldName('parameters');
91
94
  if (params === null)
92
95
  return null;
93
- const first = firstNamedParameter(params);
94
- if (first === null)
96
+ const parameter = firstBoundReceiverParameter(params);
97
+ if (parameter === null)
98
+ return null;
99
+ const name = firstParameterName(parameter);
100
+ const className = classDefinitionName(enclosingClass);
101
+ if (name === null || className === null)
102
+ return null;
103
+ return {
104
+ kind: hasDecorator(fnNode, 'classmethod') ||
105
+ functionName === '__init_subclass__' ||
106
+ functionName === '__class_getitem__'
107
+ ? 'class'
108
+ : 'instance',
109
+ parameter,
110
+ name,
111
+ className,
112
+ };
113
+ }
114
+ /**
115
+ * `__new__` is static-like for method dispatch, but Python supplies its class
116
+ * argument during construction rather than through descriptor binding. Keep
117
+ * that explicit parameter in arity metadata while still typing its local name.
118
+ */
119
+ function classifyPythonExplicitNewReceiver(fnNode) {
120
+ if (fnNode.childForFieldName('name')?.text !== '__new__')
121
+ return null;
122
+ const enclosingClass = findEnclosingClassDefinition(fnNode);
123
+ const params = fnNode.childForFieldName('parameters');
124
+ if (enclosingClass === null || params === null)
125
+ return null;
126
+ const parameter = firstBoundReceiverParameter(params);
127
+ if (parameter === null)
95
128
  return null;
129
+ const name = firstParameterName(parameter);
96
130
  const className = classDefinitionName(enclosingClass);
97
- if (className === null)
131
+ if (name === null || className === null)
98
132
  return null;
99
- const firstName = firstParameterName(first);
100
- if (firstName === null)
133
+ return { kind: 'class', parameter, name, className };
134
+ }
135
+ /**
136
+ * Build a `@type-binding.self` (instance method) or `@type-binding.cls`
137
+ * (`@classmethod`) match for `fnNode`, or `null` if `fnNode` is not a
138
+ * method, is `@staticmethod`, or has no parameters.
139
+ *
140
+ * The caller is responsible for guaranteeing `fnNode.type ===
141
+ * 'function_definition'`.
142
+ */
143
+ export function synthesizeReceiverTypeBinding(fnNode) {
144
+ const receiver = classifyPythonBoundReceiver(fnNode) ?? classifyPythonExplicitNewReceiver(fnNode);
145
+ if (receiver === null)
101
146
  return null;
102
147
  // Receiver convention: instance methods get `self`, classmethods get `cls`.
103
- // We trust the AST literal name (Python convention is strict in practice).
104
- if (isClassmethod) {
148
+ // The capture tag records the descriptor kind; the variable may use any
149
+ // spelling.
150
+ if (receiver.kind === 'class') {
105
151
  return {
106
- '@type-binding.cls': nodeToCapture('@type-binding.cls', first),
107
- '@type-binding.name': syntheticCapture('@type-binding.name', first, firstName),
108
- '@type-binding.type': syntheticCapture('@type-binding.type', first, className),
152
+ '@type-binding.cls': nodeToCapture('@type-binding.cls', receiver.parameter),
153
+ '@type-binding.name': syntheticCapture('@type-binding.name', receiver.parameter, receiver.name),
154
+ '@type-binding.type': syntheticCapture('@type-binding.type', receiver.parameter, receiver.className),
109
155
  };
110
156
  }
111
157
  return {
112
- '@type-binding.self': nodeToCapture('@type-binding.self', first),
113
- '@type-binding.name': syntheticCapture('@type-binding.name', first, firstName),
114
- '@type-binding.type': syntheticCapture('@type-binding.type', first, className),
158
+ '@type-binding.self': nodeToCapture('@type-binding.self', receiver.parameter),
159
+ '@type-binding.name': syntheticCapture('@type-binding.name', receiver.parameter, receiver.name),
160
+ '@type-binding.type': syntheticCapture('@type-binding.type', receiver.parameter, receiver.className),
115
161
  };
116
162
  }
117
163
  /**
@@ -4,13 +4,27 @@
4
4
  *
5
5
  * The provider is a thin wiring object — Python's specific bits
6
6
  * (super recognizer, LEGB merge precedence, Python's relative-import
7
- * resolver, the simplified MRO walk) plug into `runScopeResolution`.
7
+ * resolver, C3 method resolution) plug into `runScopeResolution`.
8
8
  *
9
9
  * Migration reference: when bringing up the next language
10
10
  * (TypeScript / Java / Kotlin / Ruby), copy this file's structure —
11
11
  * implement the 6 required `ScopeResolver` fields, optionally toggle
12
12
  * the 2 booleans, and register in `scope-resolution/pipeline/registry.ts`.
13
13
  */
14
- import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
14
+ import type { ReferenceSite, SymbolDefinition, TypeRef } from '../../../../_shared/index.js';
15
+ import type { ArityVerdict, ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
16
+ /**
17
+ * Python subtype dispatch is deliberately limited to instance receiver facts.
18
+ * Private names are class-mangled and cannot be matched by their source
19
+ * spelling across an eventual subtype. Argument-shape proof is candidate-level
20
+ * because it needs both the call-site and target-method capture facts.
21
+ */
22
+ export declare function pythonMissingReceiverSubtypeDecision(typeRef: TypeRef, context: {
23
+ readonly receiverBindingIsStatic: boolean | undefined;
24
+ readonly memberName: string;
25
+ readonly callArity: number | undefined;
26
+ }): boolean | 'suppress';
27
+ /** Additive compatibility proof for Python's missing-member subtype candidates. */
28
+ export declare function pythonMissingReceiverSubtypeCandidateCompatibility(callerFilePath: string, callsite: Pick<ReferenceSite, 'atRange'>, candidate: SymbolDefinition): ArityVerdict;
15
29
  declare const pythonScopeResolver: ScopeResolver;
16
30
  export { pythonScopeResolver };
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * The provider is a thin wiring object — Python's specific bits
6
6
  * (super recognizer, LEGB merge precedence, Python's relative-import
7
- * resolver, the simplified MRO walk) plug into `runScopeResolution`.
7
+ * resolver, C3 method resolution) plug into `runScopeResolution`.
8
8
  *
9
9
  * Migration reference: when bringing up the next language
10
10
  * (TypeScript / Java / Kotlin / Ruby), copy this file's structure —
@@ -12,11 +12,40 @@
12
12
  * the 2 booleans, and register in `scope-resolution/pipeline/registry.ts`.
13
13
  */
14
14
  import { SupportedLanguages } from '../../../../_shared/index.js';
15
- import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
15
+ import { buildMro, c3LinearizeStrategy } from '../../scope-resolution/passes/mro.js';
16
16
  import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
17
17
  import { indexOnlyElementType } from '../../type-extractors/shared.js';
18
18
  import { pythonProvider } from '../python.js';
19
19
  import { isPythonImportedModule, pythonNamespaceReceiverPaths, pythonArityCompatibility, pythonMergeBindings, resolvePythonImportTarget, } from './index.js';
20
+ import { applyPythonSubtypeDispatchSideChannel, pythonSubtypeCallPositionalCount, pythonSubtypePositionalCapacity, } from './subtype-dispatch.js';
21
+ /**
22
+ * Python subtype dispatch is deliberately limited to instance receiver facts.
23
+ * Private names are class-mangled and cannot be matched by their source
24
+ * spelling across an eventual subtype. Argument-shape proof is candidate-level
25
+ * because it needs both the call-site and target-method capture facts.
26
+ */
27
+ export function pythonMissingReceiverSubtypeDecision(typeRef, context) {
28
+ if (typeRef.source !== 'self' || context.receiverBindingIsStatic !== false)
29
+ return false;
30
+ const isPrivateName = context.memberName.startsWith('__') && !context.memberName.endsWith('__');
31
+ if (isPrivateName)
32
+ return 'suppress';
33
+ return true;
34
+ }
35
+ /** Additive compatibility proof for Python's missing-member subtype candidates. */
36
+ export function pythonMissingReceiverSubtypeCandidateCompatibility(callerFilePath, callsite, candidate) {
37
+ const positionalCount = pythonSubtypeCallPositionalCount(callerFilePath, callsite.atRange);
38
+ if (positionalCount === undefined)
39
+ return 'unknown';
40
+ const capacity = pythonSubtypePositionalCapacity(candidate);
41
+ const minimum = candidate.requiredParameterCount;
42
+ const maximum = candidate.parameterCount;
43
+ if (capacity === undefined || minimum === undefined || maximum === undefined)
44
+ return 'unknown';
45
+ if (positionalCount < minimum || positionalCount > maximum)
46
+ return 'incompatible';
47
+ return positionalCount <= capacity ? 'compatible' : 'incompatible';
48
+ }
20
49
  const pythonScopeResolver = {
21
50
  // A free call naming a class constructs it: `Service(db).do_work()` (#2708).
22
51
  constructionSyntax: { bare: true },
@@ -55,9 +84,21 @@ const pythonScopeResolver = {
55
84
  // Wrapper kept to honor both contracts without altering the legacy
56
85
  // shape that LanguageProvider.arityCompatibility consumes.
57
86
  arityCompatibility: (callsite, def) => pythonArityCompatibility(def, callsite),
58
- buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
87
+ buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, c3LinearizeStrategy),
59
88
  populateOwners: (parsed) => populateClassOwnedMembers(parsed),
89
+ applyCaptureSideChannel: applyPythonSubtypeDispatchSideChannel,
60
90
  isSuperReceiver: (text) => /^super\s*\(/.test(text),
91
+ // A mixin may call a method supplied only by its eventual concrete class.
92
+ // Resolve the callable that DEFINED the binding, rather than the innermost
93
+ // caller, so a class receiver inherited by a closure cannot masquerade as
94
+ // instance dispatch. The helper also suppresses Python shapes whose exact
95
+ // target cannot be represented by the existing call-site facts.
96
+ resolveMissingReceiverMembersFromSubtypes: pythonMissingReceiverSubtypeDecision,
97
+ missingReceiverSubtypeCandidateCompatibility: (callsite, candidate, context) => pythonMissingReceiverSubtypeCandidateCompatibility(context.callerFilePath, callsite, candidate),
98
+ // Python permits both @staticmethod and @classmethod access through an
99
+ // instance. The graph's generic `isStatic` bit therefore does not mean
100
+ // "unreachable by instance dispatch" for this provider.
101
+ isStaticOnly: () => false,
61
102
  // Subscript route only — Python spells collection views as method calls
62
103
  // (`.values()`), which the compound resolver's call branch already handles.
63
104
  //
@@ -0,0 +1,31 @@
1
+ import type { ParsedFile, SymbolDefinition } from '../../../../_shared/index.js';
2
+ import type { SyntaxNode } from '../../utils/ast-helpers.js';
3
+ type CallShapeTuple = readonly [line: number, column: number, positionalCount: number];
4
+ type CapacityTuple = readonly [line: number, column: number, capacity: number];
5
+ type LineMapper = (line: number) => number;
6
+ /**
7
+ * Python-private capture facts for conservative missing-member subtype dispatch.
8
+ * They stay opaque on `ParsedFile.captureSideChannel`; the public reference and
9
+ * definition schemas intentionally do not gain Python argument-binding fields.
10
+ */
11
+ export interface PythonSubtypeDispatchSideChannel {
12
+ readonly kind: 'python-subtype-dispatch';
13
+ readonly simplePositionalCalls: readonly CallShapeTuple[];
14
+ readonly positionalCapacities: readonly CapacityTuple[];
15
+ }
16
+ /** Reset one file before a fresh capture or a worker snapshot restore. */
17
+ export declare function beginPythonSubtypeDispatchCapture(filePath: string): void;
18
+ /** Record calls whose arguments are all ordinary positional expressions. */
19
+ export declare function recordPythonSimplePositionalCall(filePath: string, callNode: SyntaxNode, positionalCount: number, mapLine?: LineMapper): void;
20
+ /** Record a method's exact fixed positional capacity when the AST proves it. */
21
+ export declare function recordPythonSubtypeMethodShape(filePath: string, fnNode: SyntaxNode, mapLine?: LineMapper): void;
22
+ /** Snapshot one worker file into structured-clone-safe plain data. */
23
+ export declare function collectPythonSubtypeDispatchSideChannel(filePath: string): PythonSubtypeDispatchSideChannel | undefined;
24
+ /** Restore worker/cache facts without reparsing source on the main thread. */
25
+ export declare function applyPythonSubtypeDispatchSideChannel(parsed: ParsedFile): void;
26
+ export declare function pythonSubtypeCallPositionalCount(filePath: string, range: {
27
+ readonly startLine: number;
28
+ readonly startCol: number;
29
+ }): number | undefined;
30
+ export declare function pythonSubtypePositionalCapacity(candidate: SymbolDefinition): number | undefined;
31
+ export {};
@@ -0,0 +1,183 @@
1
+ import { definitionIdPosition } from '../../scope-resolution/utils/definition-id.js';
2
+ import { classifyPythonBoundReceiver } from './receiver-binding.js';
3
+ const simplePositionalCallsByFile = new Map();
4
+ const positionalCapacitiesByFile = new Map();
5
+ const positionKey = (line, column) => `${line}:${column}`;
6
+ const nodePosition = (node, mapLine) => {
7
+ const line = node.startPosition.row + 1;
8
+ return [mapLine?.(line) ?? line, node.startPosition.column];
9
+ };
10
+ /** Reset one file before a fresh capture or a worker snapshot restore. */
11
+ export function beginPythonSubtypeDispatchCapture(filePath) {
12
+ simplePositionalCallsByFile.delete(filePath);
13
+ positionalCapacitiesByFile.delete(filePath);
14
+ }
15
+ /** Record calls whose arguments are all ordinary positional expressions. */
16
+ export function recordPythonSimplePositionalCall(filePath, callNode, positionalCount, mapLine) {
17
+ const [line, column] = nodePosition(callNode, mapLine);
18
+ let sites = simplePositionalCallsByFile.get(filePath);
19
+ if (sites === undefined) {
20
+ sites = new Map();
21
+ simplePositionalCallsByFile.set(filePath, sites);
22
+ }
23
+ sites.set(positionKey(line, column), positionalCount);
24
+ }
25
+ function parameterBindingNode(parameter) {
26
+ if (parameter.type === 'typed_parameter' ||
27
+ parameter.type === 'default_parameter' ||
28
+ parameter.type === 'typed_default_parameter') {
29
+ return parameter.childForFieldName('name') ?? parameter.firstNamedChild ?? parameter;
30
+ }
31
+ return parameter;
32
+ }
33
+ function sameNodePosition(left, right) {
34
+ return (left.startPosition.row === right.startPosition.row &&
35
+ left.startPosition.column === right.startPosition.column &&
36
+ left.endPosition.row === right.endPosition.row &&
37
+ left.endPosition.column === right.endPosition.column);
38
+ }
39
+ /**
40
+ * Count parameters that can receive ordinary positional arguments after
41
+ * Python's descriptor-bound receiver is removed. `*args` and any required
42
+ * keyword-only parameter are deliberately unknown: this successor proves
43
+ * fixed positional calls only.
44
+ */
45
+ function positionalCapacity(fnNode) {
46
+ const parameters = fnNode.childForFieldName('parameters');
47
+ if (parameters === null)
48
+ return undefined;
49
+ const receiver = classifyPythonBoundReceiver(fnNode)?.parameter;
50
+ let capacity = 0;
51
+ let keywordOnly = false;
52
+ for (const parameter of parameters.namedChildren) {
53
+ if (parameter === null || parameter.type === 'comment')
54
+ continue;
55
+ if (receiver !== undefined && sameNodePosition(parameter, receiver))
56
+ continue;
57
+ const binding = parameterBindingNode(parameter);
58
+ if (binding.type === 'positional_separator')
59
+ continue;
60
+ if (binding.type === 'keyword_separator') {
61
+ keywordOnly = true;
62
+ continue;
63
+ }
64
+ if (binding.type === 'dictionary_splat_pattern')
65
+ break;
66
+ if (binding.type === 'list_splat_pattern')
67
+ return undefined;
68
+ if (binding.type !== 'identifier' &&
69
+ parameter.type !== 'default_parameter' &&
70
+ parameter.type !== 'typed_parameter' &&
71
+ parameter.type !== 'typed_default_parameter') {
72
+ return undefined;
73
+ }
74
+ if (keywordOnly) {
75
+ if (parameter.type !== 'default_parameter' && parameter.type !== 'typed_default_parameter') {
76
+ return undefined;
77
+ }
78
+ continue;
79
+ }
80
+ capacity++;
81
+ }
82
+ return capacity;
83
+ }
84
+ /** Record a method's exact fixed positional capacity when the AST proves it. */
85
+ export function recordPythonSubtypeMethodShape(filePath, fnNode, mapLine) {
86
+ const capacity = positionalCapacity(fnNode);
87
+ if (capacity === undefined)
88
+ return;
89
+ const [line, column] = nodePosition(fnNode, mapLine);
90
+ let capacities = positionalCapacitiesByFile.get(filePath);
91
+ if (capacities === undefined) {
92
+ capacities = new Map();
93
+ positionalCapacitiesByFile.set(filePath, capacities);
94
+ }
95
+ capacities.set(positionKey(line, column), capacity);
96
+ }
97
+ function parsePositionKey(key) {
98
+ const match = /^(\d+):(\d+)$/.exec(key);
99
+ if (match === null)
100
+ return undefined;
101
+ return [Number(match[1]), Number(match[2])];
102
+ }
103
+ /** Snapshot one worker file into structured-clone-safe plain data. */
104
+ export function collectPythonSubtypeDispatchSideChannel(filePath) {
105
+ const callSites = [];
106
+ for (const [key, positionalCount] of simplePositionalCallsByFile.get(filePath) ?? []) {
107
+ const position = parsePositionKey(key);
108
+ if (position !== undefined)
109
+ callSites.push([position[0], position[1], positionalCount]);
110
+ }
111
+ const capacities = [];
112
+ for (const [key, capacity] of positionalCapacitiesByFile.get(filePath) ?? []) {
113
+ const position = parsePositionKey(key);
114
+ if (position !== undefined)
115
+ capacities.push([position[0], position[1], capacity]);
116
+ }
117
+ if (callSites.length === 0 && capacities.length === 0)
118
+ return undefined;
119
+ return {
120
+ kind: 'python-subtype-dispatch',
121
+ simplePositionalCalls: callSites,
122
+ positionalCapacities: capacities,
123
+ };
124
+ }
125
+ /** Restore worker/cache facts without reparsing source on the main thread. */
126
+ export function applyPythonSubtypeDispatchSideChannel(parsed) {
127
+ beginPythonSubtypeDispatchCapture(parsed.filePath);
128
+ const data = parsed.captureSideChannel;
129
+ if (data === undefined ||
130
+ data === null ||
131
+ typeof data !== 'object' ||
132
+ data.kind !== 'python-subtype-dispatch' ||
133
+ !Array.isArray(data.simplePositionalCalls) ||
134
+ !Array.isArray(data.positionalCapacities)) {
135
+ return;
136
+ }
137
+ for (const position of data.simplePositionalCalls) {
138
+ if (!validCallShape(position))
139
+ continue;
140
+ let sites = simplePositionalCallsByFile.get(parsed.filePath);
141
+ if (sites === undefined) {
142
+ sites = new Map();
143
+ simplePositionalCallsByFile.set(parsed.filePath, sites);
144
+ }
145
+ sites.set(positionKey(position[0], position[1]), position[2]);
146
+ }
147
+ for (const entry of data.positionalCapacities) {
148
+ if (!validCapacity(entry))
149
+ continue;
150
+ let capacities = positionalCapacitiesByFile.get(parsed.filePath);
151
+ if (capacities === undefined) {
152
+ capacities = new Map();
153
+ positionalCapacitiesByFile.set(parsed.filePath, capacities);
154
+ }
155
+ capacities.set(positionKey(entry[0], entry[1]), entry[2]);
156
+ }
157
+ }
158
+ function validPosition(value) {
159
+ return (value.length === 2 &&
160
+ Number.isInteger(value[0]) &&
161
+ value[0] > 0 &&
162
+ Number.isInteger(value[1]) &&
163
+ value[1] >= 0);
164
+ }
165
+ function validCallShape(value) {
166
+ return validPosition(value.slice(0, 2)) && Number.isInteger(value[2]) && value[2] >= 0;
167
+ }
168
+ function validCapacity(value) {
169
+ return validPosition(value.slice(0, 2)) && Number.isInteger(value[2]) && value[2] >= 0;
170
+ }
171
+ export function pythonSubtypeCallPositionalCount(filePath, range) {
172
+ return simplePositionalCallsByFile
173
+ .get(filePath)
174
+ ?.get(positionKey(range.startLine, range.startCol));
175
+ }
176
+ export function pythonSubtypePositionalCapacity(candidate) {
177
+ const position = definitionIdPosition(candidate.nodeId, candidate.filePath);
178
+ if (position === undefined)
179
+ return undefined;
180
+ return positionalCapacitiesByFile
181
+ .get(candidate.filePath)
182
+ ?.get(positionKey(position.line, position.column));
183
+ }
@@ -30,6 +30,8 @@ import { extractDjangoRoutes } from '../route-extractors/django.js';
30
30
  import { discoverDjangoRootUrls } from '../route-extractors/django-root-discovery.js';
31
31
  import { extractPythonModuleConstants } from '../route-extractors/python-const-resolver.js';
32
32
  import { pythonDecoratorRouteHandlerName } from '../route-extractors/python-decorator-handler.js';
33
+ import { assertCloneable } from '../workers/clone-safety.js';
34
+ import { collectPythonSubtypeDispatchSideChannel } from './python/subtype-dispatch.js';
33
35
  const BUILT_INS = new Set([
34
36
  'print',
35
37
  'len',
@@ -128,6 +130,7 @@ export const pythonProvider = defineLanguage({
128
130
  // full per-hook rationale and the canonical capture vocabulary in
129
131
  // ./python/query.ts (PYTHON_SCOPE_QUERY constant).
130
132
  emitScopeCaptures: emitPythonScopeCaptures,
133
+ collectCaptureSideChannel: (filePath) => assertCloneable(collectPythonSubtypeDispatchSideChannel(filePath)),
131
134
  cfgVisitor: createPythonCfgVisitor(),
132
135
  interpretImport: interpretPythonImport,
133
136
  interpretTypeBinding: interpretPythonTypeBinding,