gitnexus 1.6.13-rc.53 → 1.6.13-rc.55
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/builtin-descriptors.d.ts +19 -0
- package/dist/core/ingestion/languages/python/builtin-descriptors.js +314 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +7 -0
- package/dist/core/ingestion/languages/python/receiver-binding.js +27 -37
- package/dist/core/ingestion/languages/python/subtype-dispatch.js +4 -13
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +1 -3
- package/dist/storage/parse-cache.js +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a method decorator names one of Python's builtin descriptor
|
|
3
|
+
* types, following CPython's evaluation of the decorator expression.
|
|
4
|
+
*
|
|
5
|
+
* A decorator in a class body is evaluated with LOAD_NAME: the class
|
|
6
|
+
* namespace first, then module globals, then builtins, each as bound at the
|
|
7
|
+
* moment the `def` statement runs. Aliases, dotted names and decorator calls
|
|
8
|
+
* have no known descriptor contract without import resolution, so only bare
|
|
9
|
+
* spellings qualify.
|
|
10
|
+
*/
|
|
11
|
+
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|
12
|
+
declare const BUILTIN_DESCRIPTOR_NAMES: readonly ['classmethod', 'staticmethod', 'property'];
|
|
13
|
+
export type BuiltinDescriptor = (typeof BUILTIN_DESCRIPTOR_NAMES)[number];
|
|
14
|
+
/** Decorator expressions, outermost first. Tree-sitter keeps a trailing
|
|
15
|
+
* comment inside the decorator node, so read the expression child only. */
|
|
16
|
+
export declare function decoratorNames(fnNode: SyntaxNode): string[];
|
|
17
|
+
/** Does `expression` denote a builtin descriptor type (or `kind`) at `fnNode`? */
|
|
18
|
+
export declare function isBuiltinDescriptor(fnNode: SyntaxNode, expression: string, kind?: BuiltinDescriptor): boolean;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a method decorator names one of Python's builtin descriptor
|
|
3
|
+
* types, following CPython's evaluation of the decorator expression.
|
|
4
|
+
*
|
|
5
|
+
* A decorator in a class body is evaluated with LOAD_NAME: the class
|
|
6
|
+
* namespace first, then module globals, then builtins, each as bound at the
|
|
7
|
+
* moment the `def` statement runs. Aliases, dotted names and decorator calls
|
|
8
|
+
* have no known descriptor contract without import resolution, so only bare
|
|
9
|
+
* spellings qualify.
|
|
10
|
+
*/
|
|
11
|
+
const BUILTIN_DESCRIPTOR_NAMES = ['classmethod', 'staticmethod', 'property'];
|
|
12
|
+
const BUILTIN_DESCRIPTORS = new Set(BUILTIN_DESCRIPTOR_NAMES);
|
|
13
|
+
/** Decorator expressions, outermost first. Tree-sitter keeps a trailing
|
|
14
|
+
* comment inside the decorator node, so read the expression child only. */
|
|
15
|
+
export function decoratorNames(fnNode) {
|
|
16
|
+
const parent = fnNode.parent;
|
|
17
|
+
if (parent === null || parent.type !== 'decorated_definition')
|
|
18
|
+
return [];
|
|
19
|
+
const names = [];
|
|
20
|
+
for (const child of parent.namedChildren) {
|
|
21
|
+
if (child.type !== 'decorator')
|
|
22
|
+
continue;
|
|
23
|
+
// An empty name matches nothing, so a malformed decorator stays unknown.
|
|
24
|
+
names.push(child.namedChildren.find((part) => part.type !== 'comment')?.text ?? '');
|
|
25
|
+
}
|
|
26
|
+
return names;
|
|
27
|
+
}
|
|
28
|
+
/** Does `expression` denote a builtin descriptor type (or `kind`) at `fnNode`? */
|
|
29
|
+
export function isBuiltinDescriptor(fnNode, expression, kind) {
|
|
30
|
+
if (kind === undefined ? !BUILTIN_DESCRIPTORS.has(expression) : expression !== kind)
|
|
31
|
+
return false;
|
|
32
|
+
// Decorators are evaluated as the `def` statement runs, so its wrapper is the
|
|
33
|
+
// use site. No statement can rebind the name between stacked decorators.
|
|
34
|
+
const use = fnNode.parent?.type === 'decorated_definition' ? fnNode.parent : fnNode;
|
|
35
|
+
return lookupName(descriptorBindings(fnNode).get(expression) ?? [], use) !== 'shadow';
|
|
36
|
+
}
|
|
37
|
+
const FUNCTION_SCOPES = new Set(['function_definition', 'lambda']);
|
|
38
|
+
const COMPREHENSIONS = new Set([
|
|
39
|
+
'list_comprehension',
|
|
40
|
+
'set_comprehension',
|
|
41
|
+
'dictionary_comprehension',
|
|
42
|
+
'generator_expression',
|
|
43
|
+
]);
|
|
44
|
+
const TARGET_WRAPPERS = new Set([
|
|
45
|
+
'pattern_list',
|
|
46
|
+
'tuple_pattern',
|
|
47
|
+
'list_pattern',
|
|
48
|
+
'list_splat_pattern',
|
|
49
|
+
'dictionary_splat_pattern',
|
|
50
|
+
'as_pattern_target',
|
|
51
|
+
'expression_list',
|
|
52
|
+
'parenthesized_expression',
|
|
53
|
+
]);
|
|
54
|
+
/** Simple statements whose effect always happens once execution reaches them. */
|
|
55
|
+
const SIMPLE_STATEMENTS = new Set([
|
|
56
|
+
'expression_statement',
|
|
57
|
+
'import_statement',
|
|
58
|
+
'import_from_statement',
|
|
59
|
+
'delete_statement',
|
|
60
|
+
]);
|
|
61
|
+
/** Is `node` the `field` child of its parent? */
|
|
62
|
+
function isField(node, field) {
|
|
63
|
+
return node.parent?.childForFieldName(field)?.id === node.id;
|
|
64
|
+
}
|
|
65
|
+
/** Does `statement` import from the `builtins` module? */
|
|
66
|
+
function importsFromBuiltins(statement) {
|
|
67
|
+
return statement?.childForFieldName('module_name')?.text === 'builtins';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The binding this identifier performs, following the binding constructs of
|
|
71
|
+
* Language Reference 4.2.1, or `null` for a plain read.
|
|
72
|
+
*/
|
|
73
|
+
function bindingOf(identifier) {
|
|
74
|
+
let node = identifier;
|
|
75
|
+
let parent = node.parent;
|
|
76
|
+
while (parent !== null && TARGET_WRAPPERS.has(parent.type)) {
|
|
77
|
+
node = parent;
|
|
78
|
+
parent = node.parent;
|
|
79
|
+
}
|
|
80
|
+
if (parent === null)
|
|
81
|
+
return null;
|
|
82
|
+
const shadow = { node: identifier, effect: 'shadow' };
|
|
83
|
+
switch (parent.type) {
|
|
84
|
+
case 'assignment':
|
|
85
|
+
case 'augmented_assignment':
|
|
86
|
+
case 'for_statement':
|
|
87
|
+
case 'for_in_clause':
|
|
88
|
+
return isField(node, 'left') ? shadow : null;
|
|
89
|
+
case 'named_expression':
|
|
90
|
+
case 'function_definition':
|
|
91
|
+
case 'class_definition':
|
|
92
|
+
case 'default_parameter':
|
|
93
|
+
case 'typed_default_parameter':
|
|
94
|
+
return isField(node, 'name') ? shadow : null;
|
|
95
|
+
case 'as_pattern':
|
|
96
|
+
return isField(node, 'alias') ? shadow : null;
|
|
97
|
+
case 'aliased_import': {
|
|
98
|
+
if (!isField(node, 'alias'))
|
|
99
|
+
return null;
|
|
100
|
+
// `from builtins import staticmethod as staticmethod` binds the builtin.
|
|
101
|
+
const source = parent.childForFieldName('name')?.text;
|
|
102
|
+
return importsFromBuiltins(parent.parent) && source === identifier.text
|
|
103
|
+
? { node: identifier, effect: 'builtin' }
|
|
104
|
+
: shadow;
|
|
105
|
+
}
|
|
106
|
+
case 'typed_parameter':
|
|
107
|
+
return node.type === 'identifier' ? shadow : null;
|
|
108
|
+
case 'parameters':
|
|
109
|
+
case 'lambda_parameters':
|
|
110
|
+
return shadow;
|
|
111
|
+
case 'delete_statement':
|
|
112
|
+
return { node: identifier, effect: 'unbind' };
|
|
113
|
+
case 'type':
|
|
114
|
+
return parent.parent?.type === 'type_parameter' ||
|
|
115
|
+
(parent.parent?.type === 'type_alias_statement' && isField(parent, 'left'))
|
|
116
|
+
? shadow
|
|
117
|
+
: null;
|
|
118
|
+
case 'dotted_name': {
|
|
119
|
+
const owner = parent.parent;
|
|
120
|
+
// `import a.b` binds `a`; `case name:` captures a single name.
|
|
121
|
+
if (owner?.type === 'import_statement')
|
|
122
|
+
return parent.firstNamedChild?.id === node.id ? shadow : null;
|
|
123
|
+
if (owner?.type === 'case_pattern')
|
|
124
|
+
return parent.namedChildCount === 1 ? shadow : null;
|
|
125
|
+
if (owner?.type !== 'import_from_statement' || !isField(parent, 'name'))
|
|
126
|
+
return null;
|
|
127
|
+
return importsFromBuiltins(owner) ? { node: identifier, effect: 'builtin' } : shadow;
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const bindingsByTree = new WeakMap();
|
|
134
|
+
/** Every binding of a builtin descriptor name in the file, in source order. */
|
|
135
|
+
function descriptorBindings(node) {
|
|
136
|
+
const tree = node.tree;
|
|
137
|
+
const cached = bindingsByTree.get(tree);
|
|
138
|
+
if (cached !== undefined)
|
|
139
|
+
return cached;
|
|
140
|
+
// `global x` / `nonlocal x` bind nothing themselves; they redirect the
|
|
141
|
+
// declaring scope's own bindings of `x` to an outer namespace.
|
|
142
|
+
const redirected = new Map();
|
|
143
|
+
for (const statement of tree.rootNode.descendantsOfType([
|
|
144
|
+
'global_statement',
|
|
145
|
+
'nonlocal_statement',
|
|
146
|
+
])) {
|
|
147
|
+
const scope = scopeOf(statement)?.id;
|
|
148
|
+
const kind = statement.type === 'global_statement' ? 'global' : 'nonlocal';
|
|
149
|
+
for (const name of statement.namedChildren)
|
|
150
|
+
redirected.set(`${name.text}@${scope}`, kind);
|
|
151
|
+
}
|
|
152
|
+
const bindings = new Map();
|
|
153
|
+
const add = (name, binding) => {
|
|
154
|
+
const list = bindings.get(name);
|
|
155
|
+
if (list === undefined)
|
|
156
|
+
bindings.set(name, [binding]);
|
|
157
|
+
else
|
|
158
|
+
list.push(binding);
|
|
159
|
+
};
|
|
160
|
+
for (const found of tree.rootNode.descendantsOfType(['identifier', 'wildcard_import'])) {
|
|
161
|
+
if (found.type === 'wildcard_import') {
|
|
162
|
+
// `from m import *` binds every public name m defines. Unless m is
|
|
163
|
+
// `builtins`, whether that includes a descriptor name is unknown here.
|
|
164
|
+
const effect = importsFromBuiltins(found.parent) ? 'builtin' : 'shadow';
|
|
165
|
+
for (const name of BUILTIN_DESCRIPTORS)
|
|
166
|
+
add(name, { node: found, effect, redirect: null });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!BUILTIN_DESCRIPTORS.has(found.text))
|
|
170
|
+
continue;
|
|
171
|
+
const binding = bindingOf(found);
|
|
172
|
+
if (binding === null)
|
|
173
|
+
continue;
|
|
174
|
+
const redirect = redirected.get(`${found.text}@${scopeOf(found)?.id}`) ?? null;
|
|
175
|
+
add(found.text, { ...binding, redirect });
|
|
176
|
+
}
|
|
177
|
+
bindingsByTree.set(tree, bindings);
|
|
178
|
+
return bindings;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The scope that owns names bound at `node`: a function or lambda body, a
|
|
182
|
+
* class body, a comprehension, or the module (`null`). A walrus target skips
|
|
183
|
+
* comprehensions, as PEP 572 binds it in the enclosing scope.
|
|
184
|
+
*/
|
|
185
|
+
function scopeOf(node) {
|
|
186
|
+
const skipComprehensions = node.parent?.type === 'named_expression';
|
|
187
|
+
let child = node;
|
|
188
|
+
for (let parent = node.parent; parent !== null; child = parent, parent = parent.parent) {
|
|
189
|
+
if (FUNCTION_SCOPES.has(parent.type)) {
|
|
190
|
+
if (isField(child, 'body') || isField(child, 'parameters'))
|
|
191
|
+
return parent;
|
|
192
|
+
}
|
|
193
|
+
else if (parent.type === 'class_definition') {
|
|
194
|
+
if (isField(child, 'body'))
|
|
195
|
+
return parent;
|
|
196
|
+
}
|
|
197
|
+
else if (COMPREHENSIONS.has(parent.type) && !skipComprehensions) {
|
|
198
|
+
return parent;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/** The statement containing `node` that sits directly in `body`. */
|
|
204
|
+
function statementIn(node, body) {
|
|
205
|
+
let current = node;
|
|
206
|
+
while (current.parent !== null && current.parent.id !== body.id)
|
|
207
|
+
current = current.parent;
|
|
208
|
+
return current.parent === null ? null : current;
|
|
209
|
+
}
|
|
210
|
+
/** Can `binding` run before `use` in the same scope, including an earlier
|
|
211
|
+
* iteration of an enclosing loop? */
|
|
212
|
+
function mayRunBefore(binding, use, scope) {
|
|
213
|
+
if (binding.startIndex < use.startIndex)
|
|
214
|
+
return true;
|
|
215
|
+
for (let loop = use.parent; loop !== null && loop.id !== scope?.id; loop = loop.parent) {
|
|
216
|
+
if ((loop.type === 'for_statement' || loop.type === 'while_statement') &&
|
|
217
|
+
binding.startIndex >= loop.startIndex &&
|
|
218
|
+
binding.endIndex <= loop.endIndex) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* What a module, class or function namespace holds for the name when
|
|
226
|
+
* execution reaches `use`. A `shadow` that may have run wins. A restoring
|
|
227
|
+
* effect (`builtin`, `unbind`) counts only when it is a simple statement
|
|
228
|
+
* directly in the scope body that runs before `use`, so it runs exactly once
|
|
229
|
+
* in order.
|
|
230
|
+
*/
|
|
231
|
+
function namespaceState(bindings, scope, use) {
|
|
232
|
+
const body = scope === null ? null : scope.childForFieldName('body');
|
|
233
|
+
let state = 'unbind';
|
|
234
|
+
for (const binding of bindings) {
|
|
235
|
+
if (binding.redirect !== null || scopeOf(binding.node)?.id !== scope?.id)
|
|
236
|
+
continue;
|
|
237
|
+
if (!mayRunBefore(binding.node, use, scope))
|
|
238
|
+
continue;
|
|
239
|
+
if (binding.effect === 'shadow') {
|
|
240
|
+
state = 'shadow';
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const statement = statementIn(binding.node, body ?? binding.node.tree.rootNode);
|
|
244
|
+
const ordered = binding.node.startIndex < use.startIndex;
|
|
245
|
+
if (ordered && statement !== null && SIMPLE_STATEMENTS.has(statement.type)) {
|
|
246
|
+
state = binding.effect;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return state;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Resolve the decorator name at `use` as a class body does (Language
|
|
253
|
+
* Reference 4.2.2): the class namespace first, then the innermost enclosing
|
|
254
|
+
* function that binds the name, then module globals, then builtins.
|
|
255
|
+
*/
|
|
256
|
+
function lookupName(bindings, use) {
|
|
257
|
+
// Enclosing scopes, innermost first, ending with the module (`null`).
|
|
258
|
+
const chain = [];
|
|
259
|
+
for (let scope = scopeOf(use); scope !== null; scope = scopeOf(scope))
|
|
260
|
+
chain.push(scope);
|
|
261
|
+
chain.push(null);
|
|
262
|
+
const classScope = chain[0]?.type === 'class_definition' ? chain[0] : null;
|
|
263
|
+
if (classScope !== null) {
|
|
264
|
+
const state = namespaceState(bindings, classScope, use);
|
|
265
|
+
if (state !== 'unbind')
|
|
266
|
+
return state;
|
|
267
|
+
}
|
|
268
|
+
const functions = chain.filter((scope) => scope !== null && FUNCTION_SCOPES.has(scope.type));
|
|
269
|
+
for (const fn of functions) {
|
|
270
|
+
const owned = bindings.some((binding) => binding.redirect === null && scopeOf(binding.node)?.id === fn.id);
|
|
271
|
+
if (!owned)
|
|
272
|
+
continue;
|
|
273
|
+
// Any binding makes the name local to this function, so the class body
|
|
274
|
+
// reads that cell. An unbound cell raises NameError, not the builtin.
|
|
275
|
+
return namespaceState(bindings, fn, use) === 'builtin' ? 'builtin' : 'shadow';
|
|
276
|
+
}
|
|
277
|
+
// A class body inside a function runs whenever that function is called,
|
|
278
|
+
// which can be any time after its top-level statement starts. Module state
|
|
279
|
+
// is therefore read at that statement, and any later module override may
|
|
280
|
+
// also have run first.
|
|
281
|
+
const deferred = functions.length > 0;
|
|
282
|
+
const moduleUse = deferred ? (statementIn(use, use.tree.rootNode) ?? use) : use;
|
|
283
|
+
for (const binding of bindings) {
|
|
284
|
+
if (binding.effect !== 'shadow')
|
|
285
|
+
continue;
|
|
286
|
+
// A nested function can rebind an enclosing function's cell whenever it
|
|
287
|
+
// is called; that order is not modelled, so assume it ran.
|
|
288
|
+
if (binding.redirect === 'nonlocal') {
|
|
289
|
+
const outer = functions[functions.length - 1];
|
|
290
|
+
const inside = outer !== undefined &&
|
|
291
|
+
binding.node.startIndex >= outer.startIndex &&
|
|
292
|
+
binding.node.endIndex <= outer.endIndex;
|
|
293
|
+
if (inside)
|
|
294
|
+
return 'shadow';
|
|
295
|
+
}
|
|
296
|
+
if (binding.redirect === 'global') {
|
|
297
|
+
// The function can only be called once the top-level statement that
|
|
298
|
+
// defines it has run. Whether a call happens is unknown, so a restoring
|
|
299
|
+
// `global` delete is ignored and a rebinding one is assumed.
|
|
300
|
+
const top = statementIn(binding.node, binding.node.tree.rootNode);
|
|
301
|
+
if (deferred || (top !== null && mayRunBefore(top, use, null)))
|
|
302
|
+
return 'shadow';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (deferred) {
|
|
306
|
+
const laterOverride = bindings.some((binding) => binding.effect === 'shadow' &&
|
|
307
|
+
binding.redirect === null &&
|
|
308
|
+
scopeOf(binding.node) === null &&
|
|
309
|
+
binding.node.startIndex > moduleUse.startIndex);
|
|
310
|
+
if (laterOverride)
|
|
311
|
+
return 'shadow';
|
|
312
|
+
}
|
|
313
|
+
return namespaceState(bindings, null, moduleUse);
|
|
314
|
+
}
|
|
@@ -13,6 +13,13 @@ import type { CaptureMatch } from '../../../../_shared/index.js';
|
|
|
13
13
|
import { type SyntaxNode } from '../../utils/ast-helpers.js';
|
|
14
14
|
/** Static-like descriptors do not inject an instance on attribute access. */
|
|
15
15
|
export declare function isPythonStaticLikeMethod(fnNode: SyntaxNode): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Can the method's parameter list prove its call shape? Only a plain function
|
|
18
|
+
* or one builtin `staticmethod` / `classmethod` wrapper qualifies. Any other
|
|
19
|
+
* decorator may replace the callable, and a descriptor stack can make it
|
|
20
|
+
* uncallable: `staticmethod(classmethod(f))` yields a classmethod object.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hasPythonProvenCallShape(fnNode: SyntaxNode): boolean;
|
|
16
23
|
export interface PythonBoundReceiver {
|
|
17
24
|
readonly kind: 'instance' | 'class';
|
|
18
25
|
readonly parameter: SyntaxNode;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* awareness that Python's runtime depends on.
|
|
11
11
|
*/
|
|
12
12
|
import { nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
|
|
13
|
+
import { decoratorNames, isBuiltinDescriptor } from './builtin-descriptors.js';
|
|
13
14
|
/** Walk up to the enclosing `class_definition`, ignoring the immediate
|
|
14
15
|
* `decorated_definition` wrapper. Returns `null` when the function is
|
|
15
16
|
* free, lambda-bodied, or nested inside another function. */
|
|
@@ -27,31 +28,6 @@ function findEnclosingClassDefinition(node) {
|
|
|
27
28
|
function classDefinitionName(classNode) {
|
|
28
29
|
return classNode.childForFieldName('name')?.text ?? null;
|
|
29
30
|
}
|
|
30
|
-
/** Syntactic decorator expressions; aliases cannot be identified by spelling. */
|
|
31
|
-
function decoratorNames(fnNode) {
|
|
32
|
-
const parent = fnNode.parent;
|
|
33
|
-
if (parent === null || parent.type !== 'decorated_definition')
|
|
34
|
-
return [];
|
|
35
|
-
const names = [];
|
|
36
|
-
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
37
|
-
const child = parent.namedChild(i);
|
|
38
|
-
if (child === null || child.type !== 'decorator')
|
|
39
|
-
continue;
|
|
40
|
-
const text = child.text.replace(/^@/, '').trim();
|
|
41
|
-
names.push(text);
|
|
42
|
-
}
|
|
43
|
-
return names;
|
|
44
|
-
}
|
|
45
|
-
/** Matches bare and module-qualified decorator spellings. */
|
|
46
|
-
function hasDecorator(fnNode, decoratorName) {
|
|
47
|
-
return decoratorNames(fnNode).some((expression) => {
|
|
48
|
-
const name = expression.split('(')[0].trim();
|
|
49
|
-
return name === decoratorName || name.endsWith(`.${decoratorName}`);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
// These spellings have a known descriptor contract in ordinary Python code.
|
|
53
|
-
// Arbitrary dotted tails, aliases and decorator calls do not.
|
|
54
|
-
const KNOWN_RECEIVER_DECORATORS = new Set(['classmethod', 'staticmethod', 'property']);
|
|
55
31
|
/** Accept a local `@property` accessor chain, skipping only plain unrelated
|
|
56
32
|
* methods. Other intervening class-suite statements may rebind the descriptor,
|
|
57
33
|
* including tuple assignment or control flow. */
|
|
@@ -90,12 +66,12 @@ function isLocalPropertyAccessor(fnNode, expression) {
|
|
|
90
66
|
}
|
|
91
67
|
if (candidate?.childForFieldName('name')?.text !== methodName)
|
|
92
68
|
return false;
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
69
|
+
const [decorator, ...rest] = decoratorNames(candidate);
|
|
70
|
+
if (decorator === undefined || rest.length > 0)
|
|
95
71
|
return false;
|
|
96
|
-
if (
|
|
72
|
+
if (isBuiltinDescriptor(candidate, decorator, 'property'))
|
|
97
73
|
return true;
|
|
98
|
-
if (!['getter', 'setter', 'deleter'].some((kind) =>
|
|
74
|
+
if (!['getter', 'setter', 'deleter'].some((kind) => decorator === `${methodName}.${kind}`)) {
|
|
99
75
|
return false;
|
|
100
76
|
}
|
|
101
77
|
}
|
|
@@ -103,10 +79,25 @@ function isLocalPropertyAccessor(fnNode, expression) {
|
|
|
103
79
|
}
|
|
104
80
|
/** Static-like descriptors do not inject an instance on attribute access. */
|
|
105
81
|
export function isPythonStaticLikeMethod(fnNode) {
|
|
106
|
-
return (fnNode.childForFieldName('name')?.text === '__new__' ||
|
|
82
|
+
return (fnNode.childForFieldName('name')?.text === '__new__' ||
|
|
83
|
+
decoratorNames(fnNode).some((name) => isBuiltinDescriptor(fnNode, name, 'staticmethod')));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Can the method's parameter list prove its call shape? Only a plain function
|
|
87
|
+
* or one builtin `staticmethod` / `classmethod` wrapper qualifies. Any other
|
|
88
|
+
* decorator may replace the callable, and a descriptor stack can make it
|
|
89
|
+
* uncallable: `staticmethod(classmethod(f))` yields a classmethod object.
|
|
90
|
+
*/
|
|
91
|
+
export function hasPythonProvenCallShape(fnNode) {
|
|
92
|
+
const [decorator, ...rest] = decoratorNames(fnNode);
|
|
93
|
+
if (decorator === undefined)
|
|
94
|
+
return true;
|
|
95
|
+
return (rest.length === 0 &&
|
|
96
|
+
(isBuiltinDescriptor(fnNode, decorator, 'staticmethod') ||
|
|
97
|
+
isBuiltinDescriptor(fnNode, decorator, 'classmethod')));
|
|
107
98
|
}
|
|
108
99
|
function isKnownReceiverDecorator(fnNode, expression) {
|
|
109
|
-
return
|
|
100
|
+
return isBuiltinDescriptor(fnNode, expression) || isLocalPropertyAccessor(fnNode, expression);
|
|
110
101
|
}
|
|
111
102
|
function firstBoundReceiverParameter(parameters) {
|
|
112
103
|
for (let i = 0; i < parameters.namedChildCount; i++) {
|
|
@@ -157,9 +148,6 @@ export function classifyPythonBoundReceiver(fnNode) {
|
|
|
157
148
|
return null;
|
|
158
149
|
}
|
|
159
150
|
const functionName = fnNode.childForFieldName('name')?.text;
|
|
160
|
-
// Python applies these descriptor kinds implicitly even without decorators.
|
|
161
|
-
// __new__ is static-like (its class argument is explicit), while
|
|
162
|
-
// __init_subclass__ and __class_getitem__ receive the class implicitly.
|
|
163
151
|
const params = fnNode.childForFieldName('parameters');
|
|
164
152
|
if (params === null)
|
|
165
153
|
return null;
|
|
@@ -171,6 +159,8 @@ export function classifyPythonBoundReceiver(fnNode) {
|
|
|
171
159
|
if (name === null || className === null)
|
|
172
160
|
return null;
|
|
173
161
|
return {
|
|
162
|
+
// Python makes __init_subclass__ and __class_getitem__ implicit
|
|
163
|
+
// classmethods, so they receive the class even without a decorator.
|
|
174
164
|
kind: decorators.includes('classmethod') ||
|
|
175
165
|
functionName === '__init_subclass__' ||
|
|
176
166
|
functionName === '__class_getitem__'
|
|
@@ -219,8 +209,10 @@ export function classifyPythonUncertainReceiver(fnNode) {
|
|
|
219
209
|
// Python applies decorators bottom-up. An outer built-in staticmethod
|
|
220
210
|
// guarantees no implicit receiver even when an inner decorator is opaque.
|
|
221
211
|
// The first parameter remains explicit and can keep its annotation.
|
|
222
|
-
|
|
212
|
+
const outermost = decorators[0];
|
|
213
|
+
if (outermost !== undefined && isBuiltinDescriptor(fnNode, outermost, 'staticmethod')) {
|
|
223
214
|
return null;
|
|
215
|
+
}
|
|
224
216
|
if (decorators.every((name) => isKnownReceiverDecorator(fnNode, name)))
|
|
225
217
|
return null;
|
|
226
218
|
const enclosingClass = findEnclosingClassDefinition(fnNode);
|
|
@@ -342,8 +334,6 @@ export function synthesizeConstructorFieldTypeBindings(fnNode) {
|
|
|
342
334
|
return [];
|
|
343
335
|
if (findEnclosingClassDefinition(fnNode) === null)
|
|
344
336
|
return [];
|
|
345
|
-
if (hasDecorator(fnNode, 'staticmethod') || hasDecorator(fnNode, 'classmethod'))
|
|
346
|
-
return [];
|
|
347
337
|
const receiver = synthesizeReceiverTypeBinding(fnNode);
|
|
348
338
|
const receiverName = receiver?.['@type-binding.self']?.text;
|
|
349
339
|
if (receiverName === undefined)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { definitionIdPosition } from '../../scope-resolution/utils/definition-id.js';
|
|
2
|
-
import { classifyPythonBoundReceiver, isPythonStaticLikeMethod } from './receiver-binding.js';
|
|
2
|
+
import { classifyPythonBoundReceiver, hasPythonProvenCallShape, isPythonStaticLikeMethod, } from './receiver-binding.js';
|
|
3
3
|
const simplePositionalCallsByFile = new Map();
|
|
4
4
|
const positionalCapacitiesByFile = new Map();
|
|
5
5
|
const positionKey = (line, column) => `${line}:${column}`;
|
|
@@ -87,19 +87,10 @@ function positionalCapacity(fnNode) {
|
|
|
87
87
|
}
|
|
88
88
|
/** Record a method's exact fixed positional capacity when the AST proves it. */
|
|
89
89
|
export function recordPythonSubtypeMethodShape(filePath, fnNode, mapLine) {
|
|
90
|
-
// An unknown decorator may replace the function or mark it abstract
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
const wrapper = fnNode.parent;
|
|
94
|
-
if (wrapper?.type === 'decorated_definition' &&
|
|
95
|
-
wrapper.namedChildren.some((child) => {
|
|
96
|
-
if (child.type !== 'decorator')
|
|
97
|
-
return false;
|
|
98
|
-
const name = child.firstNamedChild?.text;
|
|
99
|
-
return name !== 'staticmethod' && name !== 'classmethod';
|
|
100
|
-
})) {
|
|
90
|
+
// An unknown decorator may replace the function or mark it abstract, so
|
|
91
|
+
// its parameter list cannot prove a concrete subtype dispatch target.
|
|
92
|
+
if (!hasPythonProvenCallShape(fnNode))
|
|
101
93
|
return;
|
|
102
|
-
}
|
|
103
94
|
const capacity = positionalCapacity(fnNode);
|
|
104
95
|
if (capacity === undefined)
|
|
105
96
|
return;
|
|
@@ -1892,9 +1892,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
1892
1892
|
if (picked === undefined) {
|
|
1893
1893
|
// This runtime subtype has no proven binding. Preserve
|
|
1894
1894
|
// partial coverage even when a sibling supplies a target.
|
|
1895
|
-
|
|
1896
|
-
missingMemberSubtypeIds.add(subtype.nodeId);
|
|
1897
|
-
}
|
|
1895
|
+
missingMemberSubtypeIds.add(subtype.nodeId);
|
|
1898
1896
|
continue;
|
|
1899
1897
|
}
|
|
1900
1898
|
subtypeTargets.set(picked.nodeId, picked);
|
|
@@ -803,7 +803,11 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|
|
803
803
|
// v120 (#3394): decorated Python method receiver bindings now distinguish
|
|
804
804
|
// unproven decorators from instance receivers. Warm v119 ParsedFiles would
|
|
805
805
|
// replay a fabricated `self` binding or lack the uncertainty marker entirely.
|
|
806
|
-
|
|
806
|
+
// v121 (#3399 follow-up): Python decorator identity now ignores trailing
|
|
807
|
+
// comments, honors rebinding of builtin descriptor names visible where the
|
|
808
|
+
// decorator is evaluated, and withholds subtype capacity from descriptor
|
|
809
|
+
// stacks. Warm v120 captures carry the old verdicts.
|
|
810
|
+
const SCHEMA_BUMP = 121;
|
|
807
811
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
808
812
|
try {
|
|
809
813
|
// package.json sits at gitnexus/package.json — two levels up from
|
package/package.json
CHANGED