ucn 5.4.2 → 5.5.1

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.
Files changed (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +27 -16
  6. package/cli/index.js +45 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +62 -11
  9. package/core/analysis.js +517 -266
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +529 -31
  16. package/core/callers.js +12228 -921
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +472 -66
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +3026 -0
  62. package/core/rust-modules.js +203 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1185 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2265 -112
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
9
+ const { referenceScope, scopeFields } = require('./lexical-scope');
9
10
 
10
11
 
11
12
  const {
@@ -18,9 +19,63 @@ const {
18
19
  paramTypesFromStructured,
19
20
  visitNameNodes,
20
21
  sameNode,
22
+ containsOwnNode,
23
+ parseErrorRegions,
24
+ branchClausesExclusive,
25
+ getCachedNodeList,
21
26
  } = require('./utils');
27
+
28
+ // A function whose OWN body yields is a generator: calling it returns an
29
+ // iterator (an async iterator for `async def`), never a coroutine.
30
+ const PY_YIELD_TYPES = new Set(['yield']);
31
+ const PY_SCOPE_BOUNDARIES = new Set(['function_definition', 'lambda', 'class_definition']);
32
+ function isPythonGenerator(fnNode) {
33
+ const body = fnNode.childForFieldName('body');
34
+ return !!body && body.text.includes('yield') &&
35
+ containsOwnNode(body, PY_YIELD_TYPES, PY_SCOPE_BOUNDARIES);
36
+ }
22
37
  const { PARSE_OPTIONS, safeParse } = require('./index');
23
38
 
39
+ // Statements that run in the namespace of the scope holding them: a def
40
+ // under `if`/`try`/`with`/`for`/`while`/`match` in a class body executes in
41
+ // the class namespace and binds a class attribute (a method), exactly like
42
+ // one written directly in the body (fix #402).
43
+ const PY_CLASS_BODY_COMPOUNDS = new Set(['block', 'if_statement', 'elif_clause', 'else_clause',
44
+ 'try_statement', 'except_clause', 'except_group_clause', 'finally_clause', 'with_statement',
45
+ 'for_statement', 'while_statement', 'match_statement', 'case_clause', 'decorated_definition']);
46
+
47
+ /** The class whose body declares `defNode` (a def or class, decorated or
48
+ * not), through compound statements; null for any other scope. */
49
+ function pythonClassBodyOwner(defNode) {
50
+ for (let node = defNode?.parent; node; node = node.parent) {
51
+ if (node.type === 'class_definition') return node;
52
+ if (!PY_CLASS_BODY_COMPOUNDS.has(node.type)) return null;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /** Function definitions a class body declares (decorated ones as their
58
+ * `decorated_definition`), in source order, through compound statements;
59
+ * nested functions and classes are not entered. */
60
+ function pythonClassBodyDefs(body) {
61
+ const out = [];
62
+ const visit = container => {
63
+ for (let i = 0; i < container.namedChildCount; i++) {
64
+ const child = container.namedChild(i);
65
+ if (child.type === 'function_definition') { out.push(child); continue; }
66
+ if (child.type === 'decorated_definition') {
67
+ const definition = child.childForFieldName('definition');
68
+ if (definition?.type === 'function_definition') out.push(child);
69
+ continue;
70
+ }
71
+ if (child.type === 'class_definition') continue;
72
+ if (PY_CLASS_BODY_COMPOUNDS.has(child.type)) visit(child);
73
+ }
74
+ };
75
+ if (body) visit(body);
76
+ return out;
77
+ }
78
+
24
79
  function parseTree(parser, code) {
25
80
  return safeParse(parser, code, undefined, PARSE_OPTIONS);
26
81
  }
@@ -147,19 +202,9 @@ function _processFunction(node, functions, processedRanges, lines, code) {
147
202
  if (processedRanges.has(rangeKey)) return true;
148
203
  processedRanges.add(rangeKey);
149
204
 
150
- // Skip functions that are inside a class (they're extracted as class members)
151
- let parent = node.parent;
152
- // Handle decorated_definition wrapper
153
- if (parent && parent.type === 'decorated_definition') {
154
- parent = parent.parent;
155
- }
156
- // Check if parent is a class body (block inside class_definition)
157
- if (parent && parent.type === 'block') {
158
- const grandparent = parent.parent;
159
- if (grandparent && grandparent.type === 'class_definition') {
160
- return true; // Skip - this is a class method
161
- }
162
- }
205
+ // Skip functions a class body declares, directly or under its
206
+ // compound statements: they're extracted as class members.
207
+ if (pythonClassBodyOwner(node)) return true;
163
208
 
164
209
  const nameNode = node.childForFieldName('name');
165
210
  const paramsNode = node.childForFieldName('parameters');
@@ -182,6 +227,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
182
227
 
183
228
  // Check for async
184
229
  const isAsync = node.text.trimStart().startsWith('async ');
230
+ const isGenerator = isPythonGenerator(node);
185
231
 
186
232
  // Extract decorators
187
233
  const decorators = extractDecorators(node);
@@ -200,6 +246,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
200
246
  endLine,
201
247
  indent,
202
248
  isAsync,
249
+ ...(isGenerator && { isGenerator }),
203
250
  modifiers: isAsync ? ['async'] : [],
204
251
  ...(returnType && { returnType }),
205
252
  ...(returnedConstructors && { returnedConstructors }),
@@ -243,6 +290,7 @@ function _processClass(node, classes, processedRanges, lines) {
243
290
  const decorators = extractDecorators(node);
244
291
  const bases = extractBases(node);
245
292
  const nameLine = nameNode.startPosition.row + 1;
293
+ const lexicalScope = pythonFunctionLocalScope(node);
246
294
 
247
295
  classes.push({
248
296
  name: nameNode.text,
@@ -253,12 +301,34 @@ function _processClass(node, classes, processedRanges, lines) {
253
301
  ...(docstring && { docstring }),
254
302
  ...(decorators.length > 0 && { decorators }),
255
303
  ...(bases.length > 0 && { extends: bases.join(', ') }),
256
- ...(nameLine !== startLine && { nameLine })
304
+ ...(nameLine !== startLine && { nameLine }),
305
+ ...lexicalScope,
257
306
  });
258
307
  }
259
308
  return true;
260
309
  }
261
310
 
311
+ /**
312
+ * Lexical scope of a class declared inside a function (fix #381): the
313
+ * enclosing function's body. The name is a local of that function, so it
314
+ * is visible only there (including nested functions, which close over it).
315
+ * Module-level and class-body classes are unscoped.
316
+ */
317
+ function pythonFunctionLocalScope(node) {
318
+ for (let parent = node.parent; parent; parent = parent.parent) {
319
+ if (parent.type === 'class_definition') return {};
320
+ if (parent.type === 'function_definition' ||
321
+ parent.type === 'async_function_definition') {
322
+ const body = parent.childForFieldName('body') || parent;
323
+ return {
324
+ lexicalScopeStartLine: body.startPosition.row + 1,
325
+ lexicalScopeEndLine: body.endPosition.row + 1,
326
+ };
327
+ }
328
+ }
329
+ return {};
330
+ }
331
+
262
332
  // Module-level state detection patterns
263
333
  const _STATE_PATTERN = /^(CONFIG|SETTINGS|[A-Z][A-Z0-9_]+|[A-Z][a-zA-Z]*(?:Config|Settings|Options|State|Store|Context))$/;
264
334
  // Pattern for UPPER_CASE constants that may have scalar values (string, number, bool, etc.)
@@ -336,6 +406,475 @@ function _processModuleAssign(node, names) {
336
406
  }
337
407
  }
338
408
 
409
+ /**
410
+ * Module-level names bound exactly once, to another name (fix #389):
411
+ * `Alias = Box`, `Alias = models.Box`. Query time decides whether the
412
+ * target is a class; such an alias then denotes it in annotations and
413
+ * constructor calls. A name bound again at module scope (assignment,
414
+ * augmented assignment, loop/with/except target, import, def, class, walrus,
415
+ * del) or declared `global` in any function is not an alias.
416
+ */
417
+ function pythonModuleValueAliases(root) {
418
+ // Cheap first pass: a module-level statement `Name = other.name` at all
419
+ // (directly, or in a module-level if/try/with block).
420
+ const aliasShaped = (expr) => expr?.type === 'assignment' &&
421
+ expr.childForFieldName('left')?.type === 'identifier' &&
422
+ ['identifier', 'attribute'].includes(expr.childForFieldName('right')?.type);
423
+ let shaped = false;
424
+ const probe = (node) => {
425
+ for (const statement of node.namedChildren) {
426
+ if (shaped) return;
427
+ if (statement.type === 'expression_statement') {
428
+ if (statement.namedChildren.some(aliasShaped)) shaped = true;
429
+ } else if (/^(if_statement|elif_clause|else_clause|try_statement|except_clause|finally_clause|with_statement|block)$/.test(statement.type)) {
430
+ probe(statement);
431
+ }
432
+ }
433
+ };
434
+ probe(root);
435
+ if (!shaped) return [];
436
+ // Every module-level binding of each name (fix #392): an alias
437
+ // assignment, an import, or anything else.
438
+ const bindings = new Map();
439
+ const bind = (name, entry) => {
440
+ if (!name) return;
441
+ if (!bindings.has(name)) bindings.set(name, []);
442
+ bindings.get(name).push(entry);
443
+ };
444
+ const bindTarget = (node) => {
445
+ if (!node) return;
446
+ if (node.type === 'identifier') { bind(node.text, { kind: 'other' }); return; }
447
+ if (['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list', 'expression_list',
448
+ 'parenthesized_expression', 'list_splat_pattern', 'list_splat', 'as_pattern_target'].includes(node.type)) {
449
+ for (const child of node.namedChildren) bindTarget(child);
450
+ }
451
+ };
452
+ const visit = (node) => {
453
+ for (const child of node.namedChildren) {
454
+ switch (child.type) {
455
+ case 'function_definition':
456
+ case 'class_definition':
457
+ bind(child.childForFieldName('name')?.text, { kind: 'other' });
458
+ continue;
459
+ case 'decorated_definition':
460
+ bind(child.childForFieldName('definition')?.childForFieldName('name')?.text, { kind: 'other' });
461
+ continue;
462
+ case 'import_statement':
463
+ case 'future_import_statement':
464
+ for (const part of child.namedChildren) {
465
+ if (part.type === 'aliased_import') bind(part.childForFieldName('alias')?.text, { kind: 'other' });
466
+ else if (part.type === 'dotted_name') bind(part.namedChild(0)?.text, { kind: 'other' });
467
+ }
468
+ continue;
469
+ case 'import_from_statement': {
470
+ const moduleName = child.childForFieldName('module_name')?.text;
471
+ for (const part of child.namedChildren) {
472
+ if (sameNode(part, child.childForFieldName('module_name'))) continue;
473
+ if (part.type === 'wildcard_import') { bind('*', { kind: 'other' }); continue; }
474
+ const imported = part.type === 'aliased_import'
475
+ ? part.childForFieldName('name')?.text : part.type === 'dotted_name' ? part.text : null;
476
+ const local = part.type === 'aliased_import'
477
+ ? part.childForFieldName('alias')?.text : part.type === 'dotted_name'
478
+ ? part.namedChild(part.namedChildCount - 1)?.text : null;
479
+ if (!local) continue;
480
+ // A from-import binds the imported object: one binding
481
+ // site of a class the name may alias.
482
+ bind(local, moduleName && imported && !imported.includes('.')
483
+ ? { kind: 'import', module: moduleName, imported, line: child.startPosition.row + 1 }
484
+ : { kind: 'other' });
485
+ }
486
+ continue;
487
+ }
488
+ case 'expression_statement':
489
+ for (let expr of child.namedChildren) {
490
+ while (expr?.type === 'assignment' || expr?.type === 'augmented_assignment') {
491
+ const left = expr.childForFieldName('left');
492
+ const right = expr.childForFieldName('right');
493
+ if (expr.type === 'assignment' && left?.type === 'identifier' &&
494
+ !expr.childForFieldName('type') &&
495
+ (right?.type === 'identifier' || right?.type === 'attribute') &&
496
+ /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(right.text) &&
497
+ right.text !== left.text) {
498
+ bind(left.text, { kind: 'alias', target: right.text,
499
+ line: expr.startPosition.row + 1 });
500
+ } else if (expr.type === 'assignment' && left?.type === 'identifier' &&
501
+ !expr.childForFieldName('type') && right?.type === 'call' &&
502
+ right.childForFieldName('function')?.type === 'identifier') {
503
+ // `registry = Registry()` (fix #398F): a module value
504
+ // built by calling a name, read when an alias names
505
+ // one of its attributes.
506
+ bind(left.text, { kind: 'constructed',
507
+ callee: right.childForFieldName('function').text,
508
+ line: expr.startPosition.row + 1 });
509
+ } else {
510
+ bindTarget(left);
511
+ }
512
+ expr = expr.type === 'assignment' ? right : null;
513
+ }
514
+ }
515
+ continue;
516
+ case 'delete_statement':
517
+ for (const target of child.namedChildren) bindTarget(target);
518
+ continue;
519
+ case 'for_statement':
520
+ bindTarget(child.childForFieldName('left'));
521
+ break;
522
+ case 'with_statement':
523
+ case 'try_statement':
524
+ for (const target of child.descendantsOfType('as_pattern_target')) bindTarget(target);
525
+ break;
526
+ default:
527
+ break;
528
+ }
529
+ if (child.namedChildCount > 0 && child.type !== 'expression_statement') visit(child);
530
+ }
531
+ };
532
+ visit(root);
533
+ for (const walrus of root.descendantsOfType('named_expression')) {
534
+ let inFunction = false;
535
+ for (let p = walrus.parent; p; p = p.parent) {
536
+ if (p.type === 'function_definition' || p.type === 'class_definition' || p.type === 'lambda') {
537
+ inFunction = true;
538
+ break;
539
+ }
540
+ }
541
+ if (!inFunction) bind(walrus.childForFieldName('name')?.text, { kind: 'other' });
542
+ }
543
+ const declaredGlobal = new Set();
544
+ for (const statement of root.descendantsOfType('global_statement')) {
545
+ for (const name of statement.namedChildren) declaredGlobal.add(name.text);
546
+ }
547
+ const aliases = [];
548
+ const constructed = new Map();
549
+ for (const [name, sites] of bindings) {
550
+ if (declaredGlobal.has(name) || sites.some(site => site.kind === 'other')) continue;
551
+ if (sites.length === 1 && sites[0].kind === 'constructed') {
552
+ constructed.set(name, { name, callee: sites[0].callee, line: sites[0].line });
553
+ continue;
554
+ }
555
+ const assignments = sites.filter(site => site.kind === 'alias');
556
+ if (assignments.length === 0 || sites.some(site => site.kind === 'constructed')) continue;
557
+ if (sites.length === 1) {
558
+ // Bound once (also inside a module-level block, fix #392).
559
+ aliases.push({ name, target: assignments[0].target, line: assignments[0].line });
560
+ continue;
561
+ }
562
+ // Bound on several paths (if/else, try/except): an alias only when
563
+ // every binding site names the same class, decided where it resolves.
564
+ aliases.push({
565
+ name, target: assignments[0].target, line: assignments[0].line,
566
+ alternatives: sites.map(site => site.kind === 'alias'
567
+ ? { target: site.target } : { module: site.module, imported: site.imported }),
568
+ });
569
+ }
570
+ // Constructed values an alias reads an attribute of (`loads =
571
+ // registry.loads`, fix #398F).
572
+ const roots = new Set(aliases.flatMap(alias => [alias.target, ...(alias.alternatives || [])
573
+ .map(alternative => alternative.target)]).filter(Boolean)
574
+ .map(target => String(target).split('.')).filter(parts => parts.length === 2).map(parts => parts[0]));
575
+ const values = [...roots].filter(root => constructed.has(root)).sort().map(root => constructed.get(root));
576
+ if (values.length > 0) Object.defineProperty(aliases, 'constructed', { value: values });
577
+ return aliases;
578
+ }
579
+
580
+ const PY_SCOPE_NODES = new Set(['function_definition', 'class_definition', 'lambda', 'module']);
581
+ const PY_BRANCH_CLAUSES = new Set(['elif_clause', 'else_clause', 'except_clause', 'except_group_clause',
582
+ 'finally_clause', 'case_clause']);
583
+
584
+ /** Does a statement list (descending compound statements, never into
585
+ * nested scopes) bind `name`? */
586
+ function pythonClauseBindsName(node, name) {
587
+ for (const child of node.namedChildren) {
588
+ switch (child.type) {
589
+ case 'function_definition':
590
+ case 'class_definition':
591
+ if (child.childForFieldName('name')?.text === name) return true;
592
+ continue;
593
+ case 'decorated_definition':
594
+ if (child.childForFieldName('definition')?.childForFieldName('name')?.text === name) return true;
595
+ continue;
596
+ case 'import_statement':
597
+ case 'import_from_statement':
598
+ case 'expression_statement':
599
+ case 'delete_statement':
600
+ case 'for_statement':
601
+ case 'with_statement':
602
+ if (child.descendantsOfType('identifier').some(id => id.text === name)) return true;
603
+ if (child.type === 'for_statement' || child.type === 'with_statement') {
604
+ for (const part of child.namedChildren) {
605
+ if ((part.type === 'block' || part.type === 'else_clause') && pythonClauseBindsName(part, name)) return true;
606
+ }
607
+ }
608
+ continue;
609
+ default:
610
+ if (child.type === 'block' || child.type.endsWith('_statement') || PY_BRANCH_CLAUSES.has(child.type)) {
611
+ if (pythonClauseBindsName(child, name)) return true;
612
+ }
613
+ }
614
+ }
615
+ return false;
616
+ }
617
+
618
+ /**
619
+ * Cheap gate for configuration items: a def, class or import inside a
620
+ * branch whose statement binds the same name in another clause. Files
621
+ * without one (nearly all) skip the scope walk.
622
+ */
623
+ function pythonConfigurationCandidateScopes(root, bindingNodes) {
624
+ const scopes = new Set();
625
+ const nodes = bindingNodes || getCachedNodeList(root).nodes.filter(node =>
626
+ node.type === 'function_definition' || node.type === 'class_definition' ||
627
+ node.type === 'import_statement' || node.type === 'import_from_statement');
628
+ for (const node of nodes) {
629
+ // Directly in a scope body: no branch can hold it.
630
+ const holder = node.parent?.type === 'decorated_definition' ? node.parent.parent : node.parent;
631
+ if (holder?.type === 'module' || (holder?.type === 'block' && PY_SCOPE_NODES.has(holder.parent?.type))) continue;
632
+ let names = null;
633
+ const start = node.parent?.type === 'decorated_definition' ? node.parent : node;
634
+ for (let parent = start.parent; parent && !PY_SCOPE_NODES.has(parent.type); parent = parent.parent) {
635
+ const statement = parent.type === 'block' ? null
636
+ : ['if_statement', 'try_statement'].includes(parent.type) ? parent
637
+ : PY_BRANCH_CLAUSES.has(parent.type) ? (parent.type === 'case_clause' ? parent.parent?.parent : parent.parent)
638
+ : null;
639
+ if (!statement) continue;
640
+ if (!names) {
641
+ if (node.type === 'function_definition' || node.type === 'class_definition') {
642
+ names = [node.childForFieldName('name')?.text].filter(Boolean);
643
+ } else {
644
+ names = [];
645
+ for (const part of node.namedChildren) {
646
+ if (part.type === 'aliased_import') names.push(part.childForFieldName('alias')?.text);
647
+ else if (part.type === 'dotted_name' && !sameNode(part, node.childForFieldName('module_name'))) {
648
+ names.push(node.type === 'import_statement' ? part.namedChild(0)?.text
649
+ : part.namedChild(part.namedChildCount - 1)?.text);
650
+ }
651
+ }
652
+ names = names.filter(Boolean);
653
+ }
654
+ if (names.length === 0) break;
655
+ }
656
+ // Another clause of this statement (or its match body) binding
657
+ // one of the names.
658
+ const holder = statement.type === 'match_statement' ? statement.childForFieldName('body') : statement;
659
+ let found = false;
660
+ for (const clause of holder?.namedChildren || []) {
661
+ if (clause.startIndex <= node.startIndex && clause.endIndex >= node.endIndex) continue;
662
+ if (clause.type !== 'block' && !PY_BRANCH_CLAUSES.has(clause.type)) continue;
663
+ if (names.some(name => pythonClauseBindsName(clause, name))) { found = true; break; }
664
+ }
665
+ if (found) {
666
+ let scope = statement.parent;
667
+ while (scope && !PY_SCOPE_NODES.has(scope.type)) scope = scope.parent;
668
+ if (scope) scopes.add(scope);
669
+ break;
670
+ }
671
+ }
672
+ }
673
+ return scopes;
674
+ }
675
+
676
+ /**
677
+ * Configuration items (fix #398F): a name bound in one scope (module, class
678
+ * body or function body) only in mutually exclusive branches of `if` /
679
+ * `try` / `match` statements - `if sys.platform == "win32": def getchar` /
680
+ * `else: def getchar`, `try: from ._speedups import f` / `except
681
+ * ImportError: def f` - is ONE item with an alternative per configuration.
682
+ * Every binding of the name in that scope must sit in a different exclusive
683
+ * branch (a later unconditional rebinding, a loop target or a `global` /
684
+ * `nonlocal` write breaks the item), and one of them must be a def, class or
685
+ * import. Sites carry their branch path (statement line + clause tag, outer
686
+ * first); `clauses` holds the line range of every clause of those
687
+ * statements, so a call inside another branch is placed without the tree.
688
+ */
689
+ function pythonConfigurationItems(root, bindingNodes = null, reboundNodes = null) {
690
+ const candidateScopes = pythonConfigurationCandidateScopes(root, bindingNodes);
691
+ if (candidateScopes.size === 0) return [];
692
+ const scopes = new Map(); // scope line -> Map(name -> sites)
693
+ const clauseRanges = new Map(); // statement line -> [[tag, start, end]]
694
+ const rebound = new Set(); // `global` / `nonlocal` names
695
+ const lineOf = node => node.startPosition.row + 1;
696
+ const endOf = node => node.endPosition.row + 1;
697
+ const add = (scope, name, kind, line, branch) => {
698
+ if (!name) return;
699
+ let names = scopes.get(scope);
700
+ if (!names) scopes.set(scope, names = new Map());
701
+ if (!names.has(name)) names.set(name, []);
702
+ names.get(name).push({ kind, line, branch });
703
+ };
704
+ const bindTarget = (scope, node, branch) => {
705
+ if (!node) return;
706
+ if (node.type === 'identifier') { add(scope, node.text, 'value', lineOf(node), branch); return; }
707
+ if (['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list', 'expression_list',
708
+ 'parenthesized_expression', 'list_splat_pattern', 'list_splat', 'as_pattern_target'].includes(node.type)) {
709
+ for (const child of node.namedChildren) bindTarget(scope, child, branch);
710
+ }
711
+ };
712
+ const noteClauses = (statement, clauses) => {
713
+ clauseRanges.set(lineOf(statement), clauses.map(([tag, node]) => [tag, lineOf(node), endOf(node)]));
714
+ };
715
+ const visitBlock = (block, scope, branch) => {
716
+ if (!block) return;
717
+ for (const child of block.namedChildren) visitStatement(child, scope, branch);
718
+ };
719
+ const visitStatement = (node, scope, branch) => {
720
+ switch (node.type) {
721
+ case 'decorated_definition': {
722
+ const definition = node.childForFieldName('definition');
723
+ if (!definition) return;
724
+ add(scope, definition.childForFieldName('name')?.text,
725
+ definition.type === 'class_definition' ? 'class' : 'def', lineOf(node), branch);
726
+ return;
727
+ }
728
+ case 'function_definition':
729
+ case 'class_definition':
730
+ add(scope, node.childForFieldName('name')?.text,
731
+ node.type === 'class_definition' ? 'class' : 'def', lineOf(node), branch);
732
+ return;
733
+ case 'import_statement':
734
+ for (const part of node.namedChildren) {
735
+ if (part.type === 'aliased_import') add(scope, part.childForFieldName('alias')?.text, 'import', lineOf(node), branch);
736
+ else if (part.type === 'dotted_name') add(scope, part.namedChild(0)?.text, 'import', lineOf(node), branch);
737
+ }
738
+ return;
739
+ case 'import_from_statement': {
740
+ const moduleName = node.childForFieldName('module_name');
741
+ for (const part of node.namedChildren) {
742
+ if (moduleName && part.id === moduleName.id) continue;
743
+ if (part.type === 'wildcard_import') { add(scope, '*', 'import', lineOf(node), branch); continue; }
744
+ const local = part.type === 'aliased_import' ? part.childForFieldName('alias')?.text
745
+ : part.type === 'dotted_name' ? part.namedChild(part.namedChildCount - 1)?.text : null;
746
+ add(scope, local, 'import', lineOf(node), branch);
747
+ }
748
+ return;
749
+ }
750
+ case 'global_statement':
751
+ case 'nonlocal_statement':
752
+ return;
753
+ case 'expression_statement':
754
+ for (let expr of node.namedChildren) {
755
+ while (expr?.type === 'assignment' || expr?.type === 'augmented_assignment') {
756
+ bindTarget(scope, expr.childForFieldName('left'), branch);
757
+ expr = expr.type === 'assignment' ? expr.childForFieldName('right') : null;
758
+ }
759
+ }
760
+ return;
761
+ case 'delete_statement':
762
+ for (const target of node.namedChildren) bindTarget(scope, target, branch);
763
+ return;
764
+ case 'if_statement': {
765
+ const line = lineOf(node);
766
+ const consequence = node.childForFieldName('consequence');
767
+ const clauses = consequence ? [['if', consequence]] : [];
768
+ let elif = 0;
769
+ for (const alternative of node.childrenForFieldName('alternative')) {
770
+ if (alternative.type === 'elif_clause') clauses.push([`elif:${elif++}`, alternative]);
771
+ else if (alternative.type === 'else_clause') clauses.push(['else', alternative]);
772
+ }
773
+ noteClauses(node, clauses);
774
+ // `if TYPE_CHECKING:` branches exist for type checkers, not
775
+ // for one runtime configuration: their names form no item.
776
+ const typeChecking = /^(?:\w+\.)?TYPE_CHECKING$/.test(node.childForFieldName('condition')?.text || '');
777
+ for (const [tag, clause] of clauses) {
778
+ const block = clause.type === 'block' ? clause
779
+ : clause.childForFieldName('consequence') || clause.childForFieldName('body');
780
+ visitBlock(block, scope, [...branch, typeChecking ? [line, tag, 'type-checking'] : [line, tag]]);
781
+ }
782
+ return;
783
+ }
784
+ case 'try_statement': {
785
+ const line = lineOf(node);
786
+ const clauses = [];
787
+ let handler = 0;
788
+ for (const child of node.namedChildren) {
789
+ if (child.type === 'block') clauses.push(['try', child]);
790
+ else if (child.type === 'except_clause' || child.type === 'except_group_clause') {
791
+ clauses.push([`except:${handler++}`, child]);
792
+ } else if (child.type === 'else_clause') clauses.push(['try-else', child]);
793
+ else if (child.type === 'finally_clause') clauses.push(['finally', child]);
794
+ }
795
+ noteClauses(node, clauses);
796
+ for (const [tag, clause] of clauses) {
797
+ const path = [...branch, [line, tag]];
798
+ if (clause.type === 'block') { visitBlock(clause, scope, path); continue; }
799
+ for (const target of clause.descendantsOfType('as_pattern_target')) {
800
+ if (target.parent?.parent && sameNode(target.parent.parent, clause)) bindTarget(scope, target, path);
801
+ }
802
+ for (const child of clause.namedChildren) if (child.type === 'block') visitBlock(child, scope, path);
803
+ }
804
+ return;
805
+ }
806
+ case 'match_statement': {
807
+ const line = lineOf(node);
808
+ const body = node.childForFieldName('body');
809
+ const cases = body ? body.namedChildren.filter(child => child.type === 'case_clause') : [];
810
+ const clauses = cases.map((clause, i) => [`case:${i}`, clause]);
811
+ noteClauses(node, clauses);
812
+ for (const [tag, clause] of clauses) {
813
+ for (const child of clause.namedChildren) if (child.type === 'block') visitBlock(child, scope, [...branch, [line, tag]]);
814
+ }
815
+ return;
816
+ }
817
+ case 'for_statement':
818
+ bindTarget(scope, node.childForFieldName('left'), branch);
819
+ // A loop body may run several times or not at all: its
820
+ // bindings are sequential, never exclusive alternatives.
821
+ for (const child of node.namedChildren) if (child.type === 'block' || child.type === 'else_clause') visitBlock(child.type === 'block' ? child : child.namedChildren.find(c => c.type === 'block'), scope, [...branch, [lineOf(node), 'loop']]);
822
+ return;
823
+ case 'while_statement':
824
+ for (const child of node.namedChildren) if (child.type === 'block' || child.type === 'else_clause') visitBlock(child.type === 'block' ? child : child.namedChildren.find(c => c.type === 'block'), scope, [...branch, [lineOf(node), 'loop']]);
825
+ return;
826
+ case 'with_statement':
827
+ for (const target of node.descendantsOfType('as_pattern_target')) {
828
+ if (target.parent?.parent?.parent && sameNode(target.parent.parent.parent, node.namedChildren[0])) bindTarget(scope, target, branch);
829
+ }
830
+ visitBlock(node.childForFieldName('body'), scope, branch);
831
+ return;
832
+ default:
833
+ return;
834
+ }
835
+ };
836
+ // Only the scopes holding a candidate are walked (their own statements;
837
+ // nested scopes are walked when they are candidates themselves).
838
+ for (const scope of candidateScopes) {
839
+ if (scope.type === 'module') { visitBlock(scope, 0, []); continue; }
840
+ const head = scope.parent?.type === 'decorated_definition' ? scope.parent : scope;
841
+ visitBlock(scope.childForFieldName('body'), lineOf(head), []);
842
+ }
843
+ // A `global` / `nonlocal` write anywhere rebinds the name at run time.
844
+ for (const node of reboundNodes || getCachedNodeList(root).nodes) {
845
+ if (node.type !== 'global_statement' && node.type !== 'nonlocal_statement') continue;
846
+ for (const name of node.namedChildren) if (name.type === 'identifier') rebound.add(name.text);
847
+ }
848
+ const exclusive = (a, b) => {
849
+ for (let k = 0; k < Math.min(a.length, b.length); k++) {
850
+ if (a[k][0] !== b[k][0]) return false;
851
+ if (a[k][1] !== b[k][1]) return branchClausesExclusive(a[k][1], b[k][1]);
852
+ }
853
+ return false;
854
+ };
855
+ const items = [];
856
+ for (const [scope, names] of scopes) {
857
+ for (const [name, sites] of names) {
858
+ if (sites.length < 2 || name === '*' || rebound.has(name)) continue;
859
+ if (!sites.some(site => site.kind !== 'value')) continue;
860
+ if (sites.some(site => site.branch.some(step => step.length > 2))) continue;
861
+ let all = true;
862
+ for (let i = 0; all && i < sites.length; i++) {
863
+ for (let j = i + 1; all && j < sites.length; j++) all = exclusive(sites[i].branch, sites[j].branch);
864
+ }
865
+ if (!all) continue;
866
+ const statements = new Set(sites.flatMap(site => site.branch.map(([line]) => line)));
867
+ items.push({
868
+ name, scope,
869
+ sites: sites.map(site => ({ kind: site.kind, line: site.line, branch: site.branch })),
870
+ clauses: [...statements].sort((a, b) => a - b).map(line => [line, clauseRanges.get(line) || []]),
871
+ });
872
+ }
873
+ }
874
+ items.sort((a, b) => a.scope - b.scope || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
875
+ return items;
876
+ }
877
+
339
878
  // --- End single-pass helpers ---
340
879
 
341
880
  /**
@@ -530,9 +1069,7 @@ function extractClassMembers(classNode, code) {
530
1069
  const bodyNode = classNode.childForFieldName('body');
531
1070
  if (!bodyNode) return members;
532
1071
 
533
- for (let i = 0; i < bodyNode.namedChildCount; i++) {
534
- const child = bodyNode.namedChild(i);
535
-
1072
+ for (const child of pythonClassBodyDefs(bodyNode)) {
536
1073
  let funcNode = child;
537
1074
  let decoratorStart = null;
538
1075
  const memberDecorators = [];
@@ -586,6 +1123,7 @@ function extractClassMembers(classNode, code) {
586
1123
  }
587
1124
 
588
1125
  const isAsync = funcNode.text.trimStart().startsWith('async ');
1126
+ const isGenerator = isPythonGenerator(funcNode);
589
1127
  const returnType = extractReturnType(funcNode);
590
1128
  const returnedConstructors = extractReturnedConstructors(funcNode);
591
1129
  const defLine = getDefLine(funcNode);
@@ -603,6 +1141,7 @@ function extractClassMembers(classNode, code) {
603
1141
  endLine,
604
1142
  memberType,
605
1143
  isAsync,
1144
+ ...(isGenerator && { isGenerator }),
606
1145
  isMethod: true, // Mark as method for context() lookups
607
1146
  // Match top-level Python functions: `async def` → ['async'] modifiers.
608
1147
  modifiers: isAsync ? ['async'] : [],
@@ -649,18 +1188,28 @@ function parse(code, parser) {
649
1188
  const processedFn = new Set();
650
1189
  const processedCls = new Set();
651
1190
 
1191
+ // Binding statements and `global`/`nonlocal` declarations seen by the
1192
+ // one traversal feed the configuration-item gate (fix #398F).
1193
+ const bindingNodes = [];
1194
+ const reboundNodes = [];
652
1195
  traverseTreeCached(tree.rootNode, (node) => {
653
1196
  _processFunction(node, functions, processedFn, lines, code);
654
1197
  _processClass(node, classes, processedCls, lines) ||
655
1198
  _processTypeAlias(node, classes, processedCls, lines);
656
1199
  _processState(node, stateObjects, lines);
657
1200
  _processModuleAssign(node, moduleAssigned);
1201
+ const type = node.type;
1202
+ if (type === 'function_definition' || type === 'class_definition' ||
1203
+ type === 'import_statement' || type === 'import_from_statement') bindingNodes.push(node);
1204
+ else if (type === 'global_statement' || type === 'nonlocal_statement') reboundNodes.push(node);
658
1205
  return true;
659
1206
  });
660
1207
 
661
1208
  functions.sort((a, b) => a.startLine - b.startLine);
662
1209
  classes.sort((a, b) => a.startLine - b.startLine);
663
1210
  stateObjects.sort((a, b) => a.startLine - b.startLine);
1211
+ const moduleValueAliases = moduleAssigned.size > 0 ? pythonModuleValueAliases(tree.rootNode) : [];
1212
+ const configurationItems = pythonConfigurationItems(tree.rootNode, bindingNodes, reboundNodes);
664
1213
 
665
1214
  return {
666
1215
  language: 'python',
@@ -668,7 +1217,10 @@ function parse(code, parser) {
668
1217
  functions,
669
1218
  classes,
670
1219
  stateObjects,
671
- ...(tree.rootNode.hasError && { parseRecovery: true }),
1220
+ ...(moduleValueAliases.length > 0 && { moduleValueAliases: [...moduleValueAliases] }),
1221
+ ...(moduleValueAliases.constructed && { moduleConstructedValues: moduleValueAliases.constructed }),
1222
+ ...(configurationItems.length > 0 && { configurationItems }),
1223
+ ...(tree.rootNode.hasError && { parseRecovery: true, parseErrorRegions: parseErrorRegions(tree.rootNode) }),
672
1224
  ...(moduleAssigned.size > 0 && { moduleAssignedNames: [...moduleAssigned].sort() }),
673
1225
  imports: [],
674
1226
  exports: []
@@ -843,6 +1395,40 @@ function iterableBindingTypes(typeNode) {
843
1395
  return typeName ? [typeName] : [];
844
1396
  }
845
1397
 
1398
+ // Subscript contracts of builtin/typing containers (fix #359, starlette-
1399
+ // measured): `params: dict[str, Convertor]` makes `params[key]` a Convertor;
1400
+ // `items: list[Conv]` makes `items[0]` a Conv. The element is the declared
1401
+ // value slot of the annotation, never guessed: unions, Any/object, bare
1402
+ // type variables and heterogeneous tuples abstain.
1403
+ const PY_MAPPING_ANNOTATIONS = new Set([
1404
+ 'dict', 'Dict', 'Mapping', 'MutableMapping', 'OrderedDict', 'defaultdict',
1405
+ 'DefaultDict',
1406
+ ]);
1407
+ const PY_SEQUENCE_ANNOTATIONS = new Set([
1408
+ 'list', 'List', 'Sequence', 'MutableSequence', 'deque', 'Deque',
1409
+ ]);
1410
+ function subscriptElementContract(typeNode) {
1411
+ const outerRaw = typeNode && typeNode.type === 'type' && typeNode.namedChildCount > 0
1412
+ ? typeNode.namedChild(0) : typeNode;
1413
+ const outer = genericTypeParts(outerRaw);
1414
+ if (!outer) return null;
1415
+ let element = null;
1416
+ if (PY_MAPPING_ANNOTATIONS.has(outer.base) && outer.args.length === 2) {
1417
+ element = outer.args[1];
1418
+ } else if (PY_SEQUENCE_ANNOTATIONS.has(outer.base) && outer.args.length === 1) {
1419
+ element = outer.args[0];
1420
+ } else if (['tuple', 'Tuple'].includes(outer.base) && outer.args.length === 2 &&
1421
+ outer.args[1]?.type === 'ellipsis') {
1422
+ element = outer.args[0];
1423
+ }
1424
+ if (!element) return null;
1425
+ const type = typeNameFromExpr(unwrapTypeNode(element));
1426
+ if (!type || type === 'Any' || type === 'object' || type === 'None' ||
1427
+ /^[A-Z][A-Z0-9]?$/.test(type)) return null;
1428
+ const qualifier = typeQualifierFromExpr(unwrapTypeNode(element));
1429
+ return { type, ...(qualifier && { qualifier }), node: element };
1430
+ }
1431
+
846
1432
  function patternIdentifiers(node, out = []) {
847
1433
  if (!node) return out;
848
1434
  if (node.type === 'identifier') {
@@ -864,6 +1450,12 @@ function enclosingPythonClassName(node) {
864
1450
  return null;
865
1451
  }
866
1452
 
1453
+ const FIELD_CONTRACT_VALUE_NODES = new Set([
1454
+ 'call', 'attribute', 'subscript', 'binary_operator', 'boolean_operator',
1455
+ 'comparison_operator', 'list', 'dictionary', 'set', 'tuple', 'string',
1456
+ 'concatenated_string', 'parameters', 'decorator',
1457
+ ]);
1458
+
867
1459
  function explicitInstanceFieldContracts(tree, parser) {
868
1460
  const result = new Map();
869
1461
  const parseCommentType = comment => {
@@ -877,16 +1469,20 @@ function explicitInstanceFieldContracts(tree, parser) {
877
1469
  const assignment = statement?.namedChild(0);
878
1470
  return assignment?.childForFieldName('type') || null;
879
1471
  };
880
- traverseTreeCached(tree.rootNode, node => {
1472
+ traverseTree(tree.rootNode, node => {
1473
+ // Only statements can declare an annotated instance field. Value
1474
+ // expressions and parameter/default/decorator trees contain no
1475
+ // such statements, so do not materialize all their descendants.
1476
+ if (FIELD_CONTRACT_VALUE_NODES.has(node.type)) return false;
881
1477
  if (node.type !== 'expression_statement') return true;
882
1478
  const assignment = node.namedChild(0);
883
- if (assignment?.type !== 'assignment') return true;
1479
+ if (assignment?.type !== 'assignment') return false;
884
1480
  const left = assignment.childForFieldName('left');
885
1481
  if (left?.type !== 'attribute' ||
886
- left.childForFieldName('object')?.text !== 'self') return true;
1482
+ left.childForFieldName('object')?.text !== 'self') return false;
887
1483
  const field = left.childForFieldName('attribute')?.text;
888
1484
  const className = enclosingPythonClassName(node);
889
- if (!field || !className) return true;
1485
+ if (!field || !className) return false;
890
1486
  let typeNode = assignment.childForFieldName('type');
891
1487
  if (!typeNode) {
892
1488
  const parent = node.parent;
@@ -899,16 +1495,16 @@ function explicitInstanceFieldContracts(tree, parser) {
899
1495
  }
900
1496
  }
901
1497
  }
902
- if (!typeNode) return true;
1498
+ if (!typeNode) return false;
903
1499
  const type = typeNameFromAnnotation(typeNode);
904
1500
  const itemTypes = iterableBindingTypes(typeNode);
905
- if (!type && itemTypes.length === 0) return true;
1501
+ if (!type && itemTypes.length === 0) return false;
906
1502
  if (!result.has(className)) result.set(className, new Map());
907
1503
  result.get(className).set(field, {
908
1504
  ...(type && { type }),
909
1505
  ...(itemTypes.length > 0 && { itemTypes }),
910
1506
  });
911
- return true;
1507
+ return false;
912
1508
  });
913
1509
  return result;
914
1510
  }
@@ -1050,6 +1646,13 @@ function narrowedReceiverType(refNode, receiverName, declaredUnion) {
1050
1646
  return undefined;
1051
1647
  }
1052
1648
 
1649
+ /** `Union[Foo, None]` names Foo (fix #402); a union of several types names none. */
1650
+ function unionMemberName(node) {
1651
+ const members = (genericTypeParts(node)?.args || []).filter(arg => arg &&
1652
+ arg.type !== 'none' && !(arg.type === 'identifier' && arg.text === 'None'));
1653
+ return members.length === 1 ? typeNameFromExpr(members[0]) : undefined;
1654
+ }
1655
+
1053
1656
  function typeNameFromExpr(node) {
1054
1657
  if (!node) return undefined;
1055
1658
  switch (node.type) {
@@ -1074,6 +1677,7 @@ function typeNameFromExpr(node) {
1074
1677
  case 'subscript': {
1075
1678
  // typing.Optional[Foo] parses as subscript when base is dotted
1076
1679
  const base = typeNameFromExpr(node.childForFieldName('value'));
1680
+ if (base === 'Union') return unionMemberName(node);
1077
1681
  if (PY_TYPE_WRAPPERS.has(base)) {
1078
1682
  return typeNameFromExpr(node.childForFieldName('subscript'));
1079
1683
  }
@@ -1082,6 +1686,7 @@ function typeNameFromExpr(node) {
1082
1686
  case 'generic_type': {
1083
1687
  // Optional[Foo] / Mapping[str, int] in annotation position
1084
1688
  const base = typeNameFromExpr(node.namedChild(0));
1689
+ if (base === 'Union') return unionMemberName(node);
1085
1690
  if (PY_TYPE_WRAPPERS.has(base)) {
1086
1691
  const params = node.namedChild(1); // type_parameter → type wrappers
1087
1692
  const firstType = params && params.namedChildCount > 0 ? params.namedChild(0) : null;
@@ -1372,6 +1977,108 @@ function pythonScopeBoundNames(scopeNode) {
1372
1977
  return names;
1373
1978
  }
1374
1979
 
1980
+ /** Type of the innermost scope node (function, lambda, class, module). */
1981
+ function pythonEnclosingScopeType(node) {
1982
+ for (let parent = node?.parent; parent; parent = parent.parent) {
1983
+ if (['function_definition', 'async_function_definition', 'lambda',
1984
+ 'class_definition', 'module'].includes(parent.type)) return parent.type;
1985
+ }
1986
+ return 'module';
1987
+ }
1988
+
1989
+ /** Innermost enclosing function of a node; null at module/class/lambda level. */
1990
+ function pythonEnclosingFunctionNode(node) {
1991
+ for (let parent = node?.parent; parent; parent = parent.parent) {
1992
+ if (parent.type === 'function_definition' ||
1993
+ parent.type === 'async_function_definition') return parent;
1994
+ if (parent.type === 'lambda' || parent.type === 'class_definition') return null;
1995
+ }
1996
+ return null;
1997
+ }
1998
+
1999
+ const scopeBindingCountsByTree = new WeakMap();
2000
+
2001
+ /**
2002
+ * How many times a function binds each local name (fix #381): parameters,
2003
+ * assignment/augmented/walrus targets, loop and comprehension-free for
2004
+ * targets, with/except `as` targets, imports, nested def/class names; a
2005
+ * `global`/`nonlocal`/`del` of the name counts as two (never a single local
2006
+ * binding). Nested functions, lambdas and classes are their own scopes.
2007
+ */
2008
+ function pythonBindingCount(fnNode, name) {
2009
+ let byId = scopeBindingCountsByTree.get(fnNode.tree);
2010
+ if (!byId) { byId = new Map(); scopeBindingCountsByTree.set(fnNode.tree, byId); }
2011
+ let counts = byId.get(fnNode.id);
2012
+ if (!counts) {
2013
+ counts = new Map();
2014
+ const add = (id, n = 1) => { if (id) counts.set(id, (counts.get(id) || 0) + n); };
2015
+ // Only names in binding position: `self.x = ...` / `a[i] = ...`
2016
+ // bind no local.
2017
+ const addTarget = (target) => {
2018
+ if (!target) return;
2019
+ if (target.type === 'identifier') { add(target.text); return; }
2020
+ if (['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list',
2021
+ 'parenthesized_expression', 'list_splat_pattern', 'list_splat'].includes(target.type)) {
2022
+ for (const item of target.namedChildren) addTarget(item);
2023
+ }
2024
+ };
2025
+ const params = fnNode.childForFieldName('parameters');
2026
+ if (params) {
2027
+ for (const param of params.namedChildren) {
2028
+ const id = param.type === 'identifier' ? param
2029
+ : (param.childForFieldName('name') || param.namedChild(0));
2030
+ if (id?.type === 'identifier') add(id.text);
2031
+ }
2032
+ }
2033
+ const walk = (node) => {
2034
+ for (const child of node.namedChildren) {
2035
+ if (child.type === 'function_definition' ||
2036
+ child.type === 'async_function_definition' ||
2037
+ child.type === 'class_definition') {
2038
+ add(child.childForFieldName('name')?.text);
2039
+ continue;
2040
+ }
2041
+ if (child.type === 'lambda') continue;
2042
+ if (child.type === 'assignment' || child.type === 'augmented_assignment') {
2043
+ addTarget(child.childForFieldName('left'));
2044
+ } else if (child.type === 'named_expression') {
2045
+ addTarget(child.childForFieldName('name'));
2046
+ } else if (child.type === 'for_statement' || child.type === 'for_in_clause') {
2047
+ addTarget(child.childForFieldName('left'));
2048
+ } else if (child.type === 'as_pattern') {
2049
+ addTarget(child.childForFieldName('alias') || child.namedChild(child.namedChildCount - 1));
2050
+ } else if (child.type === 'except_clause') {
2051
+ for (const part of child.namedChildren) {
2052
+ if (part.type === 'as_pattern') continue;
2053
+ if (part.type === 'identifier' && part.previousSibling?.type === 'as') add(part.text);
2054
+ }
2055
+ } else if (child.type === 'import_statement' || child.type === 'import_from_statement') {
2056
+ for (const part of child.namedChildren) {
2057
+ if (part.type === 'aliased_import') {
2058
+ add(part.childForFieldName('alias')?.text);
2059
+ } else if (part.type === 'dotted_name' &&
2060
+ !(child.type === 'import_from_statement' && part === child.childForFieldName('module_name'))) {
2061
+ add(part.text.split('.')[0]);
2062
+ }
2063
+ }
2064
+ } else if (child.type === 'global_statement' || child.type === 'nonlocal_statement' ||
2065
+ child.type === 'delete_statement') {
2066
+ for (const id of patternIdentifiers(child)) add(id, 2);
2067
+ }
2068
+ walk(child);
2069
+ }
2070
+ };
2071
+ const body = fnNode.childForFieldName('body');
2072
+ if (body) walk(body);
2073
+ byId.set(fnNode.id, counts);
2074
+ }
2075
+ return counts.get(name) || 0;
2076
+ }
2077
+
2078
+ function pythonSingleLocalBinding(fnNode, name) {
2079
+ return pythonBindingCount(fnNode, name) === 1;
2080
+ }
2081
+
1375
2082
  function pythonScopeBindsName(scopeNode, name) {
1376
2083
  return pythonScopeBoundNames(scopeNode).has(name);
1377
2084
  }
@@ -1481,12 +2188,100 @@ function isPythonNameShadowedAt(refNode, name) {
1481
2188
  }
1482
2189
  }
1483
2190
  const body = parent.childForFieldName('body');
1484
- return body ? pythonScopeBindsName(body, name) : false;
2191
+ if (body && pythonScopeBindsName(body, name)) {
2192
+ // `global name` makes the function's binding the module's.
2193
+ return !pythonDeclaresScopeName(body, name);
2194
+ }
2195
+ // Unbound here: a nested function reads the enclosing function
2196
+ // scopes (closures) before the module (fix #402); class bodies
2197
+ // are skipped, as Python skips them.
2198
+ if (body && pythonDeclaresScopeName(body, name)) return false;
1485
2199
  }
1486
2200
  }
1487
2201
  return false;
1488
2202
  }
1489
2203
 
2204
+ /**
2205
+ * The start line (decorators included) of the enclosing function a closure
2206
+ * reference binds `name` in (fix #402): null when the reference's own
2207
+ * function (or a comprehension or loop around it) binds it, or no enclosing
2208
+ * function does. A lambda is a function of its own (fix #403): the call
2209
+ * records name it as the reference's enclosing function, so a name its
2210
+ * enclosing def binds is a closure name.
2211
+ */
2212
+ function pythonClosureScopeLine(refNode, name) {
2213
+ let first = true;
2214
+ for (let parent = refNode.parent; parent; parent = parent.parent) {
2215
+ if (parent.type === 'lambda') {
2216
+ const params = parent.childForFieldName('parameters');
2217
+ if (params?.namedChildren.some(param => (param.type === 'identifier' ? param
2218
+ : param.childForFieldName('name'))?.text === name)) return null;
2219
+ first = false;
2220
+ continue;
2221
+ }
2222
+ if (PY_COMPREHENSIONS.has(parent.type)) {
2223
+ if (parent.namedChildren.some(clause => clause.type === 'for_in_clause' &&
2224
+ pythonTargetBindsName(clause.childForFieldName('left'), name))) return null;
2225
+ continue;
2226
+ }
2227
+ if (parent.type !== 'function_definition' && parent.type !== 'async_function_definition') continue;
2228
+ const params = parent.childForFieldName('parameters');
2229
+ const body = parent.childForFieldName('body');
2230
+ const bindsParam = !!params && params.namedChildren.some(param => {
2231
+ const id = param.type === 'identifier' ? param : (param.childForFieldName('name') || param.namedChild(0));
2232
+ return id?.type === 'identifier' && id.text === name;
2233
+ });
2234
+ if (body && pythonDeclaresScopeName(body, name)) return null;
2235
+ if (bindsParam || (body && pythonScopeBindsName(body, name))) {
2236
+ if (first) return null;
2237
+ const start = parent.parent?.type === 'decorated_definition' ? parent.parent : parent;
2238
+ return start.startPosition.row + 1;
2239
+ }
2240
+ first = false;
2241
+ }
2242
+ return null;
2243
+ }
2244
+
2245
+ /** Shadow fields of a bare reference: `localShadow`, and the enclosing
2246
+ * function a closure binds it in. */
2247
+ function pythonShadowFields(refNode, name) {
2248
+ if (!isPythonNameShadowedAt(refNode, name)) return null;
2249
+ const scope = pythonClosureScopeLine(refNode, name);
2250
+ return scope ? { localShadow: true, localShadowScope: scope } : { localShadow: true };
2251
+ }
2252
+
2253
+ /** Names a function body declares `global` (not in nested scopes),
2254
+ * memoized per tree and body node (fix #402). */
2255
+ const globalNamesByTree = new WeakMap();
2256
+ function pythonGlobalNames(body) {
2257
+ let byId = globalNamesByTree.get(body.tree);
2258
+ if (!byId) { byId = new Map(); globalNamesByTree.set(body.tree, byId); }
2259
+ let names = byId.get(body.id);
2260
+ if (names) return names;
2261
+ names = new Set();
2262
+ // Prefilter: no `global` keyword in the body, no declaration.
2263
+ if (body.text.includes('global')) {
2264
+ const stack = [body];
2265
+ while (stack.length > 0) {
2266
+ const node = stack.pop();
2267
+ if (node.type === 'global_statement') {
2268
+ for (const child of node.namedChildren) if (child.type === 'identifier') names.add(child.text);
2269
+ continue;
2270
+ }
2271
+ if (node !== body && (node.type === 'function_definition' || node.type === 'class_definition' ||
2272
+ node.type === 'lambda')) continue;
2273
+ for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
2274
+ }
2275
+ }
2276
+ byId.set(body.id, names);
2277
+ return names;
2278
+ }
2279
+
2280
+ /** Whether a function body declares `global name` (not in nested scopes). */
2281
+ function pythonDeclaresScopeName(body, name) {
2282
+ return pythonGlobalNames(body).has(name);
2283
+ }
2284
+
1490
2285
  /**
1491
2286
  * Whether an uppercase call target is a local VALUE binding rather than a
1492
2287
  * class declaration.
@@ -1637,8 +2432,145 @@ function pythonModuleAliases(tree) {
1637
2432
  return aliases;
1638
2433
  }
1639
2434
 
1640
- function findCallsInCode(code, parser) {
2435
+ // Annotation positions whose value Python evaluates as an expression.
2436
+ const PY_ANNOTATION_OWNERS = new Set(['typed_parameter', 'typed_default_parameter', 'function_definition', 'assignment']);
2437
+
2438
+ /**
2439
+ * String annotations holding calls (fix #398F): `def f(v: "Annotated[int,
2440
+ * typer.Argument()]")`. A forward reference is an expression the runtime
2441
+ * evaluates (`typing.get_type_hints`), so the calls it spells are calls.
2442
+ * Plain one-line string literals only (no prefix, escape or
2443
+ * interpolation); the result lists the byte ranges of their quotes.
2444
+ */
2445
+ function annotationStringQuotes(tree) {
2446
+ const quotes = [];
2447
+ for (const holder of getCachedNodeList(tree.rootNode).nodes) {
2448
+ if (holder.type !== 'type') continue;
2449
+ const string = holder.namedChildCount === 1 ? holder.namedChild(0) : null;
2450
+ if (string?.type !== 'string' || string.startPosition.row !== string.endPosition.row) continue;
2451
+ const owner = holder.parent;
2452
+ if (!owner || !PY_ANNOTATION_OWNERS.has(owner.type)) continue;
2453
+ const field = owner.type === 'function_definition' ? 'return_type' : 'type';
2454
+ if (!sameNode(owner.childForFieldName(field), holder)) continue;
2455
+ const parts = string.children;
2456
+ if (parts.length !== 3 || parts[0].type !== 'string_start' || parts[1].type !== 'string_content' ||
2457
+ parts[2].type !== 'string_end') continue;
2458
+ const start = parts[0].text;
2459
+ if (start !== '"' && start !== "'") continue;
2460
+ const content = parts[1];
2461
+ if (content.namedChildCount > 0 || !content.text.includes('(')) continue;
2462
+ quotes.push({ open: parts[0].startIndex, close: parts[2].startIndex,
2463
+ contentStart: content.startIndex, contentEnd: content.endIndex });
2464
+ }
2465
+ return quotes;
2466
+ }
2467
+
2468
+ /**
2469
+ * References to the file's function-local classes (fix #398F): the row of
2470
+ * the one class declaration the name binds at the call (the receiver's
2471
+ * constructor, a class qualifier, a bare constructor call), or 0 when a
2472
+ * parameter, assignment or second declaration binds it too. Query-time
2473
+ * identity proofs read this instead of parsing the file again.
2474
+ */
2475
+ function annotateLocalClassReferences(tree, calls, classNodes) {
2476
+ const names = new Set();
2477
+ for (const definition of classNodes) {
2478
+ if (definition.parent?.type === 'module') continue;
2479
+ for (let parent = definition.parent; parent; parent = parent.parent) {
2480
+ if (parent.type === 'function_definition' || parent.type === 'lambda') {
2481
+ const name = definition.childForFieldName('name')?.text;
2482
+ if (name) names.add(name);
2483
+ break;
2484
+ }
2485
+ }
2486
+ }
2487
+ if (names.size === 0) return;
2488
+ const memo = new Map();
2489
+ const rowAt = (start, name) => {
2490
+ if (!Number.isInteger(start)) return undefined;
2491
+ let node = tree.rootNode.descendantForIndex(start);
2492
+ if (node?.text !== name) {
2493
+ for (let owner = node?.parent; owner && owner.startIndex === start; owner = owner.parent) {
2494
+ const value = owner.childForFieldName('right') || owner.childForFieldName('value');
2495
+ if (value?.type === 'call') {
2496
+ node = value.childForFieldName('function');
2497
+ break;
2498
+ }
2499
+ }
2500
+ }
2501
+ if (node?.text !== name && node?.parent?.type === 'attribute') {
2502
+ node = node.parent.childForFieldName('object');
2503
+ }
2504
+ if (!node || node.text !== name) return 0;
2505
+ const scope = referenceScope(node, 'python', memo, name, { includeClasses: true });
2506
+ return scope?.defRows?.length === 1 ? scope.defRows[0] + 1 : 0;
2507
+ };
2508
+ for (const call of calls) {
2509
+ if (!call.isMethod && !call.receiver && names.has(call.name)) {
2510
+ const row = rowAt(call.callSite?.start, call.name);
2511
+ if (row !== undefined) call.localClassRow = row;
2512
+ }
2513
+ if (call.isMethod && call.receiver && !call.receiverType && names.has(call.receiver)) {
2514
+ const row = rowAt(call.callSite?.start ?? call.callStart, call.receiver);
2515
+ if (row !== undefined) call.receiverLocalClassRow = row;
2516
+ }
2517
+ // Kept beside the evidence (never inside it: the evidence object is
2518
+ // a provenance fact), tied to the evidence position it was read at.
2519
+ const evidence = call.receiverTypeEvidence;
2520
+ if (call.receiverTypeSource === 'constructor' && evidence && names.has(call.receiverType)) {
2521
+ const row = rowAt(evidence.start, call.receiverType);
2522
+ if (row !== undefined) call.receiverTypeLocalClassRef = { start: evidence.start, row };
2523
+ }
2524
+ }
2525
+ }
2526
+
2527
+ function findCallsInCode(code, parser, options = {}) {
1641
2528
  const tree = parseTree(parser, code);
2529
+ const sink = { classNodes: [] };
2530
+ const calls = findCallsInTree(code, tree, parser, sink);
2531
+ annotateLocalClassReferences(tree, calls, sink.classNodes);
2532
+ if (options.stringAnnotations === false || !/(?::|->)\s*["'][^"'\n]*\(/.test(code)) return calls;
2533
+ const quotes = annotationStringQuotes(tree);
2534
+ if (quotes.length === 0) return calls;
2535
+ // The same source with those quotes blanked parses the annotation as
2536
+ // the expression it is, at the same offsets; only calls inside the
2537
+ // blanked strings are taken from it, and only where it parses clean.
2538
+ const chars = code.split('');
2539
+ for (const quote of quotes) {
2540
+ chars[quote.open] = ' ';
2541
+ chars[quote.close] = ' ';
2542
+ }
2543
+ const masked = chars.join('');
2544
+ const maskedTree = parseTree(parser, masked);
2545
+ const clean = quotes.filter(quote => {
2546
+ const node = maskedTree.rootNode.descendantForIndex(quote.contentStart, quote.contentEnd);
2547
+ return node && !node.hasError && !node.isMissing;
2548
+ });
2549
+ if (clean.length === 0) return calls;
2550
+ const inside = call => {
2551
+ const start = call.callSite?.start;
2552
+ return Number.isInteger(start) &&
2553
+ clean.some(quote => start >= quote.contentStart && start < quote.contentEnd);
2554
+ };
2555
+ const extra = findCallsInTree(masked, maskedTree, parser)
2556
+ .filter(inside).map(call => ({ ...call, stringAnnotation: true }));
2557
+ if (extra.length === 0) return calls;
2558
+ const position = call => [call.line, call.callSite?.column ?? call.column ?? 0];
2559
+ const merged = [...calls];
2560
+ for (const call of extra) {
2561
+ const [line, column] = position(call);
2562
+ let at = merged.length;
2563
+ while (at > 0) {
2564
+ const [l, c] = position(merged[at - 1]);
2565
+ if (l < line || (l === line && c <= column)) break;
2566
+ at--;
2567
+ }
2568
+ merged.splice(at, 0, call);
2569
+ }
2570
+ return merged;
2571
+ }
2572
+
2573
+ function findCallsInTree(code, tree, parser, sink = null) {
1642
2574
  const calls = [];
1643
2575
  const instanceFieldContracts = explicitInstanceFieldContracts(tree, parser);
1644
2576
  // Same-file callable return contracts are a closed type source for loop
@@ -1646,6 +2578,7 @@ function findCallsInCode(code, parser) {
1646
2578
  // `parse_items() -> list[Item]` without guessing about external code.
1647
2579
  const callableIterableTypes = new Map();
1648
2580
  traverseTreeCached(tree.rootNode, node => {
2581
+ if (sink && node.type === 'class_definition') sink.classNodes.push(node);
1649
2582
  if (node.type !== 'function_definition') return true;
1650
2583
  const name = node.childForFieldName('name')?.text;
1651
2584
  const returnType = node.childForFieldName('return_type');
@@ -1676,6 +2609,8 @@ function findCallsInCode(code, parser) {
1676
2609
  const localIterationSources = new Map(); // loop variable -> declared iterable path + tuple index
1677
2610
  const localDictValueTypes = new Map(); // local dict -> exact string-key value types
1678
2611
  const localSubscriptSources = new Map(); // local value -> exact typed container selected by []
2612
+ const localSubscriptElements = new Map(); // container binding -> declared element contract (fix #359)
2613
+ const localSubscriptElementsStack = [];
1679
2614
  const localVarStdlibContracts = new Map(); // variable -> stdlib module proving its type flow
1680
2615
  const assignmentRhsReceiverTypes = new Map(); // call-node id -> pre-assignment type and origin
1681
2616
  const constructedReceiverVars = new Set(); // exact constructor-result bindings
@@ -1688,6 +2623,10 @@ function findCallsInCode(code, parser) {
1688
2623
  // receiver is null for chained/deep objects (self._text.append) — the
1689
2624
  // rewritten call is then receiver-blind and routes through dispatch tiering.
1690
2625
  const memberAliases = new Map();
2626
+ // One-hop local aliases of attribute paths (fix #381):
2627
+ // `config = self.config; config.load()` receives through `self.config`.
2628
+ // name -> { node: the aliased attribute node, scope: function node id }.
2629
+ const localFieldAliases = new Map();
1691
2630
  const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
1692
2631
  // Exact class-value aliases: `_Segment = Segment` makes later
1693
2632
  // `_Segment.line()` dispatch through the imported/local class object.
@@ -1719,25 +2658,61 @@ function findCallsInCode(code, parser) {
1719
2658
  for (let i = 0; i < argsNode.namedChildCount; i++) {
1720
2659
  const arg = argsNode.namedChild(i);
1721
2660
  if (arg.type.endsWith('comment')) continue;
1722
- // Handle f-string explicitly
1723
- if (arg.type === 'string') {
1724
- // f-string detection: tree-sitter-python wraps interpolations as 'interpolation' children.
1725
- // If any interpolation child exists, this is interpolated; extract literal prefix.
1726
- let interp = false;
1727
- let prefix = '';
1728
- for (let j = 0; j < arg.namedChildCount; j++) {
1729
- const sc = arg.namedChild(j);
1730
- if (sc.type === 'interpolation') { interp = true; break; }
1731
- if (sc.type === 'string_content') prefix += sc.text;
2661
+ return stringValueOf(arg);
2662
+ }
2663
+ return null;
2664
+ };
2665
+ const stringValueOf = (arg) => {
2666
+ // Handle f-string explicitly
2667
+ if (arg.type === 'string') {
2668
+ // f-string detection: tree-sitter-python wraps interpolations as 'interpolation' children.
2669
+ // If any interpolation child exists, this is interpolated; extract literal prefix.
2670
+ let interp = false;
2671
+ let prefix = '';
2672
+ for (let j = 0; j < arg.namedChildCount; j++) {
2673
+ const sc = arg.namedChild(j);
2674
+ if (sc.type === 'interpolation') { interp = true; break; }
2675
+ if (sc.type === 'string_content') prefix += sc.text;
2676
+ }
2677
+ if (interp) {
2678
+ return { value: prefix + (prefix.endsWith('*') ? '' : '*'), interp: true };
2679
+ }
2680
+ }
2681
+ return _extractStringArg(arg);
2682
+ };
2683
+
2684
+ // fix #366: keyword request configuration — `session.request(
2685
+ // method="POST", url="/api/items")`, `send(url="/x")`. Returns
2686
+ // { url, interp, method? } when a `url=` keyword carries a string.
2687
+ // Whether the callee performs HTTP is decided at query time.
2688
+ const getRequestConfig = (callNode) => {
2689
+ const argsNode = callNode.childForFieldName('arguments');
2690
+ if (!argsNode) return null;
2691
+ let url = null;
2692
+ let method = null;
2693
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
2694
+ const arg = argsNode.namedChild(i);
2695
+ if (arg.type !== 'keyword_argument') continue;
2696
+ const nameNode = arg.childForFieldName('name');
2697
+ const valueNode = arg.childForFieldName('value');
2698
+ if (!nameNode || !valueNode || valueNode.type !== 'string') continue;
2699
+ // `path=` is overwhelmingly a filesystem path in Python APIs;
2700
+ // only `url=` names a request target.
2701
+ if (nameNode.text === 'url' && !url) {
2702
+ const v = stringValueOf(valueNode);
2703
+ if (v && typeof v.value === 'string' && v.value.length > 0) {
2704
+ url = { value: v.value, interp: !!v.interp, key: nameNode.text };
1732
2705
  }
1733
- if (interp) {
1734
- return { value: prefix + (prefix.endsWith('*') ? '' : '*'), interp: true };
2706
+ } else if (nameNode.text === 'method') {
2707
+ const v = _extractStringArg(valueNode);
2708
+ if (v && !v.interp && typeof v.value === 'string' && v.value.length > 0) {
2709
+ method = v.value.toUpperCase();
1735
2710
  }
1736
- return _extractStringArg(arg);
1737
2711
  }
1738
- return _extractStringArg(arg);
1739
2712
  }
1740
- return null;
2713
+ if (!url) return null;
2714
+ return { url: url.value, key: url.key, ...(url.interp && { interp: true }),
2715
+ ...(method && { method }) };
1741
2716
  };
1742
2717
 
1743
2718
  // Helper to check if a node is a non-callable literal
@@ -2075,6 +3050,7 @@ function findCallsInCode(code, parser) {
2075
3050
  [...localDictValueTypes].map(([name, values]) =>
2076
3051
  [name, new Map(values)])));
2077
3052
  localSubscriptSourcesStack.push(new Map(localSubscriptSources));
3053
+ localSubscriptElementsStack.push(new Map(localSubscriptElements));
2078
3054
  localVarStdlibContractsStack.push(new Map(localVarStdlibContracts));
2079
3055
  constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
2080
3056
  withBindingVarsStack.push(new Set(withBindingVars));
@@ -2109,6 +3085,12 @@ function findCallsInCode(code, parser) {
2109
3085
  localSubscriptSources.delete(name);
2110
3086
  }
2111
3087
  }
3088
+ for (const name of localSubscriptElements.keys()) {
3089
+ if ((body && pythonScopeBindsName(body, name)) ||
3090
+ functionParameterBindsName(node, name)) {
3091
+ localSubscriptElements.delete(name);
3092
+ }
3093
+ }
2112
3094
  for (const name of classValueAliases.keys()) {
2113
3095
  if ((body && pythonScopeBindsName(body, name)) ||
2114
3096
  functionParameterBindsName(node, name)) {
@@ -2151,6 +3133,10 @@ function findCallsInCode(code, parser) {
2151
3133
  else localVarUnionTypes.delete(nameNode.text);
2152
3134
  if (itemTypes.length > 0) localIterableTypes.set(nameNode.text, itemTypes);
2153
3135
  else localIterableTypes.delete(nameNode.text);
3136
+ const elementContract = parameterPattern?.type === 'identifier'
3137
+ ? subscriptElementContract(typeNode) : null;
3138
+ if (elementContract) localSubscriptElements.set(nameNode.text, elementContract);
3139
+ else localSubscriptElements.delete(nameNode.text);
2154
3140
  }
2155
3141
  }
2156
3142
 
@@ -2257,6 +3243,9 @@ function findCallsInCode(code, parser) {
2257
3243
  else localVarUnionTypes.delete(left.text);
2258
3244
  if (itemTypes.length > 0) localIterableTypes.set(left.text, itemTypes);
2259
3245
  else localIterableTypes.delete(left.text);
3246
+ const elementContract = subscriptElementContract(typeNode);
3247
+ if (elementContract) localSubscriptElements.set(left.text, elementContract);
3248
+ else localSubscriptElements.delete(left.text);
2260
3249
  }
2261
3250
  memberAliases.delete(left.text); // any assignment rebinds the name
2262
3251
  constructedReceiverVars.delete(left.text);
@@ -2270,6 +3259,7 @@ function findCallsInCode(code, parser) {
2270
3259
  localVarTypeQualifiers.delete(left.text);
2271
3260
  localVarUnionTypes.delete(left.text);
2272
3261
  localIterableTypes.delete(left.text);
3262
+ localSubscriptElements.delete(left.text);
2273
3263
  localIterationSources.delete(left.text);
2274
3264
  localDictValueTypes.delete(left.text);
2275
3265
  localVarStdlibContracts.delete(left.text);
@@ -2357,6 +3347,21 @@ function findCallsInCode(code, parser) {
2357
3347
  }
2358
3348
  if (!typeNode && right?.type === 'subscript') {
2359
3349
  const base = right.childForFieldName('value');
3350
+ // Declared container element (fix #359): `c = params[k]`
3351
+ // with `params: dict[str, Convertor]` types c. A slice
3352
+ // (`items[1:]`) is a container, not an element.
3353
+ const element = base?.type === 'identifier' && base.text !== left.text
3354
+ ? localSubscriptElements.get(base.text) : null;
3355
+ const subscriptKey = right.childForFieldName('subscript');
3356
+ if (element && subscriptKey?.type !== 'slice' &&
3357
+ !declaredVarTypes.get(left.text)) {
3358
+ localVarTypes.set(left.text, element.type, 'annotation', element.node);
3359
+ if (element.qualifier) {
3360
+ localVarTypeQualifiers.set(left.text, element.qualifier);
3361
+ } else {
3362
+ localVarTypeQualifiers.delete(left.text);
3363
+ }
3364
+ }
2360
3365
  if (base?.type === 'identifier' && base.text !== left.text) {
2361
3366
  const rootType = localVarTypes.get(base.text);
2362
3367
  if (rootType) {
@@ -2376,7 +3381,14 @@ function findCallsInCode(code, parser) {
2376
3381
  localVarTypes.set(left.text, roundTripType, 'flow', node);
2377
3382
  localVarStdlibContracts.set(left.text, 'pickle');
2378
3383
  }
2379
- if (right?.type === 'identifier') {
3384
+ // A class-body assignment binds a class attribute, never a
3385
+ // name of the enclosing function (fix #381): `Connection =
3386
+ // Mock` inside a local class leaves the function's
3387
+ // `Connection(...)` calls alone.
3388
+ const classBodyAssignment = pythonEnclosingScopeType(node) === 'class_definition';
3389
+ if (right?.type === 'identifier' && classBodyAssignment) {
3390
+ // attribute binding only
3391
+ } else if (right?.type === 'identifier') {
2380
3392
  aliases.set(left.text, right.text);
2381
3393
  const classValue = !typeNode && isDirectScopeAssignment(node)
2382
3394
  ? exactConstructorInfo(right) : null;
@@ -2393,6 +3405,49 @@ function findCallsInCode(code, parser) {
2393
3405
  receiver: objNode?.type === 'identifier' ? objNode.text : null,
2394
3406
  attr: attrName.text,
2395
3407
  });
3408
+ // A bound method read through a typed receiver
3409
+ // (`loads = registry.loads`, `run = Client().run`,
3410
+ // fix #400) references the method: a rename edits
3411
+ // the attribute, never the variable holding it.
3412
+ let valueType = null;
3413
+ let valueFields = null;
3414
+ if (objNode?.type === 'identifier' && !['self', 'cls'].includes(objNode.text) &&
3415
+ !moduleAliases.has(objNode.text)) {
3416
+ valueType = localVarTypes.get(objNode.text) || null;
3417
+ if (valueType) valueFields = localVarTypes.fields(objNode.text, valueType);
3418
+ } else if (objNode?.type === 'call') {
3419
+ const ctor = exactConstructorInfo(objNode.childForFieldName('function'));
3420
+ if (ctor && !ctor.qualifier) {
3421
+ valueType = ctor.type;
3422
+ valueFields = { receiverTypeSource: 'constructor',
3423
+ receiverTypeEvidence: { ...typeOrigin('constructor', objNode), type: ctor.type } };
3424
+ }
3425
+ }
3426
+ if (valueType) {
3427
+ // The receiver context a call on it carries.
3428
+ const recv = objNode.type === 'identifier' ? objNode.text : null;
3429
+ calls.push({
3430
+ callSite: typeOrigin('call', attrName),
3431
+ name: attrName.text,
3432
+ line: attrName.startPosition.row + 1,
3433
+ isMethod: true,
3434
+ ...(recv && { receiver: recv }),
3435
+ receiverType: valueType,
3436
+ ...valueFields,
3437
+ ...(recv && localVarTypeQualifiers.get(recv) && {
3438
+ receiverTypeQualifier: localVarTypeQualifiers.get(recv),
3439
+ }),
3440
+ ...(recv && localVarStdlibContracts.has(recv) && {
3441
+ receiverTypeStdlibModule: localVarStdlibContracts.get(recv),
3442
+ }),
3443
+ ...((!recv || constructedReceiverVars.has(recv)) && { receiverConstructed: true }),
3444
+ ...(recv && withBindingVars.has(recv) && { receiverWithBinding: true }),
3445
+ ...(recv && isShadowedByLocal(objNode, recv) && { receiverLocalBinding: true }),
3446
+ isFunctionReference: true,
3447
+ memberRead: true,
3448
+ enclosingFunction: getCurrentEnclosingFunction(),
3449
+ });
3450
+ }
2396
3451
  }
2397
3452
  }
2398
3453
  // Track partial(fn, ...) aliases: fast_process = partial(process, mode='fast')
@@ -2423,6 +3478,37 @@ function findCallsInCode(code, parser) {
2423
3478
  }
2424
3479
  }
2425
3480
  }
3481
+ // One-hop local alias (fix #381): a function local bound
3482
+ // exactly once, by `x = y` or `x = self.attr`, receives
3483
+ // like its initializer. A second binding anywhere in the
3484
+ // function (reassignment, loop target, global/nonlocal)
3485
+ // leaves it untyped.
3486
+ localFieldAliases.delete(left.text);
3487
+ if (!typeNode && right && right.text !== left.text &&
3488
+ (right.type === 'identifier' || right.type === 'attribute')) {
3489
+ const scope = pythonEnclosingFunctionNode(node);
3490
+ if (scope && pythonSingleLocalBinding(scope, left.text)) {
3491
+ if (right.type === 'identifier' && localVarTypes.has(right.text) &&
3492
+ !localVarUnionTypes.has(right.text)) {
3493
+ localVarTypes.set(left.text, localVarTypes.get(right.text),
3494
+ localVarTypes.origins.get(right.text) || 'flow');
3495
+ if (localVarTypeQualifiers.has(right.text)) {
3496
+ localVarTypeQualifiers.set(left.text, localVarTypeQualifiers.get(right.text));
3497
+ }
3498
+ if (constructedReceiverVars.has(right.text)) constructedReceiverVars.add(left.text);
3499
+ if (localVarStdlibContracts.has(right.text)) {
3500
+ localVarStdlibContracts.set(left.text, localVarStdlibContracts.get(right.text));
3501
+ }
3502
+ } else if (right.type === 'attribute') {
3503
+ const path = attributeReceiverPath(right);
3504
+ if (path && path.root !== left.text &&
3505
+ (path.root === 'self' || path.root === 'cls' ||
3506
+ pythonBindingCount(scope, path.root) <= 1)) {
3507
+ localFieldAliases.set(left.text, { node: right, scope: scope.id });
3508
+ }
3509
+ }
3510
+ }
3511
+ }
2426
3512
  // Track non-callable assignments.
2427
3513
  // First: explicit literal check (handles dicts-with-lambdas correctly)
2428
3514
  if (right && isNonCallableInit(right)) {
@@ -2538,6 +3624,7 @@ function findCallsInCode(code, parser) {
2538
3624
  // Direct call: foo()
2539
3625
  const resolvedName = aliases.get(funcNode.text);
2540
3626
  const firstArg = getFirstStringArg(node);
3627
+ const requestConfig = argCount > 0 ? getRequestConfig(node) : null;
2541
3628
  calls.push({
2542
3629
  callSite: typeOrigin('call', funcNode),
2543
3630
  name: funcNode.text,
@@ -2551,14 +3638,22 @@ function findCallsInCode(code, parser) {
2551
3638
  ...(argSpread && { argSpread: true }),
2552
3639
  enclosingFunction,
2553
3640
  uncertain,
2554
- ...(isShadowedByLocal(funcNode, funcNode.text) && { localShadow: true }),
2555
- ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
3641
+ ...pythonShadowFields(funcNode, funcNode.text),
3642
+ ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
3643
+ ...(requestConfig && { requestConfig })
2556
3644
  });
2557
3645
  }
2558
3646
  } else if (funcNode.type === 'attribute') {
2559
3647
  // Method/attribute call: obj.foo() or self.attr.foo()
2560
3648
  const attrNode = funcNode.childForFieldName('attribute');
2561
- const objNode = funcNode.childForFieldName('object');
3649
+ let objNode = funcNode.childForFieldName('object');
3650
+ // fix #381: `config.load()` after `config = self.config`
3651
+ // receives exactly like `self.config.load()`.
3652
+ if (objNode?.type === 'identifier' && localFieldAliases.has(objNode.text) &&
3653
+ !localVarTypes.has(objNode.text)) {
3654
+ const alias = localFieldAliases.get(objNode.text);
3655
+ if (alias.scope === pythonEnclosingFunctionNode(objNode)?.id) objNode = alias.node;
3656
+ }
2562
3657
 
2563
3658
  if (attrNode) {
2564
3659
  let receiver = objNode?.type === 'identifier' ? objNode.text : undefined;
@@ -2629,6 +3724,7 @@ function findCallsInCode(code, parser) {
2629
3724
  // Literal receivers carry their builtin type: {}.get() can
2630
3725
  // never be a project class method
2631
3726
  let subscriptReceiverType;
3727
+ let subscriptElementReceiver = null;
2632
3728
  let receiverSubscriptRoot;
2633
3729
  let receiverSubscriptRootType;
2634
3730
  let receiverSubscriptRootTypeQualifier;
@@ -2645,6 +3741,12 @@ function findCallsInCode(code, parser) {
2645
3741
  subscriptReceiverType =
2646
3742
  localDictValueTypes.get(base.text)?.get(key);
2647
3743
  }
3744
+ const element = localSubscriptElements.get(base.text);
3745
+ if (!subscriptReceiverType && element &&
3746
+ objNode.childForFieldName('subscript')?.type !== 'slice') {
3747
+ subscriptReceiverType = element.type;
3748
+ subscriptElementReceiver = element;
3749
+ }
2648
3750
  }
2649
3751
  }
2650
3752
  if (!receiverSubscriptRoot && receiver) {
@@ -2695,7 +3797,7 @@ function findCallsInCode(code, parser) {
2695
3797
  }
2696
3798
  const receiverTypeQualifier = receiver
2697
3799
  ? localVarTypeQualifiers.get(receiver)
2698
- : undefined;
3800
+ : subscriptElementReceiver?.qualifier;
2699
3801
  const receiverRootType = receiverPath
2700
3802
  ? localVarTypes.get(receiverPath.root) : undefined;
2701
3803
  const dottedReceiver = receiverPath
@@ -2710,8 +3812,11 @@ function findCallsInCode(code, parser) {
2710
3812
  const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
2711
3813
  !localVarTypes.has(receiver);
2712
3814
  const firstArg = getFirstStringArg(node);
3815
+ const requestConfig = argCount > 0 ? getRequestConfig(node) : null;
2713
3816
  const capabilityGuard = receiverCapabilityGuard(
2714
3817
  node, objNode, attrNode.text);
3818
+ let awaitParent = node.parent;
3819
+ while (awaitParent?.type === 'parenthesized_expression') awaitParent = awaitParent.parent;
2715
3820
  calls.push({
2716
3821
  callSite: typeOrigin('call', attrNode),
2717
3822
  name: attrNode.text,
@@ -2720,6 +3825,11 @@ function findCallsInCode(code, parser) {
2720
3825
  // the account's ground set is keyed by the name's line
2721
3826
  line: attrNode.startPosition.row + 1,
2722
3827
  isMethod: true,
3828
+ // `await x.m()` and `f(x.m())` (fix #402): the value
3829
+ // is awaited or passed on; audit-async skips them.
3830
+ ...(awaitParent?.type === 'await' && { awaited: true }),
3831
+ ...((awaitParent?.type === 'argument_list' || awaitParent?.type === 'keyword_argument') &&
3832
+ { passedAsArgument: true }),
2723
3833
  receiver,
2724
3834
  ...(receiverType && { receiverType,
2725
3835
  ...(narrowedType || comprehensionType ? {
@@ -2733,7 +3843,11 @@ function findCallsInCode(code, parser) {
2733
3843
  receiver && !localVarTypes.has(receiver) && rhsReceiver?.type === receiverType
2734
3844
  ? { receiverTypeSource: rhsReceiver.receiverTypeSource,
2735
3845
  receiverTypeEvidence: rhsReceiver.receiverTypeEvidence } :
2736
- receiver ? localVarTypes.fields(receiver, receiverType) : {
3846
+ receiver ? localVarTypes.fields(receiver, receiverType) :
3847
+ subscriptElementReceiver ? {
3848
+ receiverTypeSource: 'annotation',
3849
+ receiverTypeEvidence: typeOrigin('annotation', subscriptElementReceiver.node),
3850
+ } : {
2737
3851
  receiverTypeSource: 'literal', receiverTypeEvidence: typeOrigin('literal', objNode),
2738
3852
  }),
2739
3853
  }),
@@ -2788,7 +3902,8 @@ function findCallsInCode(code, parser) {
2788
3902
  ...(argSpread && { argSpread: true }),
2789
3903
  enclosingFunction,
2790
3904
  uncertain,
2791
- ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
3905
+ ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
3906
+ ...(requestConfig && { requestConfig })
2792
3907
  });
2793
3908
  }
2794
3909
  }
@@ -2820,7 +3935,7 @@ function findCallsInCode(code, parser) {
2820
3935
  isMethod: false,
2821
3936
  isFunctionReference: true,
2822
3937
  isPotentialCallback: true,
2823
- ...(isShadowedByLocal(arg, arg.text) && { localShadow: true }),
3938
+ ...pythonShadowFields(arg, arg.text),
2824
3939
  enclosingFunction
2825
3940
  });
2826
3941
  }
@@ -2868,7 +3983,7 @@ function findCallsInCode(code, parser) {
2868
3983
  isMethod: false,
2869
3984
  isFunctionReference: true,
2870
3985
  isPotentialCallback: true,
2871
- ...(isShadowedByLocal(val, val.text) && { localShadow: true }),
3986
+ ...pythonShadowFields(val, val.text),
2872
3987
  enclosingFunction
2873
3988
  });
2874
3989
  }
@@ -2925,6 +4040,13 @@ function findCallsInCode(code, parser) {
2925
4040
  localDictValueTypes.set(name, values);
2926
4041
  }
2927
4042
  }
4043
+ const savedSubscriptElements = localSubscriptElementsStack.pop();
4044
+ if (savedSubscriptElements) {
4045
+ localSubscriptElements.clear();
4046
+ for (const [name, element] of savedSubscriptElements) {
4047
+ localSubscriptElements.set(name, element);
4048
+ }
4049
+ }
2928
4050
  const savedSubscriptSources = localSubscriptSourcesStack.pop();
2929
4051
  if (savedSubscriptSources) {
2930
4052
  localSubscriptSources.clear();
@@ -3326,10 +4448,43 @@ function findExportsInCode(code, parser) {
3326
4448
  * @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
3327
4449
  * @returns {Array<{line: number, column: number, usageType: string}>}
3328
4450
  */
3329
- function findUsagesInCode(code, name, parser, tree) {
4451
+ function findUsagesInCode(code, name, parser, tree, options = {}) {
3330
4452
  tree = tree || parseTree(parser, code);
4453
+ const usages = findUsagesInTree(code, name, parser, tree, options);
4454
+ // Names inside string annotations that spell calls (fix #398F) are code:
4455
+ // the annotation's expression, parsed from the same source with the
4456
+ // quotes blanked (offsets unchanged).
4457
+ if (!/(?::|->)\s*["'][^"'\n]*\(/.test(code)) return usages;
4458
+ const quotes = annotationStringQuotes(tree).filter(quote =>
4459
+ code.slice(quote.contentStart, quote.contentEnd).includes(name));
4460
+ if (quotes.length === 0) return usages;
4461
+ const chars = code.split('');
4462
+ for (const quote of quotes) {
4463
+ chars[quote.open] = ' ';
4464
+ chars[quote.close] = ' ';
4465
+ }
4466
+ const masked = chars.join('');
4467
+ const maskedTree = parseTree(parser, masked);
4468
+ const ranges = [];
4469
+ for (const quote of quotes) {
4470
+ const node = maskedTree.rootNode.descendantForIndex(quote.contentStart, quote.contentEnd);
4471
+ if (!node || node.hasError || node.isMissing) continue;
4472
+ const start = maskedTree.rootNode.descendantForIndex(quote.contentStart).startPosition;
4473
+ ranges.push({ row: start.row, from: start.column, to: start.column + (quote.contentEnd - quote.contentStart) });
4474
+ }
4475
+ if (ranges.length === 0) return usages;
4476
+ const extra = findUsagesInTree(masked, name, parser, maskedTree, options).filter(usage =>
4477
+ ranges.some(range => usage.line === range.row + 1 && usage.column >= range.from && usage.column < range.to));
4478
+ if (extra.length === 0) return usages;
4479
+ return [...usages, ...extra.map(usage => ({ ...usage, stringAnnotation: true }))]
4480
+ .sort((a, b) => a.line - b.line || a.column - b.column);
4481
+ }
4482
+
4483
+ function findUsagesInTree(code, name, parser, tree, options = {}) {
3331
4484
  const usages = [];
3332
4485
  const moduleAliases = pythonModuleAliases(tree);
4486
+ // Lexical scope verdicts (fix #392) only for refactoring internals.
4487
+ const scopeMemo = options.lexicalScopes ? new Map() : null;
3333
4488
 
3334
4489
  visitNameNodes(tree, code, name, (node) => {
3335
4490
  // Only look for identifiers with the matching name
@@ -3372,11 +4527,19 @@ function findUsagesInCode(code, name, parser, tree) {
3372
4527
  sameNode(parent.childForFieldName('name'), node)) {
3373
4528
  usageType = 'definition';
3374
4529
  }
3375
- // Definition: parameter
4530
+ // Definition: parameter name. A default VALUE is a reference
4531
+ // evaluated in the enclosing scope (fix #392).
3376
4532
  else if (parent.type === 'parameter' ||
3377
- parent.type === 'default_parameter' ||
3378
- parent.type === 'typed_parameter' ||
3379
- parent.type === 'typed_default_parameter') {
4533
+ parent.type === 'parameters' ||
4534
+ parent.type === 'lambda_parameters' ||
4535
+ ((parent.type === 'default_parameter' ||
4536
+ parent.type === 'typed_default_parameter') &&
4537
+ sameNode(parent.childForFieldName('name'), node)) ||
4538
+ (parent.type === 'typed_parameter' &&
4539
+ sameNode(parent.namedChild(0), node)) ||
4540
+ ((parent.type === 'list_splat_pattern' ||
4541
+ parent.type === 'dictionary_splat_pattern') &&
4542
+ /^(parameters|lambda_parameters|typed_parameter)$/.test(parent.parent?.type || ''))) {
3380
4543
  usageType = 'definition';
3381
4544
  }
3382
4545
  // Definition: assignment target (x = ...)
@@ -3439,7 +4602,11 @@ function findUsagesInCode(code, name, parser, tree) {
3439
4602
  }
3440
4603
  }
3441
4604
 
3442
- usages.push({ line, column, usageType });
4605
+ // Where a bare reference resolves (fix #392): plan edits it only
4606
+ // when the language's scoping proves it names the renamed binding.
4607
+ const scope = scopeMemo && usageType === 'reference'
4608
+ ? scopeFields(referenceScope(node, 'python', scopeMemo)) : null;
4609
+ usages.push({ line, column, usageType, ...scope });
3443
4610
  return true;
3444
4611
  });
3445
4612
 
@@ -3454,7 +4621,7 @@ function findUsagesInCode(code, name, parser, tree) {
3454
4621
  * @returns {Map<string, Map<string, string>>} className -> (attrName -> typeName)
3455
4622
  */
3456
4623
  function findInstanceAttributeTypes(code, parser, options = {}) {
3457
- const tree = parseTree(parser, code);
4624
+ const tree = options.tree || parseTree(parser, code);
3458
4625
  const result = new Map(); // className -> Map(attrName -> typeName)
3459
4626
  const explicitContracts = explicitInstanceFieldContracts(tree, parser);
3460
4627
 
@@ -3504,8 +4671,10 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3504
4671
  return valueType === narrowedTypes[0] ? valueType : null;
3505
4672
  };
3506
4673
 
3507
- traverseTreeCached(tree.rootNode, (node) => {
3508
- if (node.type !== 'class_definition') return true;
4674
+ traverseTree(tree.rootNode, (node) => {
4675
+ if (node.type !== 'class_definition') {
4676
+ return node.type !== 'expression_statement' && !FIELD_CONTRACT_VALUE_NODES.has(node.type);
4677
+ }
3509
4678
 
3510
4679
  const classNameNode = node.childForFieldName('name');
3511
4680
  if (!classNameNode) return true;
@@ -3519,12 +4688,22 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3519
4688
  Object.defineProperty(attrTypes, 'origins', { value: attrOrigins });
3520
4689
  const externalFlows = new Map();
3521
4690
  Object.defineProperty(attrTypes, 'externalFlows', { value: externalFlows });
4691
+ // Lines of the writes that typed each field (fix #381): a class name
4692
+ // resolves where it is written, so a function-local class assigned
4693
+ // to self.x names that local class, not a same-name module class.
4694
+ const typeWrites = new Map();
4695
+ const writeLines = new Map();
4696
+ Object.defineProperty(attrTypes, 'writeLines', { value: writeLines });
4697
+ const noteTypeWrite = (field, type, node) => {
4698
+ if (!typeWrites.has(field)) typeWrites.set(field, []);
4699
+ typeWrites.get(field).push({ type, line: node.startPosition.row + 1 });
4700
+ };
3522
4701
  for (const [field, contract] of explicitContracts.get(className) || []) {
3523
4702
  if (contract.type) attrTypes.set(field, contract.type);
3524
4703
  }
3525
4704
  const methodReturns = new Map();
3526
- for (let i = 0; i < body.namedChildCount; i++) {
3527
- let member = body.namedChild(i);
4705
+ const memberDefs = pythonClassBodyDefs(body);
4706
+ for (let member of memberDefs) {
3528
4707
  if (member.type === 'decorated_definition') {
3529
4708
  member = Array.from({ length: member.namedChildCount },
3530
4709
  (_, index) => member.namedChild(index))
@@ -3581,8 +4760,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3581
4760
  }
3582
4761
 
3583
4762
  // Scan __init__ for self.X = ClassName(...) assignments
3584
- for (let i = 0; i < body.childCount; i++) {
3585
- let child = body.child(i);
4763
+ for (let child of memberDefs) {
3586
4764
  // Handle decorated_definition wrapper
3587
4765
  if (child.type === 'decorated_definition') {
3588
4766
  for (let j = 0; j < child.childCount; j++) {
@@ -3662,6 +4840,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3662
4840
  extractConstructorName(rhs);
3663
4841
  if (typeName) {
3664
4842
  attrTypes.set(attrName, typeName);
4843
+ noteTypeWrite(attrName, typeName, assign);
3665
4844
  } else if (rhs.type === 'call') {
3666
4845
  const fn = rhs.childForFieldName('function');
3667
4846
  if (fn?.type === 'attribute') {
@@ -3678,6 +4857,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3678
4857
  } else if (rhs.type === 'identifier' && paramTypes.has(rhs.text)) {
3679
4858
  // self.X = param where param has type annotation
3680
4859
  attrTypes.set(attrName, paramTypes.get(rhs.text));
4860
+ noteTypeWrite(attrName, paramTypes.get(rhs.text), assign);
3681
4861
  }
3682
4862
 
3683
4863
  return true;
@@ -3694,8 +4874,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3694
4874
  const runtimeFieldWrites = new Map();
3695
4875
  const externalFieldWrites = new Map();
3696
4876
  const invalidRuntimeFields = new Set();
3697
- for (let i = 0; i < body.namedChildCount; i++) {
3698
- let member = body.namedChild(i);
4877
+ for (let member of memberDefs) {
3699
4878
  if (member.type === 'decorated_definition') {
3700
4879
  member = Array.from({ length: member.namedChildCount },
3701
4880
  (_, index) => member.namedChild(index))
@@ -3715,7 +4894,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3715
4894
  if (!field) return true;
3716
4895
  let enclosing = assignment.parent;
3717
4896
  while (enclosing && !['function_definition', 'lambda', 'class_definition'].includes(enclosing.type)) enclosing = enclosing.parent;
3718
- if (assignment.type !== 'assignment' || enclosing !== member) {
4897
+ if (assignment.type !== 'assignment' || !sameNode(enclosing, member)) {
3719
4898
  invalidRuntimeFields.add(field);
3720
4899
  return true;
3721
4900
  }
@@ -3772,6 +4951,9 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3772
4951
  }
3773
4952
  if (!runtimeFieldTypes.has(field)) runtimeFieldTypes.set(field, new Set());
3774
4953
  runtimeFieldTypes.get(field).add(runtimeType);
4954
+ if (directConstructor && runtimeType === directConstructor) {
4955
+ noteTypeWrite(field, runtimeType, assignment);
4956
+ }
3775
4957
  if (!runtimeFieldWrites.has(field)) runtimeFieldWrites.set(field, []);
3776
4958
  runtimeFieldWrites.get(field).push({ type: runtimeType,
3777
4959
  assignment: typeOrigin('field', assignment), expression: typeOrigin('field', right),
@@ -3797,6 +4979,11 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3797
4979
  }
3798
4980
  }
3799
4981
 
4982
+ for (const [field, writes] of typeWrites) {
4983
+ const lines = writes.filter(write => write.type === attrTypes.get(field))
4984
+ .map(write => write.line);
4985
+ if (lines.length > 0) writeLines.set(field, [...new Set(lines)].sort((a, b) => a - b));
4986
+ }
3800
4987
  if (attrTypes.size > 0 || externalFlows.size > 0) {
3801
4988
  result.set(className, attrTypes);
3802
4989
  }
@@ -3807,6 +4994,119 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3807
4994
  return result;
3808
4995
  }
3809
4996
 
4997
+ const PY_STORE_CONTAINERS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list',
4998
+ 'parenthesized_expression', 'list_splat_pattern', 'list_splat', 'expression_list']);
4999
+
5000
+ /** Whether an attribute node is written (assigned, deleted, a loop or
5001
+ * `with ... as` target), not read. */
5002
+ function isPythonStoreTarget(node) {
5003
+ let target = node;
5004
+ while (target.parent && PY_STORE_CONTAINERS.has(target.parent.type)) target = target.parent;
5005
+ const parent = target.parent;
5006
+ if (!parent) return false;
5007
+ switch (parent.type) {
5008
+ case 'assignment':
5009
+ case 'augmented_assignment':
5010
+ case 'for_statement':
5011
+ case 'for_in_clause':
5012
+ return sameNode(parent.childForFieldName('left'), target);
5013
+ case 'as_pattern_target':
5014
+ case 'delete_statement':
5015
+ return true;
5016
+ default:
5017
+ return false;
5018
+ }
5019
+ }
5020
+
5021
+ /**
5022
+ * What a class body does to its instance fields (fix #402): the class
5023
+ * members it declares (defs, class-level assignments, nested classes,
5024
+ * through compound statements), the fields its methods write on the
5025
+ * instance or class (`self.x = ..`, `cls.x = ..`, augmented, deleted, loop
5026
+ * and `with ... as` targets, nested functions included), and whether it
5027
+ * writes fields dynamically (`setattr(self, ..)`, `self.__dict__`,
5028
+ * `vars(self)`). Null when no class is declared at `line`.
5029
+ */
5030
+ function findClassFieldFacts(tree, line, name) {
5031
+ if (!tree) return null;
5032
+ const row = line - 1;
5033
+ let classNode = null;
5034
+ const stack = [tree.rootNode];
5035
+ while (stack.length > 0 && !classNode) {
5036
+ const node = stack.pop();
5037
+ if (node.startPosition.row > row || node.endPosition.row < row) continue;
5038
+ if (node.type === 'class_definition') {
5039
+ const nameNode = node.childForFieldName('name');
5040
+ const start = node.parent?.type === 'decorated_definition' ? node.parent : node;
5041
+ if (nameNode?.text === name && (start.startPosition.row === row || node.startPosition.row === row ||
5042
+ nameNode.startPosition.row === row)) { classNode = node; break; }
5043
+ }
5044
+ for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
5045
+ }
5046
+ const body = classNode?.childForFieldName('body');
5047
+ if (!body) return null;
5048
+ const members = new Set();
5049
+ const receivers = new Set(['self', 'cls']);
5050
+ const addTargets = target => {
5051
+ if (!target) return;
5052
+ if (target.type === 'identifier') { members.add(target.text); return; }
5053
+ if (PY_STORE_CONTAINERS.has(target.type)) {
5054
+ for (let i = 0; i < target.namedChildCount; i++) addTargets(target.namedChild(i));
5055
+ }
5056
+ };
5057
+ const visitBody = container => {
5058
+ for (let i = 0; i < container.namedChildCount; i++) {
5059
+ const child = container.namedChild(i);
5060
+ const definition = child.type === 'decorated_definition' ? child.childForFieldName('definition') : child;
5061
+ if (definition?.type === 'function_definition' || definition?.type === 'class_definition') {
5062
+ const declared = definition.childForFieldName('name')?.text;
5063
+ if (declared) members.add(declared);
5064
+ if (definition.type === 'function_definition') {
5065
+ const params = definition.childForFieldName('parameters');
5066
+ const first = params?.namedChild(0);
5067
+ const firstName = first?.type === 'identifier' ? first.text
5068
+ : (first?.childForFieldName?.('name') || first?.namedChild(0))?.text;
5069
+ if (firstName) receivers.add(firstName);
5070
+ }
5071
+ continue;
5072
+ }
5073
+ if (child.type === 'expression_statement') {
5074
+ for (let j = 0; j < child.namedChildCount; j++) {
5075
+ const expression = child.namedChild(j);
5076
+ if (expression.type === 'assignment' || expression.type === 'augmented_assignment') {
5077
+ addTargets(expression.childForFieldName('left'));
5078
+ }
5079
+ }
5080
+ continue;
5081
+ }
5082
+ if (PY_CLASS_BODY_COMPOUNDS.has(child.type)) visitBody(child);
5083
+ }
5084
+ };
5085
+ visitBody(body);
5086
+ const written = new Set();
5087
+ let dynamic = false;
5088
+ const pending = [body];
5089
+ while (pending.length > 0) {
5090
+ const node = pending.pop();
5091
+ if (node.type === 'class_definition') continue;
5092
+ if (node.type === 'attribute') {
5093
+ const object = node.childForFieldName('object');
5094
+ const attribute = node.childForFieldName('attribute');
5095
+ if (object?.type === 'identifier' && receivers.has(object.text) && attribute) {
5096
+ if (attribute.text === '__dict__') dynamic = true;
5097
+ else if (isPythonStoreTarget(node)) written.add(attribute.text);
5098
+ }
5099
+ } else if (node.type === 'call') {
5100
+ const fn = node.childForFieldName('function');
5101
+ const first = node.childForFieldName('arguments')?.namedChild(0);
5102
+ if (fn?.type === 'identifier' && (fn.text === 'setattr' || fn.text === 'vars' ||
5103
+ fn.text === 'delattr') && first?.type === 'identifier' && receivers.has(first.text)) dynamic = true;
5104
+ }
5105
+ for (let i = node.namedChildCount - 1; i >= 0; i--) pending.push(node.namedChild(i));
5106
+ }
5107
+ return { members, written, dynamic };
5108
+ }
5109
+
3810
5110
  /**
3811
5111
  * Extract constructor class name from an expression node.
3812
5112
  * Handles: ClassName(...), param or ClassName(...), (param or ClassName(...)),
@@ -3899,8 +5199,8 @@ function isEntryPoint(symbol) {
3899
5199
  /** Module-level pytest declarations. Decorator ownership is resolved by the
3900
5200
  * index; a decorator merely spelled `fixture` is not sufficient evidence.
3901
5201
  */
3902
- function findPythonModuleEvidence(code, parser) {
3903
- const tree = parseTree(parser, code), references = [], wildcards = [];
5202
+ function findPythonModuleEvidence(code, parser, parsedTree) {
5203
+ const tree = parsedTree || parseTree(parser, code), references = [], wildcards = [];
3904
5204
  traverseTree(tree.rootNode, node => {
3905
5205
  if (node.type === 'identifier' && node.text === '__all__') references.push(node);
3906
5206
  if (node.type === 'import_from_statement' && node.namedChildren.some(child => child.type === 'wildcard_import')) {
@@ -3913,7 +5213,7 @@ function findPythonModuleEvidence(code, parser) {
3913
5213
  let exports = null;
3914
5214
  if (references.length === 1) {
3915
5215
  const name = references[0], assignment = name.parent, value = assignment.childForFieldName('right');
3916
- if (assignment.type === 'assignment' && assignment.childForFieldName('left') === name &&
5216
+ if (assignment.type === 'assignment' && sameNode(assignment.childForFieldName('left'), name) &&
3917
5217
  assignment.parent?.parent?.type === 'module' && ['list', 'tuple'].includes(value?.type)) {
3918
5218
  const literals = value.namedChildren.map(item => {
3919
5219
  const content = item.type === 'string' && item.namedChildren.find(child => child.type === 'string_content');
@@ -3927,8 +5227,8 @@ function findPythonModuleEvidence(code, parser) {
3927
5227
  return { exports, wildcards };
3928
5228
  }
3929
5229
 
3930
- function findPytestFunctions(code, parser) {
3931
- const tree = parseTree(parser, code);
5230
+ function findPytestFunctions(code, parser, parsedTree) {
5231
+ const tree = parsedTree || parseTree(parser, code);
3932
5232
  const pathOf = node => {
3933
5233
  if (node?.type === 'identifier') return [node.text];
3934
5234
  if (node?.type !== 'attribute') return null;
@@ -4015,7 +5315,191 @@ function getBuiltinFieldType(moduleName, fieldName) {
4015
5315
  return PY_BUILTIN_FIELD_TYPES[`${moduleName}.${fieldName}`] || null;
4016
5316
  }
4017
5317
 
5318
+ /**
5319
+ * Other bindings of `name` in the scope that holds the definition on
5320
+ * `defLine` (fix #376). Python binds names at run time: every `def`,
5321
+ * `class`, assignment target and import of the name in one scope (module,
5322
+ * class body or function body, through if/elif/else, try/except/finally,
5323
+ * with and loop blocks) is the SAME variable, so version-conditional
5324
+ * definitions (`if sys.version_info >= ...: f = lib.f else: def f(...)`),
5325
+ * try/except import fallbacks and class-body rebindings are alternatives a
5326
+ * rename must change together. In a class body, bare reads of the name
5327
+ * (`close = release`, `@prop.setter`) bind to the class-scope variable and
5328
+ * are references to the member.
5329
+ * @returns {{line, column, kind: 'definition'|'assignment'|'import'|'import-name'|'reference'}[]}
5330
+ * column is the byte column of the name token; 'import-name' is an
5331
+ * unaliased `from m import name` (its alias must be added, not renamed).
5332
+ */
5333
+ function findScopeAlternativeBindings(tree, defLine, name) {
5334
+ if (!tree || !name) return [];
5335
+ const row = defLine - 1;
5336
+ let defNode = null;
5337
+ const stack = [tree.rootNode];
5338
+ while (stack.length > 0 && !defNode) {
5339
+ const node = stack.pop();
5340
+ if (node.startPosition.row > row || node.endPosition.row < row) continue;
5341
+ if ((node.type === 'function_definition' || node.type === 'class_definition')) {
5342
+ const nameNode = node.childForFieldName('name');
5343
+ if (nameNode && nameNode.text === name && nameNode.startPosition.row === row) {
5344
+ defNode = node;
5345
+ break;
5346
+ }
5347
+ }
5348
+ for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
5349
+ }
5350
+ if (!defNode) return [];
5351
+ let container = null;
5352
+ for (let cur = defNode; cur.parent; cur = cur.parent) {
5353
+ const parent = cur.parent;
5354
+ if (parent.type === 'module') { container = parent; break; }
5355
+ if (parent.type === 'block' && parent.parent &&
5356
+ (parent.parent.type === 'class_definition' ||
5357
+ parent.parent.type === 'function_definition')) {
5358
+ container = parent;
5359
+ break;
5360
+ }
5361
+ }
5362
+ if (!container) return [];
5363
+ const classBody = container.parent?.type === 'class_definition';
5364
+ const out = [];
5365
+ const push = (node, kind) => out.push({
5366
+ line: node.startPosition.row + 1, column: node.startPosition.column, kind,
5367
+ });
5368
+ const references = expr => {
5369
+ if (!classBody || !expr) return;
5370
+ const refStack = [expr];
5371
+ while (refStack.length > 0) {
5372
+ const node = refStack.pop();
5373
+ if (node.type === 'lambda' || node.type === 'function_definition' ||
5374
+ node.type === 'class_definition' || node.type.endsWith('comprehension') ||
5375
+ node.type === 'generator_expression') continue;
5376
+ if (node.type === 'identifier') {
5377
+ if (node.text === name) push(node, 'reference');
5378
+ continue;
5379
+ }
5380
+ if (node.type === 'attribute') {
5381
+ refStack.push(node.childForFieldName('object'));
5382
+ continue;
5383
+ }
5384
+ if (node.type === 'keyword_argument') {
5385
+ refStack.push(node.childForFieldName('value'));
5386
+ continue;
5387
+ }
5388
+ for (let i = 0; i < node.namedChildCount; i++) {
5389
+ const child = node.namedChild(i);
5390
+ if (child) refStack.push(child);
5391
+ }
5392
+ }
5393
+ };
5394
+ const targets = (target, kind) => {
5395
+ if (!target) return;
5396
+ if (target.type === 'identifier') {
5397
+ if (target.text === name) push(target, kind);
5398
+ return;
5399
+ }
5400
+ if (['pattern_list', 'tuple_pattern', 'list_pattern', 'expression_list', 'tuple',
5401
+ 'list', 'parenthesized_expression'].includes(target.type)) {
5402
+ for (let i = 0; i < target.namedChildCount; i++) targets(target.namedChild(i), kind);
5403
+ return;
5404
+ }
5405
+ references(target);
5406
+ };
5407
+ const visit = node => {
5408
+ for (let i = 0; i < node.namedChildCount; i++) {
5409
+ const child = node.namedChild(i);
5410
+ if (!child) continue;
5411
+ switch (child.type) {
5412
+ case 'function_definition':
5413
+ case 'class_definition': {
5414
+ const nameNode = child.childForFieldName('name');
5415
+ if (nameNode && nameNode.text === name && child.startIndex !== defNode.startIndex) {
5416
+ push(nameNode, 'definition');
5417
+ }
5418
+ // Evaluated in this scope: base classes, return and
5419
+ // parameter annotations, parameter defaults - never the
5420
+ // parameter names themselves.
5421
+ references(child.childForFieldName('superclasses'));
5422
+ references(child.childForFieldName('return_type'));
5423
+ const params = child.childForFieldName('parameters');
5424
+ for (let j = 0; j < (params?.namedChildCount || 0); j++) {
5425
+ const param = params.namedChild(j);
5426
+ references(param.childForFieldName?.('type'));
5427
+ references(param.childForFieldName?.('value'));
5428
+ }
5429
+ break;
5430
+ }
5431
+ case 'decorated_definition':
5432
+ for (let j = 0; j < child.namedChildCount; j++) {
5433
+ const part = child.namedChild(j);
5434
+ if (part.type === 'decorator') references(part);
5435
+ }
5436
+ visit({ namedChildCount: 1, namedChild: () => child.childForFieldName('definition') });
5437
+ break;
5438
+ case 'expression_statement':
5439
+ visit(child);
5440
+ break;
5441
+ case 'assignment':
5442
+ case 'augmented_assignment': {
5443
+ targets(child.childForFieldName('left'), 'assignment');
5444
+ references(child.childForFieldName('type'));
5445
+ const right = child.childForFieldName('right');
5446
+ if (right && right.type === 'assignment') visit({ namedChildCount: 1, namedChild: () => right });
5447
+ else references(right);
5448
+ break;
5449
+ }
5450
+ case 'import_from_statement':
5451
+ case 'import_statement':
5452
+ for (let j = 0; j < child.namedChildCount; j++) {
5453
+ const item = child.namedChild(j);
5454
+ if (item.type === 'aliased_import') {
5455
+ const alias = item.childForFieldName('alias');
5456
+ if (alias && alias.text === name) push(alias, 'import');
5457
+ } else if (item.type === 'dotted_name' &&
5458
+ (child.type === 'import_statement' ||
5459
+ !sameNode(item, child.childForFieldName('module_name')))) {
5460
+ const parts = item.namedChildren || [];
5461
+ if (parts.length === 1 && parts[0].text === name) {
5462
+ push(parts[0], 'import-name');
5463
+ }
5464
+ }
5465
+ }
5466
+ break;
5467
+ case 'if_statement':
5468
+ case 'elif_clause':
5469
+ case 'else_clause':
5470
+ case 'try_statement':
5471
+ case 'except_clause':
5472
+ case 'except_group_clause':
5473
+ case 'finally_clause':
5474
+ case 'with_statement':
5475
+ case 'for_statement':
5476
+ case 'while_statement':
5477
+ case 'block':
5478
+ case 'match_statement':
5479
+ case 'case_clause':
5480
+ if (child.type === 'for_statement') {
5481
+ targets(child.childForFieldName('left'), 'assignment');
5482
+ references(child.childForFieldName('right'));
5483
+ for (const field of ['body', 'alternative']) {
5484
+ const part = child.childForFieldName(field);
5485
+ if (part) visit({ namedChildCount: 1, namedChild: () => part });
5486
+ }
5487
+ break;
5488
+ }
5489
+ visit(child);
5490
+ break;
5491
+ default:
5492
+ if (child.type !== 'comment' && child.type !== 'pass_statement') references(child);
5493
+ }
5494
+ }
5495
+ };
5496
+ visit(container);
5497
+ out.sort((a, b) => a.line - b.line || a.column - b.column);
5498
+ return out;
5499
+ }
5500
+
4018
5501
  module.exports = {
5502
+ findScopeAlternativeBindings,
4019
5503
  findPythonModuleEvidence,
4020
5504
  findFunctions,
4021
5505
  findClasses,
@@ -4025,6 +5509,7 @@ module.exports = {
4025
5509
  findExportsInCode,
4026
5510
  findUsagesInCode,
4027
5511
  findInstanceAttributeTypes,
5512
+ findClassFieldFacts,
4028
5513
  getBuiltinCallReturnType,
4029
5514
  getBuiltinFieldType,
4030
5515
  findPytestFunctions,