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.
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +5 -4
- package/dist/core/ingestion/languages/python/arity-metadata.js +5 -4
- package/dist/core/ingestion/languages/python/captures.js +32 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +15 -1
- package/dist/core/ingestion/languages/python/receiver-binding.js +87 -41
- package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -2
- package/dist/core/ingestion/languages/python/scope-resolver.js +44 -3
- package/dist/core/ingestion/languages/python/subtype-dispatch.d.ts +31 -0
- package/dist/core/ingestion/languages/python/subtype-dispatch.js +183 -0
- package/dist/core/ingestion/languages/python.js +3 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +17 -13
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +55 -10
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +5 -4
- package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +6 -7
- package/dist/core/ingestion/scope-resolution/passes/mro.js +30 -7
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +21 -2
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +227 -2
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +16 -4
- package/dist/core/run-analyze.js +45 -1
- package/dist/core/shared-store-analyze.js +2 -3
- package/dist/storage/parse-cache.js +11 -1
- package/package.json +1 -1
|
@@ -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
|
-
* -
|
|
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`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
* -
|
|
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`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
52
|
-
if (child.type === 'positional_separator' || child.type === 'keyword_separator')
|
|
52
|
+
if (child.type === 'comment')
|
|
53
53
|
continue;
|
|
54
|
-
|
|
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
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
94
|
-
if (
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
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',
|
|
107
|
-
'@type-binding.name': syntheticCapture('@type-binding.name',
|
|
108
|
-
'@type-binding.type': syntheticCapture('@type-binding.type',
|
|
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',
|
|
113
|
-
'@type-binding.name': syntheticCapture('@type-binding.name',
|
|
114
|
-
'@type-binding.type': syntheticCapture('@type-binding.type',
|
|
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,
|
|
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 {
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|