ucn 5.1.1 → 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.
- package/.claude/skills/ucn/SKILL.md +11 -4
- package/.claude/skills/ucn/references/commands.md +2 -2
- package/README.md +28 -13
- package/core/cache.js +28 -1
- package/core/callers.js +1242 -57
- package/core/graph-build.js +6 -0
- package/core/index-ir.js +3 -2
- package/core/ir.js +3 -2
- package/core/output/refactoring.js +1 -1
- package/core/project.js +32 -0
- package/core/search.js +9 -0
- package/core/verify.js +1084 -36
- package/languages/c-family.js +231 -9
- package/languages/csharp.js +14 -3
- package/languages/go.js +473 -71
- package/languages/javascript.js +85 -3
- package/languages/python.js +364 -95
- package/languages/rust.js +87 -4
- package/languages/utils.js +11 -0
- package/mcp/server.js +99 -103
- package/mcp/stdio-server.js +296 -0
- package/package.json +10 -8
package/languages/python.js
CHANGED
|
@@ -1076,6 +1076,35 @@ function assignmentTargetOf(callNode) {
|
|
|
1076
1076
|
return undefined;
|
|
1077
1077
|
}
|
|
1078
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
|
+
|
|
1079
1108
|
function contextTargetOf(callNode) {
|
|
1080
1109
|
const pattern = callNode?.parent;
|
|
1081
1110
|
if (pattern?.type !== 'as_pattern' ||
|
|
@@ -1231,6 +1260,247 @@ function pickleRoundTripSource(node) {
|
|
|
1231
1260
|
return value?.type === 'identifier' ? value.text : null;
|
|
1232
1261
|
}
|
|
1233
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
|
+
|
|
1234
1504
|
function findCallsInCode(code, parser) {
|
|
1235
1505
|
const tree = parseTree(parser, code);
|
|
1236
1506
|
const calls = [];
|
|
@@ -1373,89 +1643,22 @@ function findCallsInCode(code, parser) {
|
|
|
1373
1643
|
};
|
|
1374
1644
|
};
|
|
1375
1645
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
};
|
|
1393
|
-
const _bindsNameInScope = (scopeNode, name) => {
|
|
1394
|
-
for (let i = 0; i < scopeNode.namedChildCount; i++) {
|
|
1395
|
-
const c = scopeNode.namedChild(i);
|
|
1396
|
-
if (c.type === 'function_definition' || c.type === 'async_function_definition' ||
|
|
1397
|
-
c.type === 'class_definition') {
|
|
1398
|
-
// The body is a separate scope, but the DEF NAME itself is an
|
|
1399
|
-
// assignment in THIS scope (fix #218: a nested `def get_style`
|
|
1400
|
-
// shadows the name for sibling references).
|
|
1401
|
-
if (c.childForFieldName('name')?.text === name) return true;
|
|
1402
|
-
continue;
|
|
1403
|
-
}
|
|
1404
|
-
if (c.type === 'lambda') continue; // separate scope, no name
|
|
1405
|
-
if (c.type === 'assignment' || c.type === 'augmented_assignment' || c.type === 'named_expression') {
|
|
1406
|
-
if (_targetBindsName(c.childForFieldName('left') || c.childForFieldName('name'), name)) return true;
|
|
1407
|
-
} else if (c.type === 'for_statement') {
|
|
1408
|
-
if (_targetBindsName(c.childForFieldName('left'), name)) return true;
|
|
1409
|
-
} else if (c.type === 'with_statement') {
|
|
1410
|
-
// with open(f) as fh: — as-target is inside with_clause/with_item
|
|
1411
|
-
const text = c.namedChild(0)?.text || '';
|
|
1412
|
-
const m = text.match(/\bas\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
|
1413
|
-
if (m && m[1] === name) return true;
|
|
1414
|
-
}
|
|
1415
|
-
if (_bindsNameInScope(c, name)) return true;
|
|
1416
|
-
}
|
|
1417
|
-
return false;
|
|
1418
|
-
};
|
|
1419
|
-
const PY_COMPREHENSIONS = new Set([
|
|
1420
|
-
'generator_expression', 'list_comprehension', 'set_comprehension', 'dictionary_comprehension',
|
|
1421
|
-
]);
|
|
1422
|
-
const isShadowedByLocal = (refNode, name) => {
|
|
1423
|
-
for (let p = refNode.parent; p; p = p.parent) {
|
|
1424
|
-
// Comprehension for-clause targets are scoped to the comprehension
|
|
1425
|
-
// itself (PEP 3110-era scoping): `cell_len(line) for line in lines`
|
|
1426
|
-
// binds `line` ONLY inside the comprehension — block-accurate, so
|
|
1427
|
-
// check on the way up rather than function-wide (fix #218).
|
|
1428
|
-
if (PY_COMPREHENSIONS.has(p.type)) {
|
|
1429
|
-
for (let i = 0; i < p.namedChildCount; i++) {
|
|
1430
|
-
const c = p.namedChild(i);
|
|
1431
|
-
if (c.type === 'for_in_clause' && _targetBindsName(c.childForFieldName('left'), name)) return true;
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
// Lambda params shadow their body the same way (fix #218).
|
|
1435
|
-
if (p.type === 'lambda') {
|
|
1436
|
-
const params = p.childForFieldName('parameters');
|
|
1437
|
-
if (params) for (let i = 0; i < params.namedChildCount; i++) {
|
|
1438
|
-
const c = params.namedChild(i);
|
|
1439
|
-
if (c.type === 'identifier' && c.text === name) return true;
|
|
1440
|
-
if (c.type === 'default_parameter' && c.childForFieldName('name')?.text === name) return true;
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
if (p.type === 'function_definition' || p.type === 'async_function_definition') {
|
|
1444
|
-
const params = p.childForFieldName('parameters');
|
|
1445
|
-
if (params) {
|
|
1446
|
-
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1447
|
-
const prm = params.namedChild(i);
|
|
1448
|
-
const prmName = prm.type === 'identifier'
|
|
1449
|
-
? prm
|
|
1450
|
-
: (prm.childForFieldName('name') || prm.namedChild(0));
|
|
1451
|
-
if (prmName?.type === 'identifier' && prmName.text === name) return true;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
const body = p.childForFieldName('body');
|
|
1455
|
-
return body ? _bindsNameInScope(body, name) : false;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
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;
|
|
1459
1662
|
};
|
|
1460
1663
|
|
|
1461
1664
|
traverseTree(tree.rootNode, (node) => {
|
|
@@ -1595,12 +1798,14 @@ function findCallsInCode(code, parser) {
|
|
|
1595
1798
|
};
|
|
1596
1799
|
recordWithTargets(targetId || target);
|
|
1597
1800
|
if (ctx?.type === 'call' && targetId?.type === 'identifier') {
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1801
|
+
const constructorNode = ctx.childForFieldName('function');
|
|
1802
|
+
const exactCtor = exactConstructorInfo(constructorNode);
|
|
1803
|
+
if (exactCtor) {
|
|
1804
|
+
localVarTypes.set(targetId.text, exactCtor.type);
|
|
1601
1805
|
constructedReceiverVars.add(targetId.text);
|
|
1602
|
-
if (
|
|
1603
|
-
|
|
1806
|
+
if (exactCtor.qualifier &&
|
|
1807
|
+
moduleAliases.has(exactCtor.qualifier.split('.')[0])) {
|
|
1808
|
+
localVarTypeQualifiers.set(targetId.text, exactCtor.qualifier);
|
|
1604
1809
|
} else {
|
|
1605
1810
|
localVarTypeQualifiers.delete(targetId.text);
|
|
1606
1811
|
}
|
|
@@ -1800,12 +2005,14 @@ function findCallsInCode(code, parser) {
|
|
|
1800
2005
|
nonCallableNames.add(left.text);
|
|
1801
2006
|
// Infer type from constructor call: x = ClassName(...) or
|
|
1802
2007
|
// x = pkg.ClassName(...). Python convention: classes start uppercase
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
2008
|
+
const constructorNode = right.childForFieldName('function');
|
|
2009
|
+
const exactCtor = exactConstructorInfo(constructorNode);
|
|
2010
|
+
if (exactCtor) {
|
|
2011
|
+
localVarTypes.set(left.text, exactCtor.type);
|
|
1806
2012
|
constructedReceiverVars.add(left.text);
|
|
1807
|
-
if (
|
|
1808
|
-
|
|
2013
|
+
if (exactCtor.qualifier &&
|
|
2014
|
+
moduleAliases.has(exactCtor.qualifier.split('.')[0])) {
|
|
2015
|
+
localVarTypeQualifiers.set(left.text, exactCtor.qualifier);
|
|
1809
2016
|
} else {
|
|
1810
2017
|
localVarTypeQualifiers.delete(left.text);
|
|
1811
2018
|
}
|
|
@@ -1830,7 +2037,26 @@ function findCallsInCode(code, parser) {
|
|
|
1830
2037
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1831
2038
|
let uncertain = false;
|
|
1832
2039
|
const assignedContext = contextTargetOf(node);
|
|
1833
|
-
|
|
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
|
+
};
|
|
1834
2060
|
|
|
1835
2061
|
// Call-site arg count (positional + keyword) for arity pruning.
|
|
1836
2062
|
// *args/**kwargs splats make the count open-ended — flag them so
|
|
@@ -1870,6 +2096,7 @@ function findCallsInCode(code, parser) {
|
|
|
1870
2096
|
...(recvType && { receiverType: recvType }),
|
|
1871
2097
|
...(recvIsModule && { receiverIsModule: true }),
|
|
1872
2098
|
...(assignedTo && { assignedTo }),
|
|
2099
|
+
...assignedIterFields,
|
|
1873
2100
|
...(assignedContext && { assignedContext: true }),
|
|
1874
2101
|
argCount,
|
|
1875
2102
|
...(argSpread && { argSpread: true }),
|
|
@@ -1886,6 +2113,7 @@ function findCallsInCode(code, parser) {
|
|
|
1886
2113
|
line: node.startPosition.row + 1,
|
|
1887
2114
|
isMethod: false,
|
|
1888
2115
|
...(assignedTo && { assignedTo }),
|
|
2116
|
+
...assignedIterFields,
|
|
1889
2117
|
...(assignedContext && { assignedContext: true }),
|
|
1890
2118
|
argCount,
|
|
1891
2119
|
...(argSpread && { argSpread: true }),
|
|
@@ -2067,6 +2295,7 @@ function findCallsInCode(code, parser) {
|
|
|
2067
2295
|
receiverCapabilityGuard: capabilityGuard,
|
|
2068
2296
|
}),
|
|
2069
2297
|
...(assignedTo && { assignedTo }),
|
|
2298
|
+
...assignedIterFields,
|
|
2070
2299
|
...(assignedContext && { assignedContext: true }),
|
|
2071
2300
|
argCount,
|
|
2072
2301
|
...(argSpread && { argSpread: true }),
|
|
@@ -2101,6 +2330,36 @@ function findCallsInCode(code, parser) {
|
|
|
2101
2330
|
enclosingFunction
|
|
2102
2331
|
});
|
|
2103
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
|
+
}
|
|
2104
2363
|
// Scan dict literal args for function refs in values
|
|
2105
2364
|
// e.g., do_request({'on_success': handle_success})
|
|
2106
2365
|
if (arg.type === 'dictionary') {
|
|
@@ -2423,6 +2682,7 @@ function findExportsInCode(code, parser) {
|
|
|
2423
2682
|
function findUsagesInCode(code, name, parser, tree) {
|
|
2424
2683
|
tree = tree || parseTree(parser, code);
|
|
2425
2684
|
const usages = [];
|
|
2685
|
+
const moduleAliases = pythonModuleAliases(tree);
|
|
2426
2686
|
|
|
2427
2687
|
visitNameNodes(tree, code, name, (node) => {
|
|
2428
2688
|
// Only look for identifiers with the matching name
|
|
@@ -2494,7 +2754,16 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
2494
2754
|
// Track receiver for member expressions (obj.name → receiver = 'obj')
|
|
2495
2755
|
const object = parent.childForFieldName('object');
|
|
2496
2756
|
if (object && object.type === 'identifier') {
|
|
2497
|
-
usages.push({
|
|
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
|
+
});
|
|
2498
2767
|
return true;
|
|
2499
2768
|
}
|
|
2500
2769
|
// Constructed receiver: ColorTriplet(...).normalized is a
|