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.
@@ -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 decorators = decoratorNames(candidate);
94
- if (decorators.length !== 1)
69
+ const [decorator, ...rest] = decoratorNames(candidate);
70
+ if (decorator === undefined || rest.length > 0)
95
71
  return false;
96
- if (decorators[0] === 'property')
72
+ if (isBuiltinDescriptor(candidate, decorator, 'property'))
97
73
  return true;
98
- if (!['getter', 'setter', 'deleter'].some((kind) => decorators[0] === `${methodName}.${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__' || hasDecorator(fnNode, 'staticmethod'));
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 KNOWN_RECEIVER_DECORATORS.has(expression) || isLocalPropertyAccessor(fnNode, expression);
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
- if (decorators[0] === 'staticmethod')
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
- // Positional shape alone cannot prove a concrete subtype dispatch target.
92
- // The two built-in descriptor decorators are handled by receiver binding.
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
- if (!incompleteInheritanceSubtypeIds.has(subtype.nodeId)) {
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
- const SCHEMA_BUMP = 120;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.53",
3
+ "version": "1.6.13-rc.55",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",