gitnexus 1.6.13-rc.55 → 1.6.13-rc.56

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.
@@ -80,6 +80,8 @@ function bindingOf(identifier) {
80
80
  if (parent === null)
81
81
  return null;
82
82
  const shadow = { node: identifier, effect: 'shadow' };
83
+ if (inCasePattern(identifier))
84
+ return isCaseCapture(identifier) ? shadow : null;
83
85
  switch (parent.type) {
84
86
  case 'assignment':
85
87
  case 'augmented_assignment':
@@ -117,11 +119,9 @@ function bindingOf(identifier) {
117
119
  : null;
118
120
  case 'dotted_name': {
119
121
  const owner = parent.parent;
120
- // `import a.b` binds `a`; `case name:` captures a single name.
122
+ // `import a.b` binds `a`.
121
123
  if (owner?.type === 'import_statement')
122
124
  return parent.firstNamedChild?.id === node.id ? shadow : null;
123
- if (owner?.type === 'case_pattern')
124
- return parent.namedChildCount === 1 ? shadow : null;
125
125
  if (owner?.type !== 'import_from_statement' || !isField(parent, 'name'))
126
126
  return null;
127
127
  return importsFromBuiltins(owner) ? { node: identifier, effect: 'builtin' } : shadow;
@@ -174,9 +174,145 @@ function descriptorBindings(node) {
174
174
  const redirect = redirected.get(`${found.text}@${scopeOf(found)?.id}`) ?? null;
175
175
  add(found.text, { ...binding, redirect });
176
176
  }
177
+ addHelperCalls(tree.rootNode, bindings, redirected);
177
178
  bindingsByTree.set(tree, bindings);
178
179
  return bindings;
179
180
  }
181
+ /**
182
+ * A bare module-level call to a same-file helper that always restores a
183
+ * descriptor name through `global` runs that restore at the call. Record it
184
+ * there as a module binding, so the call's position orders it.
185
+ */
186
+ function addHelperCalls(root, bindings, redirected) {
187
+ // Helper function name -> its `def` and, per descriptor name, the restore
188
+ // its call performs.
189
+ const helpers = new Map();
190
+ for (const [name, list] of bindings) {
191
+ const byHelper = new Map();
192
+ for (const binding of list) {
193
+ if (binding.redirect !== 'global')
194
+ continue;
195
+ const fn = scopeOf(binding.node);
196
+ if (fn === null)
197
+ continue;
198
+ const entry = byHelper.get(fn.id);
199
+ if (entry === undefined)
200
+ byHelper.set(fn.id, { fn, globals: [binding] });
201
+ else
202
+ entry.globals.push(binding);
203
+ }
204
+ for (const { fn, globals } of byHelper.values()) {
205
+ const effect = restoreOnCall(fn, globals);
206
+ const helper = fn.childForFieldName('name')?.text;
207
+ if (effect === null || helper === undefined)
208
+ continue;
209
+ const entry = helpers.get(helper) ?? { fn, restores: new Map() };
210
+ entry.restores.set(name, effect);
211
+ helpers.set(helper, entry);
212
+ }
213
+ }
214
+ if (helpers.size === 0)
215
+ return;
216
+ // A call only denotes the helper when its `def` is the name's one module
217
+ // binding. A local of the same name elsewhere cannot rebind it.
218
+ if (root.descendantsOfType('wildcard_import').length > 0)
219
+ return;
220
+ const bindingCounts = new Map();
221
+ for (const found of root.descendantsOfType('identifier')) {
222
+ if (!helpers.has(found.text) || bindingOf(found) === null)
223
+ continue;
224
+ const scope = scopeOf(found);
225
+ if (scope !== null && redirected.get(`${found.text}@${scope.id}`) !== 'global')
226
+ continue;
227
+ bindingCounts.set(found.text, (bindingCounts.get(found.text) ?? 0) + 1);
228
+ }
229
+ for (const [helper, count] of bindingCounts)
230
+ if (count > 1)
231
+ helpers.delete(helper);
232
+ const touched = new Set();
233
+ for (const statement of root.namedChildren) {
234
+ // Only a call that is the whole statement surely runs when reached.
235
+ if (statement.type !== 'expression_statement' || statement.namedChildCount !== 1)
236
+ continue;
237
+ const call = statement.firstNamedChild;
238
+ const callee = call?.type === 'call' ? call.childForFieldName('function') : null;
239
+ const helper = callee?.type === 'identifier' ? helpers.get(callee.text) : undefined;
240
+ // The call reaches the helper only once its `def` statement has run, and
241
+ // an argument-free call binds only when every parameter is optional.
242
+ if (helper === undefined || statement.startIndex < helper.fn.endIndex)
243
+ continue;
244
+ if ((call?.childForFieldName('arguments')?.namedChildCount ?? 0) > 0)
245
+ continue;
246
+ for (const [name, effect] of helper.restores) {
247
+ const list = bindings.get(name);
248
+ if (list === undefined)
249
+ continue;
250
+ list.push({ node: callee, effect, redirect: null });
251
+ touched.add(list);
252
+ }
253
+ }
254
+ for (const list of touched)
255
+ list.sort((a, b) => a.node.startIndex - b.node.startIndex);
256
+ }
257
+ /**
258
+ * Does this name in a case pattern capture? Value patterns (`a.b`), class
259
+ * names and keyword keys are reads. Every other name is a capture.
260
+ */
261
+ function isCaseCapture(identifier) {
262
+ const parent = identifier.parent;
263
+ if (parent?.type === 'keyword_pattern')
264
+ return parent.firstNamedChild?.id !== identifier.id;
265
+ if (parent?.type !== 'dotted_name')
266
+ return true;
267
+ if (parent.namedChildCount > 1)
268
+ return false;
269
+ return !(parent.parent?.type === 'class_pattern' && parent.parent.firstNamedChild?.id === parent.id);
270
+ }
271
+ /** Is `node` inside a `match` case pattern? */
272
+ function inCasePattern(node) {
273
+ for (let parent = node.parent; parent !== null; parent = parent.parent) {
274
+ if (parent.type === 'case_pattern')
275
+ return true;
276
+ }
277
+ return false;
278
+ }
279
+ /**
280
+ * The restore a call to `fn` always performs on a module global, or `null`.
281
+ * `fn` must be a plain module-level `def` whose body runs on call (not a
282
+ * generator or coroutine), and every `global` binding of the name in it must
283
+ * be a restore in a simple statement that no earlier `return` can skip.
284
+ */
285
+ function restoreOnCall(fn, globals) {
286
+ if (fn.type !== 'function_definition' || fn.parent?.type !== 'module')
287
+ return null;
288
+ if (fn.children.some((child) => child.type === 'async'))
289
+ return null;
290
+ const required = fn
291
+ .childForFieldName('parameters')
292
+ ?.namedChildren.some((param) => param.type === 'identifier' ||
293
+ (param.type === 'typed_parameter' && param.firstNamedChild?.type === 'identifier'));
294
+ if (required === true)
295
+ return null;
296
+ const body = fn.childForFieldName('body');
297
+ // `globals` is in source order, so the last restore is the one that sticks.
298
+ const first = globals[0];
299
+ const last = globals[globals.length - 1];
300
+ if (body === null || first === undefined || last === undefined)
301
+ return null;
302
+ for (const binding of globals) {
303
+ const statement = statementIn(binding.node, body);
304
+ if (binding.effect === 'shadow' ||
305
+ statement === null ||
306
+ !SIMPLE_STATEMENTS.has(statement.type)) {
307
+ return null;
308
+ }
309
+ }
310
+ const escapes = body
311
+ .descendantsOfType(['yield', 'return_statement'])
312
+ .some((node) => scopeOf(node)?.id === fn.id &&
313
+ (node.type === 'yield' || node.startIndex < first.node.startIndex));
314
+ return escapes ? null : last.effect;
315
+ }
180
316
  /**
181
317
  * The scope that owns names bound at `node`: a function or lambda body, a
182
318
  * class body, a comprehension, or the module (`null`). A walrus target skips
@@ -207,6 +343,10 @@ function statementIn(node, body) {
207
343
  current = current.parent;
208
344
  return current.parent === null ? null : current;
209
345
  }
346
+ /** Does `outer` span `node`? */
347
+ function contains(outer, node) {
348
+ return node.startIndex >= outer.startIndex && node.endIndex <= outer.endIndex;
349
+ }
210
350
  /** Can `binding` run before `use` in the same scope, including an earlier
211
351
  * iteration of an enclosing loop? */
212
352
  function mayRunBefore(binding, use, scope) {
@@ -214,8 +354,7 @@ function mayRunBefore(binding, use, scope) {
214
354
  return true;
215
355
  for (let loop = use.parent; loop !== null && loop.id !== scope?.id; loop = loop.parent) {
216
356
  if ((loop.type === 'for_statement' || loop.type === 'while_statement') &&
217
- binding.startIndex >= loop.startIndex &&
218
- binding.endIndex <= loop.endIndex) {
357
+ contains(loop, binding)) {
219
358
  return true;
220
359
  }
221
360
  }
@@ -270,6 +409,13 @@ function lookupName(bindings, use) {
270
409
  const owned = bindings.some((binding) => binding.redirect === null && scopeOf(binding.node)?.id === fn.id);
271
410
  if (!owned)
272
411
  continue;
412
+ // A nested function can rebind this cell whenever it is called; that
413
+ // order is not modelled, so assume it ran.
414
+ const rebound = bindings.some((binding) => binding.effect === 'shadow' &&
415
+ binding.redirect === 'nonlocal' &&
416
+ contains(fn, binding.node));
417
+ if (rebound)
418
+ return 'shadow';
273
419
  // Any binding makes the name local to this function, so the class body
274
420
  // reads that cell. An unbound cell raises NameError, not the builtin.
275
421
  return namespaceState(bindings, fn, use) === 'builtin' ? 'builtin' : 'shadow';
@@ -287,16 +433,14 @@ function lookupName(bindings, use) {
287
433
  // is called; that order is not modelled, so assume it ran.
288
434
  if (binding.redirect === 'nonlocal') {
289
435
  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)
436
+ if (outer !== undefined && contains(outer, binding.node))
294
437
  return 'shadow';
295
438
  }
296
439
  if (binding.redirect === 'global') {
297
440
  // 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.
441
+ // defines it has run. Whether a call happens is unknown, so a rebinding
442
+ // one is assumed. A restore counts only at a proven call site (see
443
+ // `addHelperCalls`).
300
444
  const top = statementIn(binding.node, binding.node.tree.rootNode);
301
445
  if (deferred || (top !== null && mayRunBefore(top, use, null)))
302
446
  return 'shadow';
@@ -807,7 +807,10 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
807
807
  // comments, honors rebinding of builtin descriptor names visible where the
808
808
  // decorator is evaluated, and withholds subtype capacity from descriptor
809
809
  // stacks. Warm v120 captures carry the old verdicts.
810
- const SCHEMA_BUMP = 121;
810
+ // v122 (#3414): Python decorator identity models restoring helper calls and
811
+ // treats match-pattern captures and nested nonlocal rebinds as shadowing.
812
+ // Warm v121 captures carry the old verdicts.
813
+ const SCHEMA_BUMP = 122;
811
814
  const GITNEXUS_PKG_VERSION = (() => {
812
815
  try {
813
816
  // 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.55",
3
+ "version": "1.6.13-rc.56",
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",