ucn 5.1.1 → 5.2.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.
- package/.claude/skills/ucn/SKILL.md +26 -5
- package/.claude/skills/ucn/references/commands.md +5 -5
- package/README.md +42 -15
- package/core/accessors.js +183 -0
- package/core/analysis.js +46 -0
- package/core/ast-analysis.js +104 -0
- package/core/cache.js +32 -1
- package/core/callers.js +1255 -64
- package/core/command-contracts.js +13 -13
- package/core/deadcode.js +41 -2
- package/core/execute.js +4 -1
- package/core/graph-build.js +6 -0
- package/core/graph.js +72 -5
- package/core/index-ir.js +13 -2
- package/core/ir.js +5 -3
- package/core/output/analysis.js +30 -1
- package/core/output/graph.js +28 -8
- package/core/output/public.js +4 -0
- package/core/output/refactoring.js +31 -2
- package/core/output/reporting.js +7 -0
- package/core/project.js +32 -0
- package/core/search.js +9 -0
- package/core/verify.js +1168 -38
- package/languages/c-family.js +239 -26
- package/languages/csharp.js +40 -4
- package/languages/go.js +473 -71
- package/languages/javascript.js +86 -4
- package/languages/python.js +392 -101
- 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') {
|
|
@@ -2209,10 +2468,24 @@ function findImportsInCode(code, parser) {
|
|
|
2209
2468
|
const imports = [];
|
|
2210
2469
|
let importAliases = null; // {original, local}[] — tracks renamed imports
|
|
2211
2470
|
|
|
2471
|
+
// Imports nested in a function/lambda do not execute during ordinary
|
|
2472
|
+
// module initialization. Preserve that AST fact so dependency-cycle
|
|
2473
|
+
// reporting can distinguish an eager import loop from a deliberate lazy
|
|
2474
|
+
// edge without deleting either edge from the graph.
|
|
2475
|
+
const isDeferredImport = (node) => {
|
|
2476
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
2477
|
+
if (parent.type === 'function_definition' || parent.type === 'lambda') {
|
|
2478
|
+
return true;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
return false;
|
|
2482
|
+
};
|
|
2483
|
+
|
|
2212
2484
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2213
2485
|
// import statement: import os, import sys as system
|
|
2214
2486
|
if (node.type === 'import_statement') {
|
|
2215
2487
|
const line = node.startPosition.row + 1;
|
|
2488
|
+
const deferred = isDeferredImport(node);
|
|
2216
2489
|
|
|
2217
2490
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2218
2491
|
const child = node.namedChild(i);
|
|
@@ -2228,20 +2501,23 @@ function findImportsInCode(code, parser) {
|
|
|
2228
2501
|
module: parts[0],
|
|
2229
2502
|
names: [parts[0]],
|
|
2230
2503
|
type: 'import',
|
|
2231
|
-
line
|
|
2504
|
+
line,
|
|
2505
|
+
...(deferred && { deferred: true })
|
|
2232
2506
|
});
|
|
2233
2507
|
imports.push({
|
|
2234
2508
|
module: child.text,
|
|
2235
2509
|
names: [],
|
|
2236
2510
|
type: 'import-submodule',
|
|
2237
|
-
line
|
|
2511
|
+
line,
|
|
2512
|
+
...(deferred && { deferred: true })
|
|
2238
2513
|
});
|
|
2239
2514
|
} else {
|
|
2240
2515
|
imports.push({
|
|
2241
2516
|
module: child.text,
|
|
2242
2517
|
names: [child.text],
|
|
2243
2518
|
type: 'import',
|
|
2244
|
-
line
|
|
2519
|
+
line,
|
|
2520
|
+
...(deferred && { deferred: true })
|
|
2245
2521
|
});
|
|
2246
2522
|
}
|
|
2247
2523
|
} else if (child.type === 'aliased_import') {
|
|
@@ -2253,7 +2529,8 @@ function findImportsInCode(code, parser) {
|
|
|
2253
2529
|
module: nameNode.text,
|
|
2254
2530
|
names: [aliasNode ? aliasNode.text : nameNode.text.split('.').pop()],
|
|
2255
2531
|
type: 'import',
|
|
2256
|
-
line
|
|
2532
|
+
line,
|
|
2533
|
+
...(deferred && { deferred: true })
|
|
2257
2534
|
});
|
|
2258
2535
|
if (aliasNode && aliasNode.text !== nameNode.text) {
|
|
2259
2536
|
if (!importAliases) importAliases = [];
|
|
@@ -2268,6 +2545,7 @@ function findImportsInCode(code, parser) {
|
|
|
2268
2545
|
// from ... import statement
|
|
2269
2546
|
if (node.type === 'import_from_statement') {
|
|
2270
2547
|
const line = node.startPosition.row + 1;
|
|
2548
|
+
const deferred = isDeferredImport(node);
|
|
2271
2549
|
let modulePath = '';
|
|
2272
2550
|
const names = [];
|
|
2273
2551
|
|
|
@@ -2300,7 +2578,8 @@ function findImportsInCode(code, parser) {
|
|
|
2300
2578
|
module: modulePath,
|
|
2301
2579
|
names,
|
|
2302
2580
|
type: isRelative ? 'relative' : 'from',
|
|
2303
|
-
line
|
|
2581
|
+
line,
|
|
2582
|
+
...(deferred && { deferred: true })
|
|
2304
2583
|
});
|
|
2305
2584
|
}
|
|
2306
2585
|
return true;
|
|
@@ -2315,13 +2594,15 @@ function findImportsInCode(code, parser) {
|
|
|
2315
2594
|
const firstArg = argsNode.namedChild(0);
|
|
2316
2595
|
if ((funcName === 'importlib.import_module' || funcName === '__import__') && firstArg) {
|
|
2317
2596
|
const line = node.startPosition.row + 1;
|
|
2597
|
+
const deferred = isDeferredImport(node);
|
|
2318
2598
|
const isLiteral = firstArg.type === 'string';
|
|
2319
2599
|
imports.push({
|
|
2320
2600
|
module: isLiteral ? firstArg.text.replace(/^['"]|['"]$/g, '') : firstArg.text,
|
|
2321
2601
|
names: [],
|
|
2322
2602
|
type: 'dynamic',
|
|
2323
2603
|
line,
|
|
2324
|
-
dynamic: !isLiteral
|
|
2604
|
+
dynamic: !isLiteral,
|
|
2605
|
+
...(deferred && { deferred: true })
|
|
2325
2606
|
});
|
|
2326
2607
|
}
|
|
2327
2608
|
}
|
|
@@ -2423,6 +2704,7 @@ function findExportsInCode(code, parser) {
|
|
|
2423
2704
|
function findUsagesInCode(code, name, parser, tree) {
|
|
2424
2705
|
tree = tree || parseTree(parser, code);
|
|
2425
2706
|
const usages = [];
|
|
2707
|
+
const moduleAliases = pythonModuleAliases(tree);
|
|
2426
2708
|
|
|
2427
2709
|
visitNameNodes(tree, code, name, (node) => {
|
|
2428
2710
|
// Only look for identifiers with the matching name
|
|
@@ -2494,7 +2776,16 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
2494
2776
|
// Track receiver for member expressions (obj.name → receiver = 'obj')
|
|
2495
2777
|
const object = parent.childForFieldName('object');
|
|
2496
2778
|
if (object && object.type === 'identifier') {
|
|
2497
|
-
usages.push({
|
|
2779
|
+
usages.push({
|
|
2780
|
+
line,
|
|
2781
|
+
column,
|
|
2782
|
+
usageType,
|
|
2783
|
+
receiver: object.text,
|
|
2784
|
+
...(moduleAliases.has(object.text) && { receiverIsModule: true }),
|
|
2785
|
+
...(isPythonNameShadowedAt(object, object.text) && {
|
|
2786
|
+
receiverLocalBinding: true,
|
|
2787
|
+
}),
|
|
2788
|
+
});
|
|
2498
2789
|
return true;
|
|
2499
2790
|
}
|
|
2500
2791
|
// Constructed receiver: ColorTriplet(...).normalized is a
|