ucn 5.0.6 → 5.2.0

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.
@@ -686,6 +686,67 @@ function typeNameFromAnnotation(typeNode) {
686
686
  return typeNameFromExpr(inner);
687
687
  }
688
688
 
689
+ /**
690
+ * Companion to typeNameFromAnnotation: the dotted module qualifier that owns
691
+ * the annotated name (fix #286e, flask-measured: `app: flask.Flask` typed the
692
+ * receiver bare 'Flask', and with a second test-local Flask class the origin
693
+ * fell back to directory proximity and excluded a compiler-true caller).
694
+ * Returns undefined when the annotation carries no qualifier.
695
+ */
696
+ function typeQualifierFromAnnotation(typeNode) {
697
+ if (!typeNode) return undefined;
698
+ const inner = typeNode.namedChildCount > 0 ? typeNode.namedChild(0) : null;
699
+ return typeQualifierFromExpr(inner);
700
+ }
701
+
702
+ function typeQualifierFromExpr(node) {
703
+ if (!node) return undefined;
704
+ switch (node.type) {
705
+ case 'attribute':
706
+ return node.childForFieldName('object')?.text;
707
+ case 'parenthesized_expression':
708
+ return node.namedChildCount === 1
709
+ ? typeQualifierFromExpr(node.namedChild(0)) : undefined;
710
+ case 'binary_operator': {
711
+ const left = node.namedChild(0);
712
+ const right = node.namedChild(1);
713
+ if (left?.type === 'none' && right?.type !== 'none') return typeQualifierFromExpr(right);
714
+ if (right?.type === 'none' && left?.type !== 'none') return typeQualifierFromExpr(left);
715
+ return undefined;
716
+ }
717
+ case 'subscript': {
718
+ const base = typeNameFromExpr(node.childForFieldName('value'));
719
+ if (PY_TYPE_WRAPPERS.has(base)) {
720
+ return typeQualifierFromExpr(node.childForFieldName('subscript'));
721
+ }
722
+ return undefined;
723
+ }
724
+ case 'generic_type': {
725
+ const base = typeNameFromExpr(node.namedChild(0));
726
+ if (PY_TYPE_WRAPPERS.has(base)) {
727
+ const params = node.namedChild(1);
728
+ const firstType = params && params.namedChildCount > 0 ? params.namedChild(0) : null;
729
+ return typeQualifierFromAnnotation(firstType);
730
+ }
731
+ return undefined;
732
+ }
733
+ case 'string': {
734
+ for (let i = 0; i < node.childCount; i++) {
735
+ const c = node.child(i);
736
+ if (c.type === 'string_content') {
737
+ const txt = c.text.trim();
738
+ if (/^[A-Za-z_][\w.]*$/.test(txt) && txt.includes('.')) {
739
+ return txt.split('.').slice(0, -1).join('.');
740
+ }
741
+ }
742
+ }
743
+ return undefined;
744
+ }
745
+ default:
746
+ return undefined;
747
+ }
748
+ }
749
+
689
750
  function typeNamesFromAnnotation(typeNode) {
690
751
  if (!typeNode) return [];
691
752
  const inner = typeNode.namedChildCount > 0 ? typeNode.namedChild(0) : null;
@@ -1015,6 +1076,35 @@ function assignmentTargetOf(callNode) {
1015
1076
  return undefined;
1016
1077
  }
1017
1078
 
1079
+ /**
1080
+ * For-loop iteration target of a call used as the iterable:
1081
+ * `for ep in entry_points(...)` / comprehension `for x in items()`.
1082
+ * The loop variable holds an ELEMENT of the producer's result, never the
1083
+ * return type itself — consumers must only use this for provenance
1084
+ * (external-producer demotion), never positive typing (fix #294).
1085
+ */
1086
+ function iterTargetOf(callNode) {
1087
+ let n = callNode;
1088
+ let p = n.parent;
1089
+ if (p && p.type === 'await') { n = p; p = n.parent; }
1090
+ if (!p || (p.type !== 'for_statement' && p.type !== 'for_in_clause')) return undefined;
1091
+ const right = p.childForFieldName('right');
1092
+ if (!right || right.id !== n.id) return undefined;
1093
+ const left = p.childForFieldName('left');
1094
+ if (!left) return undefined;
1095
+ const names = [];
1096
+ if (left.type === 'identifier') {
1097
+ names.push(left.text);
1098
+ } else if (left.type === 'pattern_list' || left.type === 'tuple_pattern') {
1099
+ for (let i = 0; i < left.namedChildCount; i++) {
1100
+ const c = left.namedChild(i);
1101
+ if (c.type === 'identifier') names.push(c.text);
1102
+ }
1103
+ }
1104
+ if (names.length === 0) return undefined;
1105
+ return { first: names[0], rest: names.slice(1) };
1106
+ }
1107
+
1018
1108
  function contextTargetOf(callNode) {
1019
1109
  const pattern = callNode?.parent;
1020
1110
  if (pattern?.type !== 'as_pattern' ||
@@ -1170,6 +1260,247 @@ function pickleRoundTripSource(node) {
1170
1260
  return value?.type === 'identifier' ? value.text : null;
1171
1261
  }
1172
1262
 
1263
+ function pythonTargetBindsName(left, name) {
1264
+ if (!left) return false;
1265
+ if (left.type === 'identifier' && left.text === name) return true;
1266
+ if (left.type === 'pattern_list' || left.type === 'tuple_pattern') {
1267
+ for (let i = 0; i < left.namedChildCount; i++) {
1268
+ if (left.namedChild(i).type === 'identifier' &&
1269
+ left.namedChild(i).text === name) return true;
1270
+ }
1271
+ }
1272
+ return false;
1273
+ }
1274
+
1275
+ function pythonScopeBindsName(scopeNode, name) {
1276
+ for (let i = 0; i < scopeNode.namedChildCount; i++) {
1277
+ const child = scopeNode.namedChild(i);
1278
+ if (child.type === 'function_definition' ||
1279
+ child.type === 'async_function_definition' ||
1280
+ child.type === 'class_definition') {
1281
+ // The nested body is a separate scope, but the declaration name
1282
+ // binds in this scope.
1283
+ if (child.childForFieldName('name')?.text === name) return true;
1284
+ continue;
1285
+ }
1286
+ if (child.type === 'lambda') continue;
1287
+ if (child.type === 'assignment' ||
1288
+ child.type === 'augmented_assignment' ||
1289
+ child.type === 'named_expression') {
1290
+ if (pythonTargetBindsName(
1291
+ child.childForFieldName('left') || child.childForFieldName('name'),
1292
+ name)) return true;
1293
+ } else if (child.type === 'for_statement') {
1294
+ if (pythonTargetBindsName(child.childForFieldName('left'), name)) return true;
1295
+ } else if (child.type === 'with_statement') {
1296
+ const text = child.namedChild(0)?.text || '';
1297
+ const match = text.match(/\bas\s+([A-Za-z_][A-Za-z0-9_]*)/);
1298
+ if (match && match[1] === name) return true;
1299
+ }
1300
+ if (pythonScopeBindsName(child, name)) return true;
1301
+ }
1302
+ return false;
1303
+ }
1304
+
1305
+ const PY_COMPREHENSIONS = new Set([
1306
+ 'generator_expression', 'list_comprehension', 'set_comprehension',
1307
+ 'dictionary_comprehension',
1308
+ ]);
1309
+
1310
+ // Python locals are function-scoped: an assignment anywhere in the function
1311
+ // shadows an imported module for every reference in that function. Keep this
1312
+ // shared between call records and reference records so plan cannot promote a
1313
+ // receiver that callers would reject as locally rebound.
1314
+ function isPythonNameShadowedAt(refNode, name) {
1315
+ for (let parent = refNode.parent; parent; parent = parent.parent) {
1316
+ if (PY_COMPREHENSIONS.has(parent.type)) {
1317
+ for (let i = 0; i < parent.namedChildCount; i++) {
1318
+ const clause = parent.namedChild(i);
1319
+ if (clause.type === 'for_in_clause' &&
1320
+ pythonTargetBindsName(clause.childForFieldName('left'), name)) {
1321
+ return true;
1322
+ }
1323
+ }
1324
+ }
1325
+ if (parent.type === 'lambda') {
1326
+ const params = parent.childForFieldName('parameters');
1327
+ if (params) for (let i = 0; i < params.namedChildCount; i++) {
1328
+ const param = params.namedChild(i);
1329
+ if (param.type === 'identifier' && param.text === name) return true;
1330
+ if (param.type === 'default_parameter' &&
1331
+ param.childForFieldName('name')?.text === name) return true;
1332
+ }
1333
+ }
1334
+ if (parent.type === 'function_definition' ||
1335
+ parent.type === 'async_function_definition') {
1336
+ const params = parent.childForFieldName('parameters');
1337
+ if (params) {
1338
+ for (let i = 0; i < params.namedChildCount; i++) {
1339
+ const param = params.namedChild(i);
1340
+ const paramName = param.type === 'identifier'
1341
+ ? param
1342
+ : (param.childForFieldName('name') || param.namedChild(0));
1343
+ if (paramName?.type === 'identifier' &&
1344
+ paramName.text === name) return true;
1345
+ }
1346
+ }
1347
+ const body = parent.childForFieldName('body');
1348
+ return body ? pythonScopeBindsName(body, name) : false;
1349
+ }
1350
+ }
1351
+ return false;
1352
+ }
1353
+
1354
+ /**
1355
+ * Whether an uppercase call target is a local VALUE binding rather than a
1356
+ * class declaration.
1357
+ *
1358
+ * Python permits runtime class factories and decorators to be rebound under
1359
+ * class-shaped names:
1360
+ *
1361
+ * C2 = attrs.define(...)(Base)
1362
+ * value = C2()
1363
+ *
1364
+ * The capitalization convention alone cannot type `value` as an indexed
1365
+ * `class C2` from another lexical scope. Scan the nearest Python scope using
1366
+ * AST bindings and reject constructor-name inference when a parameter,
1367
+ * function, assignment, loop, or with-target owns the name. A sole local
1368
+ * class declaration is deliberately allowed; its exact declaration is
1369
+ * selected later with the call site's enclosing-function range.
1370
+ */
1371
+ function isPythonConstructorValueShadowedAt(refNode, name) {
1372
+ let scope = null;
1373
+ for (let parent = refNode?.parent; parent; parent = parent.parent) {
1374
+ if (PY_COMPREHENSIONS.has(parent.type)) {
1375
+ for (let i = 0; i < parent.namedChildCount; i++) {
1376
+ const clause = parent.namedChild(i);
1377
+ if (clause.type === 'for_in_clause' &&
1378
+ pythonTargetBindsName(
1379
+ clause.childForFieldName('left'), name)) return true;
1380
+ }
1381
+ }
1382
+ if (parent.type === 'lambda') {
1383
+ const params = parent.childForFieldName('parameters');
1384
+ if (params) {
1385
+ for (let i = 0; i < params.namedChildCount; i++) {
1386
+ const param = params.namedChild(i);
1387
+ const paramName = param.type === 'identifier'
1388
+ ? param
1389
+ : (param.childForFieldName('name') || param.namedChild(0));
1390
+ if (paramName?.type === 'identifier' &&
1391
+ paramName.text === name) return true;
1392
+ }
1393
+ }
1394
+ scope = parent;
1395
+ break;
1396
+ }
1397
+ if (parent.type === 'function_definition' ||
1398
+ parent.type === 'async_function_definition') {
1399
+ const params = parent.childForFieldName('parameters');
1400
+ if (params) {
1401
+ for (let i = 0; i < params.namedChildCount; i++) {
1402
+ const param = params.namedChild(i);
1403
+ const paramName = param.type === 'identifier'
1404
+ ? param
1405
+ : (param.childForFieldName('name') || param.namedChild(0));
1406
+ if (paramName?.type === 'identifier' &&
1407
+ paramName.text === name) return true;
1408
+ }
1409
+ }
1410
+ scope = parent.childForFieldName('body');
1411
+ break;
1412
+ }
1413
+ if (parent.type === 'class_definition') {
1414
+ scope = parent.childForFieldName('body');
1415
+ break;
1416
+ }
1417
+ if (parent.type === 'module') {
1418
+ scope = parent;
1419
+ break;
1420
+ }
1421
+ }
1422
+ if (!scope) return false;
1423
+
1424
+ let classDeclarations = 0;
1425
+ const stack = [scope];
1426
+ while (stack.length > 0) {
1427
+ const node = stack.pop();
1428
+ if (node !== scope && (node.type === 'function_definition' ||
1429
+ node.type === 'async_function_definition' ||
1430
+ node.type === 'class_definition')) {
1431
+ const declaredName = node.childForFieldName('name')?.text;
1432
+ if (declaredName === name) {
1433
+ if (node.type === 'class_definition') classDeclarations++;
1434
+ else return true;
1435
+ }
1436
+ // The declaration name binds in this scope; its body does not.
1437
+ continue;
1438
+ }
1439
+ if (node.type === 'decorated_definition') {
1440
+ let declaration = null;
1441
+ for (let i = 0; i < node.namedChildCount; i++) {
1442
+ const child = node.namedChild(i);
1443
+ if (child.type === 'function_definition' ||
1444
+ child.type === 'async_function_definition' ||
1445
+ child.type === 'class_definition') {
1446
+ declaration = child;
1447
+ break;
1448
+ }
1449
+ }
1450
+ if (declaration) {
1451
+ const declaredName = declaration.childForFieldName('name')?.text;
1452
+ if (declaredName === name) {
1453
+ if (declaration.type === 'class_definition') classDeclarations++;
1454
+ else return true;
1455
+ }
1456
+ continue;
1457
+ }
1458
+ }
1459
+ if (node.type === 'assignment' ||
1460
+ node.type === 'augmented_assignment' ||
1461
+ node.type === 'named_expression') {
1462
+ if (pythonTargetBindsName(
1463
+ node.childForFieldName('left') || node.childForFieldName('name'),
1464
+ name)) return true;
1465
+ } else if (node.type === 'for_statement' || node.type === 'for_in_clause') {
1466
+ if (pythonTargetBindsName(node.childForFieldName('left'), name)) return true;
1467
+ } else if (node.type === 'with_item') {
1468
+ const value = node.childForFieldName('value') || node.namedChild(0);
1469
+ const target = value?.type === 'as_pattern'
1470
+ ? value.namedChild(value.namedChildCount - 1) : null;
1471
+ if (pythonTargetBindsName(
1472
+ target?.type === 'as_pattern_target' ? target.namedChild(0) : target,
1473
+ name)) return true;
1474
+ }
1475
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
1476
+ stack.push(node.namedChild(i));
1477
+ }
1478
+ }
1479
+ // Two conditional/repeated local class declarations with the same name
1480
+ // are not one stable identity. Refuse the heuristic and leave the value
1481
+ // on the visible unverified rail.
1482
+ return classDeclarations > 1;
1483
+ }
1484
+
1485
+ function pythonModuleAliases(tree) {
1486
+ const aliases = new Set();
1487
+ traverseTreeCached(tree.rootNode, node => {
1488
+ if (node.type !== 'import_statement') return true;
1489
+ for (let i = 0; i < node.namedChildCount; i++) {
1490
+ const child = node.namedChild(i);
1491
+ if (child.type === 'dotted_name') {
1492
+ const first = child.namedChild(0);
1493
+ if (first?.type === 'identifier') aliases.add(first.text);
1494
+ } else if (child.type === 'aliased_import') {
1495
+ const alias = child.childForFieldName('alias');
1496
+ if (alias?.type === 'identifier') aliases.add(alias.text);
1497
+ }
1498
+ }
1499
+ return true;
1500
+ });
1501
+ return aliases;
1502
+ }
1503
+
1173
1504
  function findCallsInCode(code, parser) {
1174
1505
  const tree = parseTree(parser, code);
1175
1506
  const calls = [];
@@ -1312,89 +1643,22 @@ function findCallsInCode(code, parser) {
1312
1643
  };
1313
1644
  };
1314
1645
 
1315
- // fix #203: is a bare-identifier function REFERENCE shadowed by a local of
1316
- // the enclosing function? Python locals are FUNCTION-scoped and an
1317
- // assignment ANYWHERE in the function makes the name local for ALL its
1318
- // references (UnboundLocalError semantics) — so scan the whole enclosing
1319
- // function subtree (excluding nested function bodies, which are separate
1320
- // scopes) for assignment/for/with-as/walrus bindings of the name.
1321
- // Enclosing-function PARAMS are checked at query time in findCallers.
1322
- const _targetBindsName = (left, name) => {
1323
- if (!left) return false;
1324
- if (left.type === 'identifier' && left.text === name) return true;
1325
- if (left.type === 'pattern_list' || left.type === 'tuple_pattern') {
1326
- for (let j = 0; j < left.namedChildCount; j++) {
1327
- if (left.namedChild(j).type === 'identifier' && left.namedChild(j).text === name) return true;
1328
- }
1329
- }
1330
- return false;
1331
- };
1332
- const _bindsNameInScope = (scopeNode, name) => {
1333
- for (let i = 0; i < scopeNode.namedChildCount; i++) {
1334
- const c = scopeNode.namedChild(i);
1335
- if (c.type === 'function_definition' || c.type === 'async_function_definition' ||
1336
- c.type === 'class_definition') {
1337
- // The body is a separate scope, but the DEF NAME itself is an
1338
- // assignment in THIS scope (fix #218: a nested `def get_style`
1339
- // shadows the name for sibling references).
1340
- if (c.childForFieldName('name')?.text === name) return true;
1341
- continue;
1342
- }
1343
- if (c.type === 'lambda') continue; // separate scope, no name
1344
- if (c.type === 'assignment' || c.type === 'augmented_assignment' || c.type === 'named_expression') {
1345
- if (_targetBindsName(c.childForFieldName('left') || c.childForFieldName('name'), name)) return true;
1346
- } else if (c.type === 'for_statement') {
1347
- if (_targetBindsName(c.childForFieldName('left'), name)) return true;
1348
- } else if (c.type === 'with_statement') {
1349
- // with open(f) as fh: — as-target is inside with_clause/with_item
1350
- const text = c.namedChild(0)?.text || '';
1351
- const m = text.match(/\bas\s+([A-Za-z_][A-Za-z0-9_]*)/);
1352
- if (m && m[1] === name) return true;
1353
- }
1354
- if (_bindsNameInScope(c, name)) return true;
1355
- }
1356
- return false;
1357
- };
1358
- const PY_COMPREHENSIONS = new Set([
1359
- 'generator_expression', 'list_comprehension', 'set_comprehension', 'dictionary_comprehension',
1360
- ]);
1361
- const isShadowedByLocal = (refNode, name) => {
1362
- for (let p = refNode.parent; p; p = p.parent) {
1363
- // Comprehension for-clause targets are scoped to the comprehension
1364
- // itself (PEP 3110-era scoping): `cell_len(line) for line in lines`
1365
- // binds `line` ONLY inside the comprehension — block-accurate, so
1366
- // check on the way up rather than function-wide (fix #218).
1367
- if (PY_COMPREHENSIONS.has(p.type)) {
1368
- for (let i = 0; i < p.namedChildCount; i++) {
1369
- const c = p.namedChild(i);
1370
- if (c.type === 'for_in_clause' && _targetBindsName(c.childForFieldName('left'), name)) return true;
1371
- }
1372
- }
1373
- // Lambda params shadow their body the same way (fix #218).
1374
- if (p.type === 'lambda') {
1375
- const params = p.childForFieldName('parameters');
1376
- if (params) for (let i = 0; i < params.namedChildCount; i++) {
1377
- const c = params.namedChild(i);
1378
- if (c.type === 'identifier' && c.text === name) return true;
1379
- if (c.type === 'default_parameter' && c.childForFieldName('name')?.text === name) return true;
1380
- }
1381
- }
1382
- if (p.type === 'function_definition' || p.type === 'async_function_definition') {
1383
- const params = p.childForFieldName('parameters');
1384
- if (params) {
1385
- for (let i = 0; i < params.namedChildCount; i++) {
1386
- const prm = params.namedChild(i);
1387
- const prmName = prm.type === 'identifier'
1388
- ? prm
1389
- : (prm.childForFieldName('name') || prm.namedChild(0));
1390
- if (prmName?.type === 'identifier' && prmName.text === name) return true;
1391
- }
1392
- }
1393
- const body = p.childForFieldName('body');
1394
- return body ? _bindsNameInScope(body, name) : false;
1395
- }
1396
- }
1397
- return false; // module level — that's a module binding, not a shadow
1646
+ const isShadowedByLocal = isPythonNameShadowedAt;
1647
+ const exactConstructorInfo = funcNode => {
1648
+ const ctor = constructorTypeInfo(funcNode);
1649
+ if (!ctor) return undefined;
1650
+ if (!ctor.qualifier) {
1651
+ return isPythonConstructorValueShadowedAt(funcNode, ctor.type)
1652
+ ? undefined : ctor;
1653
+ }
1654
+ // Attribute calls are constructors only when the receiver is an
1655
+ // unshadowed module alias. `self.Widget()` / `factory.Widget()` and a
1656
+ // parameter shadowing `import pkg` are dynamic attribute dispatch,
1657
+ // not type identity. Previously their qualifier was discarded and
1658
+ // the terminal name could borrow an unrelated project class.
1659
+ const root = ctor.qualifier.split('.')[0];
1660
+ return moduleAliases.has(root) && !isShadowedByLocal(funcNode, root)
1661
+ ? ctor : undefined;
1398
1662
  };
1399
1663
 
1400
1664
  traverseTree(tree.rootNode, (node) => {
@@ -1470,6 +1734,15 @@ function findCallsInCode(code, parser) {
1470
1734
  if (receiverType && !['self', 'cls'].includes(nameNode.text)) {
1471
1735
  localVarTypes.set(nameNode.text, receiverType);
1472
1736
  declaredVarTypes.set(nameNode.text, receiverType);
1737
+ // The annotation's module qualifier is identity (fix
1738
+ // #286e) — splat params got a builtin type, no qualifier.
1739
+ const annotationQualifier = receiverType === typeName
1740
+ ? typeQualifierFromAnnotation(typeNode) : undefined;
1741
+ if (annotationQualifier) {
1742
+ localVarTypeQualifiers.set(nameNode.text, annotationQualifier);
1743
+ } else {
1744
+ localVarTypeQualifiers.delete(nameNode.text);
1745
+ }
1473
1746
  }
1474
1747
  if (unionTypes.length > 1) localVarUnionTypes.set(nameNode.text, unionTypes);
1475
1748
  else localVarUnionTypes.delete(nameNode.text);
@@ -1525,12 +1798,14 @@ function findCallsInCode(code, parser) {
1525
1798
  };
1526
1799
  recordWithTargets(targetId || target);
1527
1800
  if (ctx?.type === 'call' && targetId?.type === 'identifier') {
1528
- const ctor = constructorTypeInfo(ctx.childForFieldName('function'));
1529
- if (ctor) {
1530
- localVarTypes.set(targetId.text, ctor.type);
1801
+ const constructorNode = ctx.childForFieldName('function');
1802
+ const exactCtor = exactConstructorInfo(constructorNode);
1803
+ if (exactCtor) {
1804
+ localVarTypes.set(targetId.text, exactCtor.type);
1531
1805
  constructedReceiverVars.add(targetId.text);
1532
- if (ctor.qualifier && moduleAliases.has(ctor.qualifier.split('.')[0])) {
1533
- localVarTypeQualifiers.set(targetId.text, ctor.qualifier);
1806
+ if (exactCtor.qualifier &&
1807
+ moduleAliases.has(exactCtor.qualifier.split('.')[0])) {
1808
+ localVarTypeQualifiers.set(targetId.text, exactCtor.qualifier);
1534
1809
  } else {
1535
1810
  localVarTypeQualifiers.delete(targetId.text);
1536
1811
  }
@@ -1599,8 +1874,15 @@ function findCallsInCode(code, parser) {
1599
1874
  if (declaredType) localVarTypes.set(left.text, declaredType);
1600
1875
  } else {
1601
1876
  // An annotation is the authoritative type source; a
1602
- // previous constructor qualifier must not survive it.
1603
- localVarTypeQualifiers.delete(left.text);
1877
+ // previous constructor qualifier must not survive it —
1878
+ // the annotation's OWN qualifier does (fix #286e).
1879
+ const annotationQualifier = typeNameFromAnnotation(typeNode)
1880
+ ? typeQualifierFromAnnotation(typeNode) : undefined;
1881
+ if (annotationQualifier) {
1882
+ localVarTypeQualifiers.set(left.text, annotationQualifier);
1883
+ } else {
1884
+ localVarTypeQualifiers.delete(left.text);
1885
+ }
1604
1886
  }
1605
1887
  // Preserve a declared collection contract through the common
1606
1888
  // normalization idiom `x = {} if x is None else x`. The
@@ -1723,12 +2005,14 @@ function findCallsInCode(code, parser) {
1723
2005
  nonCallableNames.add(left.text);
1724
2006
  // Infer type from constructor call: x = ClassName(...) or
1725
2007
  // x = pkg.ClassName(...). Python convention: classes start uppercase
1726
- const ctor = constructorTypeInfo(right.childForFieldName('function'));
1727
- if (ctor) {
1728
- localVarTypes.set(left.text, ctor.type);
2008
+ const constructorNode = right.childForFieldName('function');
2009
+ const exactCtor = exactConstructorInfo(constructorNode);
2010
+ if (exactCtor) {
2011
+ localVarTypes.set(left.text, exactCtor.type);
1729
2012
  constructedReceiverVars.add(left.text);
1730
- if (ctor.qualifier && moduleAliases.has(ctor.qualifier.split('.')[0])) {
1731
- localVarTypeQualifiers.set(left.text, ctor.qualifier);
2013
+ if (exactCtor.qualifier &&
2014
+ moduleAliases.has(exactCtor.qualifier.split('.')[0])) {
2015
+ localVarTypeQualifiers.set(left.text, exactCtor.qualifier);
1732
2016
  } else {
1733
2017
  localVarTypeQualifiers.delete(left.text);
1734
2018
  }
@@ -1753,7 +2037,26 @@ function findCallsInCode(code, parser) {
1753
2037
  const enclosingFunction = getCurrentEnclosingFunction();
1754
2038
  let uncertain = false;
1755
2039
  const assignedContext = contextTargetOf(node);
1756
- const assignedTo = assignmentTargetOf(node) || assignedContext;
2040
+ let assignedTo = assignmentTargetOf(node) || assignedContext;
2041
+ // For-loop iterable producer (fix #294): the loop variable's
2042
+ // provenance comes from this call. assignedIter marks that the
2043
+ // variable holds an ELEMENT, not the return value — the flow map
2044
+ // uses it for external-producer demotion only, never typing.
2045
+ let assignedIter = false;
2046
+ let assignedIterRest = null;
2047
+ if (!assignedTo) {
2048
+ const iterTarget = iterTargetOf(node);
2049
+ if (iterTarget) {
2050
+ assignedTo = iterTarget.first;
2051
+ assignedIterRest = iterTarget.rest;
2052
+ assignedIter = true;
2053
+ }
2054
+ }
2055
+ const assignedIterFields = {
2056
+ ...(assignedIter && { assignedIter: true }),
2057
+ ...(assignedIter && assignedIterRest && assignedIterRest.length > 0 &&
2058
+ { assignedTupleRest: assignedIterRest }),
2059
+ };
1757
2060
 
1758
2061
  // Call-site arg count (positional + keyword) for arity pruning.
1759
2062
  // *args/**kwargs splats make the count open-ended — flag them so
@@ -1793,6 +2096,7 @@ function findCallsInCode(code, parser) {
1793
2096
  ...(recvType && { receiverType: recvType }),
1794
2097
  ...(recvIsModule && { receiverIsModule: true }),
1795
2098
  ...(assignedTo && { assignedTo }),
2099
+ ...assignedIterFields,
1796
2100
  ...(assignedContext && { assignedContext: true }),
1797
2101
  argCount,
1798
2102
  ...(argSpread && { argSpread: true }),
@@ -1809,6 +2113,7 @@ function findCallsInCode(code, parser) {
1809
2113
  line: node.startPosition.row + 1,
1810
2114
  isMethod: false,
1811
2115
  ...(assignedTo && { assignedTo }),
2116
+ ...assignedIterFields,
1812
2117
  ...(assignedContext && { assignedContext: true }),
1813
2118
  argCount,
1814
2119
  ...(argSpread && { argSpread: true }),
@@ -1990,6 +2295,7 @@ function findCallsInCode(code, parser) {
1990
2295
  receiverCapabilityGuard: capabilityGuard,
1991
2296
  }),
1992
2297
  ...(assignedTo && { assignedTo }),
2298
+ ...assignedIterFields,
1993
2299
  ...(assignedContext && { assignedContext: true }),
1994
2300
  argCount,
1995
2301
  ...(argSpread && { argSpread: true }),
@@ -2024,6 +2330,36 @@ function findCallsInCode(code, parser) {
2024
2330
  enclosingFunction
2025
2331
  });
2026
2332
  }
2333
+ // Method-value references (fix #295, flask-check-measured):
2334
+ // `pytest.raises(NIE, t.check, None)` passes the bound
2335
+ // method t.check — an attribute argument on a one-hop
2336
+ // identifier receiver is a potential method value, typed
2337
+ // from the same localVarTypes evidence method calls use.
2338
+ // No isPotentialCallback: the record rides the MAIN path's
2339
+ // full receiver physics (the JS HOF member-value
2340
+ // convention), not the bare-name callback fast path.
2341
+ // self/cls receivers (same-class values) and module-alias
2342
+ // receivers (name-level ownership physics) are
2343
+ // classified-deferred families — not emitted.
2344
+ if (arg.type === 'attribute') {
2345
+ const mvObj = arg.childForFieldName('object');
2346
+ const mvAttr = arg.childForFieldName('attribute');
2347
+ if (mvObj?.type === 'identifier' && mvAttr &&
2348
+ !PYTHON_SKIP.has(mvObj.text) &&
2349
+ !moduleAliases.has(mvObj.text) &&
2350
+ !PYTHON_SKIP.has(mvAttr.text)) {
2351
+ const mvType = localVarTypes.get(mvObj.text);
2352
+ calls.push({
2353
+ name: mvAttr.text,
2354
+ line: mvAttr.startPosition.row + 1,
2355
+ isMethod: true,
2356
+ receiver: mvObj.text,
2357
+ ...(mvType && { receiverType: mvType }),
2358
+ isFunctionReference: true,
2359
+ enclosingFunction
2360
+ });
2361
+ }
2362
+ }
2027
2363
  // Scan dict literal args for function refs in values
2028
2364
  // e.g., do_request({'on_success': handle_success})
2029
2365
  if (arg.type === 'dictionary') {
@@ -2346,6 +2682,7 @@ function findExportsInCode(code, parser) {
2346
2682
  function findUsagesInCode(code, name, parser, tree) {
2347
2683
  tree = tree || parseTree(parser, code);
2348
2684
  const usages = [];
2685
+ const moduleAliases = pythonModuleAliases(tree);
2349
2686
 
2350
2687
  visitNameNodes(tree, code, name, (node) => {
2351
2688
  // Only look for identifiers with the matching name
@@ -2417,7 +2754,16 @@ function findUsagesInCode(code, name, parser, tree) {
2417
2754
  // Track receiver for member expressions (obj.name → receiver = 'obj')
2418
2755
  const object = parent.childForFieldName('object');
2419
2756
  if (object && object.type === 'identifier') {
2420
- usages.push({ line, column, usageType, receiver: object.text });
2757
+ usages.push({
2758
+ line,
2759
+ column,
2760
+ usageType,
2761
+ receiver: object.text,
2762
+ ...(moduleAliases.has(object.text) && { receiverIsModule: true }),
2763
+ ...(isPythonNameShadowedAt(object, object.text) && {
2764
+ receiverLocalBinding: true,
2765
+ }),
2766
+ });
2421
2767
  return true;
2422
2768
  }
2423
2769
  // Constructed receiver: ColorTriplet(...).normalized is a