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.
- package/.claude/skills/ucn/SKILL.md +12 -5
- package/.claude/skills/ucn/references/commands.md +2 -2
- package/.claude/skills/ucn/references/trust-contract.md +3 -2
- package/README.md +31 -14
- package/core/analysis.js +66 -1
- package/core/bridge.js +2 -1
- package/core/cache.js +64 -8
- package/core/callers.js +1722 -90
- package/core/graph-build.js +6 -0
- package/core/index-ir.js +16 -5
- package/core/ir.js +50 -3
- package/core/output/analysis.js +25 -0
- package/core/output/refactoring.js +1 -1
- package/core/output/shared.js +2 -2
- 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 +288 -21
- package/languages/python.js +443 -97
- package/languages/rust.js +87 -4
- package/languages/utils.js +11 -0
- package/mcp/server.js +100 -104
- package/mcp/stdio-server.js +296 -0
- package/package.json +10 -8
package/languages/python.js
CHANGED
|
@@ -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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
|
1529
|
-
|
|
1530
|
-
|
|
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 (
|
|
1533
|
-
|
|
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
|
-
|
|
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
|
|
1727
|
-
|
|
1728
|
-
|
|
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 (
|
|
1731
|
-
|
|
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
|
-
|
|
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({
|
|
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
|