ucn 5.2.0 → 5.2.2
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 +17 -3
- package/.claude/skills/ucn/references/commands.md +4 -4
- package/README.md +17 -5
- package/cli/index.js +3 -0
- package/core/accessors.js +183 -0
- package/core/account.js +36 -8
- package/core/analysis.js +46 -0
- package/core/ast-analysis.js +104 -0
- package/core/cache.js +150 -9
- package/core/callers.js +1196 -122
- package/core/command-contracts.js +13 -13
- package/core/deadcode.js +41 -2
- package/core/execute.js +4 -1
- package/core/graph.js +72 -5
- package/core/index-ir.js +15 -3
- package/core/ir.js +58 -9
- 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 +88 -3
- package/core/verify.js +85 -3
- package/languages/c-family.js +27 -34
- package/languages/csharp.js +26 -1
- package/languages/go.js +170 -42
- package/languages/javascript.js +258 -10
- package/languages/python.js +591 -32
- package/languages/rust.js +1 -0
- package/package.json +1 -1
package/languages/python.js
CHANGED
|
@@ -408,6 +408,24 @@ function pythonAliasBase(node) {
|
|
|
408
408
|
return base ? (PY_ALIAS_RUNTIME_TYPES.get(base) || base) : null;
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
+
function pythonAliasMembers(node) {
|
|
412
|
+
const current = unwrapTypeNode(node);
|
|
413
|
+
if (!current) return [];
|
|
414
|
+
if (current.type === 'binary_operator' && current.text.includes('|')) {
|
|
415
|
+
return [...new Set([
|
|
416
|
+
...pythonAliasMembers(current.namedChild(0)),
|
|
417
|
+
...pythonAliasMembers(current.namedChild(1)),
|
|
418
|
+
])];
|
|
419
|
+
}
|
|
420
|
+
if (current.type === 'subscript' || current.type === 'generic_type') {
|
|
421
|
+
const parts = genericTypeParts(current);
|
|
422
|
+
if (!parts || !['Union', 'Optional'].includes(parts.base)) return [];
|
|
423
|
+
return [...new Set(parts.args.flatMap(pythonAliasMembers))];
|
|
424
|
+
}
|
|
425
|
+
const name = typeNameFromExpr(current);
|
|
426
|
+
return name && name !== 'None' ? [name] : [];
|
|
427
|
+
}
|
|
428
|
+
|
|
411
429
|
/**
|
|
412
430
|
* Python type aliases: PEP 695 `type X = int`, annotated
|
|
413
431
|
* `X: TypeAlias = ...`, and module-scope static aliases such as
|
|
@@ -448,6 +466,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
|
448
466
|
processedRanges.add(rangeKey);
|
|
449
467
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
450
468
|
const aliasOf = pythonAliasBase(valueNode);
|
|
469
|
+
const aliasMembers = pythonAliasMembers(valueNode);
|
|
451
470
|
classes.push({
|
|
452
471
|
name,
|
|
453
472
|
type: 'type',
|
|
@@ -456,6 +475,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
|
456
475
|
methods: [],
|
|
457
476
|
members: [],
|
|
458
477
|
...(aliasOf && { aliasOf }),
|
|
478
|
+
...(aliasMembers.length > 1 && { aliasMembers }),
|
|
459
479
|
});
|
|
460
480
|
return true;
|
|
461
481
|
}
|
|
@@ -969,8 +989,30 @@ function isinstanceTypes(condition, receiverName) {
|
|
|
969
989
|
function narrowedReceiverType(refNode, receiverName, declaredUnion) {
|
|
970
990
|
for (let current = refNode; current?.parent; current = current.parent) {
|
|
971
991
|
const parent = current.parent;
|
|
972
|
-
if (parent.type === 'if_statement') {
|
|
992
|
+
if (parent.type === 'if_statement' || parent.type === 'elif_clause') {
|
|
973
993
|
const condition = parent.childForFieldName('condition');
|
|
994
|
+
const noneIdentity = condition?.type === 'comparison_operator' &&
|
|
995
|
+
condition.namedChildCount === 2 &&
|
|
996
|
+
condition.children.find(child =>
|
|
997
|
+
child.type === 'is' || child.type === 'is not');
|
|
998
|
+
const noneIndex = noneIdentity
|
|
999
|
+
? [condition.namedChild(0), condition.namedChild(1)]
|
|
1000
|
+
.findIndex(child => child?.type === 'none')
|
|
1001
|
+
: -1;
|
|
1002
|
+
const compared = noneIndex >= 0
|
|
1003
|
+
? condition.namedChild(1 - noneIndex) : null;
|
|
1004
|
+
if (compared?.type === 'identifier' &&
|
|
1005
|
+
compared.text === receiverName && declaredUnion?.length) {
|
|
1006
|
+
const remaining = declaredUnion.filter(type => type !== 'None');
|
|
1007
|
+
const consequence = parent.childForFieldName('consequence');
|
|
1008
|
+
const alternative = parent.childForFieldName('alternative');
|
|
1009
|
+
const inNonNullBranch = noneIdentity.type === 'is not'
|
|
1010
|
+
? nodeContains(consequence, refNode)
|
|
1011
|
+
: nodeContains(alternative, refNode);
|
|
1012
|
+
if (inNonNullBranch && remaining.length === 1) {
|
|
1013
|
+
return remaining[0];
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
974
1016
|
const positive = isinstanceTypes(condition, receiverName);
|
|
975
1017
|
if (positive.length === 0) continue;
|
|
976
1018
|
const consequence = parent.childForFieldName('consequence');
|
|
@@ -1122,18 +1164,21 @@ function contextTargetOf(callNode) {
|
|
|
1122
1164
|
* pkg.ClassName(...). Preserve the qualifier: dropping `threading` from
|
|
1123
1165
|
* `threading.Thread()` turns an external class into an unqualified project
|
|
1124
1166
|
* type name and can falsely confirm `thread.join()` against Project.join.
|
|
1125
|
-
*
|
|
1126
|
-
* qualifier as routing/provenance evidence
|
|
1167
|
+
* PascalCase after optional privacy underscores remains a Python class-naming
|
|
1168
|
+
* heuristic, so callers use the qualifier as routing/provenance evidence
|
|
1169
|
+
* rather than a hidden hard claim. Private classes such as `_ClassBuilder`
|
|
1170
|
+
* are ordinary constructor values and must retain their type identity.
|
|
1127
1171
|
*/
|
|
1128
1172
|
function constructorTypeInfo(funcNode) {
|
|
1129
1173
|
if (!funcNode) return undefined;
|
|
1130
1174
|
if (funcNode.type === 'identifier') {
|
|
1131
|
-
return /^[A-Z]/.test(funcNode.text)
|
|
1175
|
+
return /^_*[A-Z]/.test(funcNode.text)
|
|
1176
|
+
? { type: funcNode.text } : undefined;
|
|
1132
1177
|
}
|
|
1133
1178
|
if (funcNode.type === 'attribute') {
|
|
1134
1179
|
const attr = funcNode.childForFieldName('attribute');
|
|
1135
1180
|
const object = funcNode.childForFieldName('object');
|
|
1136
|
-
return attr && /^[A-Z]/.test(attr.text)
|
|
1181
|
+
return attr && /^_*[A-Z]/.test(attr.text)
|
|
1137
1182
|
? { type: attr.text, qualifier: object?.text || undefined }
|
|
1138
1183
|
: undefined;
|
|
1139
1184
|
}
|
|
@@ -1307,11 +1352,73 @@ const PY_COMPREHENSIONS = new Set([
|
|
|
1307
1352
|
'dictionary_comprehension',
|
|
1308
1353
|
]);
|
|
1309
1354
|
|
|
1355
|
+
// Whether this reference is evaluated with `name` bound by an enclosing
|
|
1356
|
+
// for/comprehension target. Keep the check lexical: a nested function has its
|
|
1357
|
+
// own scope, and the iterable expression of `for x in source(x)` sees the
|
|
1358
|
+
// outer x rather than the new loop binding.
|
|
1359
|
+
function isPythonIterationBindingAt(refNode, name) {
|
|
1360
|
+
for (let parent = refNode?.parent; parent; parent = parent.parent) {
|
|
1361
|
+
if (parent.type === 'for_statement' &&
|
|
1362
|
+
pythonTargetBindsName(parent.childForFieldName('left'), name)) {
|
|
1363
|
+
const body = parent.childForFieldName('body');
|
|
1364
|
+
if (nodeContains(body, refNode)) return true;
|
|
1365
|
+
}
|
|
1366
|
+
if (PY_COMPREHENSIONS.has(parent.type)) {
|
|
1367
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
1368
|
+
const clause = parent.namedChild(i);
|
|
1369
|
+
if (clause.type !== 'for_in_clause' ||
|
|
1370
|
+
!pythonTargetBindsName(
|
|
1371
|
+
clause.childForFieldName('left'), name)) continue;
|
|
1372
|
+
const iterable = clause.childForFieldName('right');
|
|
1373
|
+
if (!nodeContains(iterable, refNode)) return true;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (parent.type === 'function_definition' ||
|
|
1377
|
+
parent.type === 'async_function_definition' ||
|
|
1378
|
+
parent.type === 'lambda') break;
|
|
1379
|
+
}
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Stronger subset used for dispatch tiering: an element drawn from an
|
|
1384
|
+
// identifier/attribute iterable has unknown provenance. Direct call
|
|
1385
|
+
// producers are handled separately by assignedIter flow: external calls
|
|
1386
|
+
// demote there, while a project-internal producer intentionally keeps the
|
|
1387
|
+
// measured single-owner rule (#294's counter-probe).
|
|
1388
|
+
function isPythonUnprovenIterationBindingAt(refNode, name) {
|
|
1389
|
+
for (let parent = refNode?.parent; parent; parent = parent.parent) {
|
|
1390
|
+
if (parent.type === 'for_statement' &&
|
|
1391
|
+
pythonTargetBindsName(parent.childForFieldName('left'), name)) {
|
|
1392
|
+
const body = parent.childForFieldName('body');
|
|
1393
|
+
if (nodeContains(body, refNode)) {
|
|
1394
|
+
return unwrapTypeNode(parent.childForFieldName('right'))?.type !== 'call';
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (PY_COMPREHENSIONS.has(parent.type)) {
|
|
1398
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
1399
|
+
const clause = parent.namedChild(i);
|
|
1400
|
+
if (clause.type !== 'for_in_clause' ||
|
|
1401
|
+
!pythonTargetBindsName(
|
|
1402
|
+
clause.childForFieldName('left'), name)) continue;
|
|
1403
|
+
const iterable = clause.childForFieldName('right');
|
|
1404
|
+
if (!nodeContains(iterable, refNode)) {
|
|
1405
|
+
return unwrapTypeNode(iterable)?.type !== 'call';
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (parent.type === 'function_definition' ||
|
|
1410
|
+
parent.type === 'async_function_definition' ||
|
|
1411
|
+
parent.type === 'lambda') break;
|
|
1412
|
+
}
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1310
1416
|
// Python locals are function-scoped: an assignment anywhere in the function
|
|
1311
1417
|
// shadows an imported module for every reference in that function. Keep this
|
|
1312
1418
|
// shared between call records and reference records so plan cannot promote a
|
|
1313
1419
|
// receiver that callers would reject as locally rebound.
|
|
1314
1420
|
function isPythonNameShadowedAt(refNode, name) {
|
|
1421
|
+
if (isPythonIterationBindingAt(refNode, name)) return true;
|
|
1315
1422
|
for (let parent = refNode.parent; parent; parent = parent.parent) {
|
|
1316
1423
|
if (PY_COMPREHENSIONS.has(parent.type)) {
|
|
1317
1424
|
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
@@ -1526,14 +1633,20 @@ function findCallsInCode(code, parser) {
|
|
|
1526
1633
|
});
|
|
1527
1634
|
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
1528
1635
|
const aliases = new Map(); // Track local aliases: aliasName -> originalName
|
|
1636
|
+
const aliasesStack = [];
|
|
1529
1637
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
1638
|
+
const nonCallableNamesStack = [];
|
|
1530
1639
|
const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
1531
1640
|
const declaredVarTypes = new Map(); // Compiler-checked annotations survive later assignments
|
|
1532
1641
|
const localVarTypeQualifiers = new Map(); // varName -> imported module alias that owns the inferred type
|
|
1533
|
-
|
|
1642
|
+
// varName -> concrete union alternatives. `None` is retained only for a
|
|
1643
|
+
// complete constructor/nullable branch join so a later identity guard can
|
|
1644
|
+
// narrow it without typing unguarded calls.
|
|
1645
|
+
const localVarUnionTypes = new Map();
|
|
1534
1646
|
const localIterableTypes = new Map(); // iterable binding -> loop-variable types
|
|
1535
1647
|
const localIterationSources = new Map(); // loop variable -> declared iterable path + tuple index
|
|
1536
1648
|
const localDictValueTypes = new Map(); // local dict -> exact string-key value types
|
|
1649
|
+
const localSubscriptSources = new Map(); // local value -> exact typed container selected by []
|
|
1537
1650
|
const localVarStdlibContracts = new Map(); // variable -> stdlib module proving its type flow
|
|
1538
1651
|
const assignmentRhsReceiverTypes = new Map(); // call-node id -> pre-assignment receiver type
|
|
1539
1652
|
const constructedReceiverVars = new Set(); // exact constructor-result bindings
|
|
@@ -1547,7 +1660,14 @@ function findCallsInCode(code, parser) {
|
|
|
1547
1660
|
// rewritten call is then receiver-blind and routes through dispatch tiering.
|
|
1548
1661
|
const memberAliases = new Map();
|
|
1549
1662
|
const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
|
|
1663
|
+
// Exact class-value aliases: `_Segment = Segment` makes later
|
|
1664
|
+
// `_Segment.line()` dispatch through the imported/local class object.
|
|
1665
|
+
// Keep this separate from callable aliases: receiver evidence must be
|
|
1666
|
+
// scope-local, position-aware, and invalidated by every later write.
|
|
1667
|
+
const classValueAliases = new Map();
|
|
1668
|
+
const classValueAliasesStack = [];
|
|
1550
1669
|
const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
|
|
1670
|
+
const moduleImportSpecifiers = new Set(); // Exact dotted imports: import rich.repr
|
|
1551
1671
|
const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
|
|
1552
1672
|
const declaredVarTypesStack = [];
|
|
1553
1673
|
const localVarTypeQualifiersStack = [];
|
|
@@ -1555,6 +1675,7 @@ function findCallsInCode(code, parser) {
|
|
|
1555
1675
|
const localIterableTypesStack = [];
|
|
1556
1676
|
const localIterationSourcesStack = [];
|
|
1557
1677
|
const localDictValueTypesStack = [];
|
|
1678
|
+
const localSubscriptSourcesStack = [];
|
|
1558
1679
|
const localVarStdlibContractsStack = [];
|
|
1559
1680
|
const constructedReceiverVarsStack = [];
|
|
1560
1681
|
const withBindingVarsStack = [];
|
|
@@ -1644,6 +1765,33 @@ function findCallsInCode(code, parser) {
|
|
|
1644
1765
|
};
|
|
1645
1766
|
|
|
1646
1767
|
const isShadowedByLocal = isPythonNameShadowedAt;
|
|
1768
|
+
const isDirectScopeAssignment = assignment => {
|
|
1769
|
+
const statement = assignment?.parent?.type === 'expression_statement'
|
|
1770
|
+
? assignment.parent : assignment;
|
|
1771
|
+
const parent = statement?.parent;
|
|
1772
|
+
if (parent?.type === 'module') return true;
|
|
1773
|
+
if (parent?.type !== 'block') return false;
|
|
1774
|
+
return ['function_definition', 'async_function_definition']
|
|
1775
|
+
.includes(parent.parent?.type);
|
|
1776
|
+
};
|
|
1777
|
+
const functionParameterBindsName = (functionNode, name) => {
|
|
1778
|
+
const params = functionNode?.childForFieldName('parameters');
|
|
1779
|
+
if (!params) return false;
|
|
1780
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1781
|
+
const param = params.namedChild(i);
|
|
1782
|
+
let paramName = param.type === 'identifier'
|
|
1783
|
+
? param
|
|
1784
|
+
: (param.childForFieldName('name') || param.namedChild(0));
|
|
1785
|
+
if (paramName && ['dictionary_splat_pattern', 'list_splat_pattern']
|
|
1786
|
+
.includes(paramName.type)) {
|
|
1787
|
+
paramName = paramName.namedChild(0);
|
|
1788
|
+
}
|
|
1789
|
+
if (paramName?.type === 'identifier' && paramName.text === name) {
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
return false;
|
|
1794
|
+
};
|
|
1647
1795
|
const exactConstructorInfo = funcNode => {
|
|
1648
1796
|
const ctor = constructorTypeInfo(funcNode);
|
|
1649
1797
|
if (!ctor) return undefined;
|
|
@@ -1660,6 +1808,194 @@ function findCallsInCode(code, parser) {
|
|
|
1660
1808
|
return moduleAliases.has(root) && !isShadowedByLocal(funcNode, root)
|
|
1661
1809
|
? ctor : undefined;
|
|
1662
1810
|
};
|
|
1811
|
+
// Compiler-stable two-branch join (#330):
|
|
1812
|
+
// if value is not None and not isinstance(value, T): out = T(value)
|
|
1813
|
+
// else: out = value
|
|
1814
|
+
// The result is T | None. Require one direct assignment per branch and
|
|
1815
|
+
// exactly the two proving conjuncts; extra control flow must abstain.
|
|
1816
|
+
const nullableBranchJoinType = (assignment, targetName, source) => {
|
|
1817
|
+
if (!assignment || !source ||
|
|
1818
|
+
!['identifier', 'attribute'].includes(source.type)) return null;
|
|
1819
|
+
const block = assignment.parent?.parent;
|
|
1820
|
+
const elseClause = block?.parent;
|
|
1821
|
+
const statement = elseClause?.parent;
|
|
1822
|
+
if (block?.type !== 'block' || block.namedChildCount !== 1 ||
|
|
1823
|
+
elseClause?.type !== 'else_clause' ||
|
|
1824
|
+
statement?.type !== 'if_statement') return null;
|
|
1825
|
+
|
|
1826
|
+
const directAssignment = branch => {
|
|
1827
|
+
if (branch?.type !== 'block' || branch.namedChildCount !== 1) {
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
const expression = branch.namedChild(0);
|
|
1831
|
+
const candidate = expression?.type === 'expression_statement'
|
|
1832
|
+
? expression.namedChild(0) : null;
|
|
1833
|
+
return candidate?.type === 'assignment' &&
|
|
1834
|
+
candidate.childForFieldName('left')?.type === 'identifier' &&
|
|
1835
|
+
candidate.childForFieldName('left').text === targetName
|
|
1836
|
+
? candidate.childForFieldName('right') : null;
|
|
1837
|
+
};
|
|
1838
|
+
const alternative = elseClause.childForFieldName('body') ||
|
|
1839
|
+
elseClause.namedChild(0);
|
|
1840
|
+
const alternativeRhs = directAssignment(alternative);
|
|
1841
|
+
if (alternativeRhs?.id !== source.id) return null;
|
|
1842
|
+
|
|
1843
|
+
const condition = statement.childForFieldName('condition');
|
|
1844
|
+
const andTerms = node => {
|
|
1845
|
+
const isAnd = node?.type === 'boolean_operator' &&
|
|
1846
|
+
node.children.some(child => child.type === 'and');
|
|
1847
|
+
return isAnd
|
|
1848
|
+
? node.namedChildren.flatMap(andTerms)
|
|
1849
|
+
: (node ? [node] : []);
|
|
1850
|
+
};
|
|
1851
|
+
const terms = andTerms(condition);
|
|
1852
|
+
if (terms.length !== 2) return null;
|
|
1853
|
+
const sameSource = node => node?.type === source.type &&
|
|
1854
|
+
node.text === source.text;
|
|
1855
|
+
const nonNull = terms.find(term => {
|
|
1856
|
+
if (term.type !== 'comparison_operator' ||
|
|
1857
|
+
!term.children.some(child => child.type === 'is not') ||
|
|
1858
|
+
term.namedChildCount !== 2) return false;
|
|
1859
|
+
const parts = [term.namedChild(0), term.namedChild(1)];
|
|
1860
|
+
return parts.some(part => part?.type === 'none') &&
|
|
1861
|
+
parts.some(sameSource);
|
|
1862
|
+
});
|
|
1863
|
+
const negativeIsinstance = terms.find(term =>
|
|
1864
|
+
term.type === 'not_operator' &&
|
|
1865
|
+
term.namedChild(0)?.type === 'call');
|
|
1866
|
+
if (!nonNull || !negativeIsinstance) return null;
|
|
1867
|
+
const check = negativeIsinstance.namedChild(0);
|
|
1868
|
+
if (check.childForFieldName('function')?.type !== 'identifier' ||
|
|
1869
|
+
check.childForFieldName('function').text !== 'isinstance') return null;
|
|
1870
|
+
const args = check.childForFieldName('arguments');
|
|
1871
|
+
if (!args || args.namedChildCount !== 2 ||
|
|
1872
|
+
!sameSource(args.namedChild(0))) return null;
|
|
1873
|
+
const narrowedType = typeNameFromExpr(args.namedChild(1));
|
|
1874
|
+
if (!narrowedType) return null;
|
|
1875
|
+
|
|
1876
|
+
const consequence = statement.childForFieldName('consequence');
|
|
1877
|
+
const consequenceRhs = directAssignment(consequence);
|
|
1878
|
+
if (consequenceRhs?.type !== 'call') return null;
|
|
1879
|
+
const constructor = exactConstructorInfo(
|
|
1880
|
+
consequenceRhs.childForFieldName('function'));
|
|
1881
|
+
return constructor?.type === narrowedType
|
|
1882
|
+
? [narrowedType, 'None'] : null;
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
// A function may legally reference a module global declared later in the
|
|
1886
|
+
// file. The source-order walk cannot see that binding yet, so collect the
|
|
1887
|
+
// narrow compiler-stable subset up front: one direct module assignment to
|
|
1888
|
+
// a bare constructor, with no other module-scope write to the name. Keep
|
|
1889
|
+
// qualified constructors out of this forward pass because their module
|
|
1890
|
+
// import must itself be proven at the assignment position.
|
|
1891
|
+
const moduleBindingCounts = new Map();
|
|
1892
|
+
const forwardModuleConstructorCandidates = new Map();
|
|
1893
|
+
const moduleTargetNames = target => {
|
|
1894
|
+
if (!target) return [];
|
|
1895
|
+
if (target.type === 'identifier') return [target.text];
|
|
1896
|
+
if (!['tuple', 'pattern_list', 'list', 'list_pattern',
|
|
1897
|
+
'tuple_pattern', 'list_splat_pattern',
|
|
1898
|
+
'dictionary_splat_pattern'].includes(target.type)) return [];
|
|
1899
|
+
const names = [];
|
|
1900
|
+
for (let i = 0; i < target.namedChildCount; i++) {
|
|
1901
|
+
names.push(...moduleTargetNames(target.namedChild(i)));
|
|
1902
|
+
}
|
|
1903
|
+
return names;
|
|
1904
|
+
};
|
|
1905
|
+
const isModuleScopeNode = node => {
|
|
1906
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
1907
|
+
if (parent.type === 'function_definition' ||
|
|
1908
|
+
parent.type === 'class_definition' ||
|
|
1909
|
+
parent.type === 'lambda') return false;
|
|
1910
|
+
}
|
|
1911
|
+
return true;
|
|
1912
|
+
};
|
|
1913
|
+
const moduleBindingNodeTypes = new Set([
|
|
1914
|
+
'assignment', 'augmented_assignment', 'named_expression',
|
|
1915
|
+
'for_statement', 'function_definition', 'async_function_definition',
|
|
1916
|
+
'class_definition', 'import_statement', 'import_from_statement',
|
|
1917
|
+
'global_statement', 'delete_statement', 'as_pattern',
|
|
1918
|
+
]);
|
|
1919
|
+
let hasModuleWildcardImport = false;
|
|
1920
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
1921
|
+
if (!moduleBindingNodeTypes.has(node.type)) return true;
|
|
1922
|
+
if (node.type === 'global_statement') {
|
|
1923
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1924
|
+
const child = node.namedChild(i);
|
|
1925
|
+
if (child.type !== 'identifier') continue;
|
|
1926
|
+
moduleBindingCounts.set(child.text,
|
|
1927
|
+
(moduleBindingCounts.get(child.text) || 0) + 1);
|
|
1928
|
+
}
|
|
1929
|
+
return true;
|
|
1930
|
+
}
|
|
1931
|
+
if (!isModuleScopeNode(node)) return true;
|
|
1932
|
+
let names = [];
|
|
1933
|
+
if (node.type === 'assignment' ||
|
|
1934
|
+
node.type === 'augmented_assignment' ||
|
|
1935
|
+
node.type === 'named_expression') {
|
|
1936
|
+
names = moduleTargetNames(
|
|
1937
|
+
node.childForFieldName('left') ||
|
|
1938
|
+
node.childForFieldName('name'));
|
|
1939
|
+
} else if (node.type === 'for_statement') {
|
|
1940
|
+
names = moduleTargetNames(node.childForFieldName('left'));
|
|
1941
|
+
} else if (node.type === 'function_definition' ||
|
|
1942
|
+
node.type === 'async_function_definition' ||
|
|
1943
|
+
node.type === 'class_definition') {
|
|
1944
|
+
names = moduleTargetNames(node.childForFieldName('name'));
|
|
1945
|
+
} else if (node.type === 'import_statement') {
|
|
1946
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1947
|
+
const child = node.namedChild(i);
|
|
1948
|
+
if (child.type === 'aliased_import') {
|
|
1949
|
+
const alias = child.childForFieldName('alias');
|
|
1950
|
+
const imported = child.childForFieldName('name');
|
|
1951
|
+
if (alias) names.push(alias.text);
|
|
1952
|
+
else if (imported) names.push(imported.text.split('.')[0]);
|
|
1953
|
+
} else if (child.type === 'dotted_name') {
|
|
1954
|
+
names.push(child.text.split('.')[0]);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
} else if (node.type === 'import_from_statement') {
|
|
1958
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1959
|
+
const child = node.namedChild(i);
|
|
1960
|
+
if (child.type === 'wildcard_import') {
|
|
1961
|
+
hasModuleWildcardImport = true;
|
|
1962
|
+
} else if (child.type === 'aliased_import') {
|
|
1963
|
+
const alias = child.childForFieldName('alias');
|
|
1964
|
+
const imported = child.childForFieldName('name');
|
|
1965
|
+
if (alias) names.push(alias.text);
|
|
1966
|
+
else if (imported) names.push(imported.text.split('.').pop());
|
|
1967
|
+
} else if (i > 0 && child.type === 'dotted_name') {
|
|
1968
|
+
names.push(child.text.split('.').pop());
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
} else if (node.type === 'delete_statement') {
|
|
1972
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1973
|
+
names.push(...moduleTargetNames(node.namedChild(i)));
|
|
1974
|
+
}
|
|
1975
|
+
} else if (node.type === 'as_pattern') {
|
|
1976
|
+
names = moduleTargetNames(
|
|
1977
|
+
node.childForFieldName('alias')?.namedChild(0));
|
|
1978
|
+
}
|
|
1979
|
+
for (const name of names) {
|
|
1980
|
+
moduleBindingCounts.set(name,
|
|
1981
|
+
(moduleBindingCounts.get(name) || 0) + 1);
|
|
1982
|
+
}
|
|
1983
|
+
if (node.type !== 'assignment' || names.length !== 1) return true;
|
|
1984
|
+
const statement = node.parent?.type === 'expression_statement'
|
|
1985
|
+
? node.parent : node;
|
|
1986
|
+
if (statement.parent?.type !== 'module') return true;
|
|
1987
|
+
const right = node.childForFieldName('right');
|
|
1988
|
+
if (right?.type !== 'call') return true;
|
|
1989
|
+
const ctor = constructorTypeInfo(right.childForFieldName('function'));
|
|
1990
|
+
if (!ctor || ctor.qualifier ||
|
|
1991
|
+
isPythonConstructorValueShadowedAt(
|
|
1992
|
+
right.childForFieldName('function'), ctor.type)) return true;
|
|
1993
|
+
forwardModuleConstructorCandidates.set(names[0], ctor);
|
|
1994
|
+
return true;
|
|
1995
|
+
});
|
|
1996
|
+
const stableForwardModuleConstructors = new Map(
|
|
1997
|
+
[...forwardModuleConstructorCandidates].filter(([name]) =>
|
|
1998
|
+
!hasModuleWildcardImport && moduleBindingCounts.get(name) === 1));
|
|
1663
1999
|
|
|
1664
2000
|
traverseTree(tree.rootNode, (node) => {
|
|
1665
2001
|
// Track module-alias bindings: `import httpx` binds 'httpx' (a module),
|
|
@@ -1672,6 +2008,7 @@ function findCallsInCode(code, parser) {
|
|
|
1672
2008
|
if (child.type === 'dotted_name') {
|
|
1673
2009
|
const first = child.namedChild(0);
|
|
1674
2010
|
if (first?.type === 'identifier') moduleAliases.add(first.text);
|
|
2011
|
+
moduleImportSpecifiers.add(child.text);
|
|
1675
2012
|
} else if (child.type === 'aliased_import') {
|
|
1676
2013
|
const alias = child.childForFieldName('alias');
|
|
1677
2014
|
if (alias?.type === 'identifier') moduleAliases.add(alias.text);
|
|
@@ -1708,10 +2045,47 @@ function findCallsInCode(code, parser) {
|
|
|
1708
2045
|
localDictValueTypesStack.push(new Map(
|
|
1709
2046
|
[...localDictValueTypes].map(([name, values]) =>
|
|
1710
2047
|
[name, new Map(values)])));
|
|
2048
|
+
localSubscriptSourcesStack.push(new Map(localSubscriptSources));
|
|
1711
2049
|
localVarStdlibContractsStack.push(new Map(localVarStdlibContracts));
|
|
1712
2050
|
constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
|
|
1713
2051
|
withBindingVarsStack.push(new Set(withBindingVars));
|
|
1714
2052
|
memberAliasesStack.push(new Map(memberAliases));
|
|
2053
|
+
classValueAliasesStack.push(new Map(classValueAliases));
|
|
2054
|
+
aliasesStack.push(new Map(aliases));
|
|
2055
|
+
nonCallableNamesStack.push(new Set(nonCallableNames));
|
|
2056
|
+
const body = node.childForFieldName('body');
|
|
2057
|
+
for (const [name, ctor] of stableForwardModuleConstructors) {
|
|
2058
|
+
if (functionParameterBindsName(node, name) ||
|
|
2059
|
+
(body && pythonScopeBindsName(body, name))) continue;
|
|
2060
|
+
if (!localVarTypes.has(name)) {
|
|
2061
|
+
localVarTypes.set(name, ctor.type);
|
|
2062
|
+
constructedReceiverVars.add(name);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
for (const name of aliases.keys()) {
|
|
2066
|
+
if ((body && pythonScopeBindsName(body, name)) ||
|
|
2067
|
+
functionParameterBindsName(node, name)) {
|
|
2068
|
+
aliases.delete(name);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
for (const name of nonCallableNames) {
|
|
2072
|
+
if ((body && pythonScopeBindsName(body, name)) ||
|
|
2073
|
+
functionParameterBindsName(node, name)) {
|
|
2074
|
+
nonCallableNames.delete(name);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
for (const name of localSubscriptSources.keys()) {
|
|
2078
|
+
if ((body && pythonScopeBindsName(body, name)) ||
|
|
2079
|
+
functionParameterBindsName(node, name)) {
|
|
2080
|
+
localSubscriptSources.delete(name);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
for (const name of classValueAliases.keys()) {
|
|
2084
|
+
if ((body && pythonScopeBindsName(body, name)) ||
|
|
2085
|
+
functionParameterBindsName(node, name)) {
|
|
2086
|
+
classValueAliases.delete(name);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
1715
2089
|
}
|
|
1716
2090
|
|
|
1717
2091
|
// Track parameter type annotations: def foo(x: Foo) → x is Foo
|
|
@@ -1826,6 +2200,8 @@ function findCallsInCode(code, parser) {
|
|
|
1826
2200
|
const left = node.childForFieldName('left');
|
|
1827
2201
|
const right = node.childForFieldName('right');
|
|
1828
2202
|
if (left?.type === 'identifier') {
|
|
2203
|
+
classValueAliases.delete(left.text);
|
|
2204
|
+
localSubscriptSources.delete(left.text);
|
|
1829
2205
|
const previousType = localVarTypes.get(left.text);
|
|
1830
2206
|
if (previousType && right?.type === 'call') {
|
|
1831
2207
|
const rightFunction = right.childForFieldName('function');
|
|
@@ -1837,6 +2213,8 @@ function findCallsInCode(code, parser) {
|
|
|
1837
2213
|
}
|
|
1838
2214
|
// Track type annotation: x: Foo = ... → x is Foo
|
|
1839
2215
|
const typeNode = node.childForFieldName('type');
|
|
2216
|
+
const branchJoinUnion = !typeNode
|
|
2217
|
+
? nullableBranchJoinType(node, left.text, right) : null;
|
|
1840
2218
|
if (typeNode) {
|
|
1841
2219
|
const typeName = typeNameFromAnnotation(typeNode);
|
|
1842
2220
|
const unionTypes = typeNamesFromAnnotation(typeNode);
|
|
@@ -1865,6 +2243,9 @@ function findCallsInCode(code, parser) {
|
|
|
1865
2243
|
localIterationSources.delete(left.text);
|
|
1866
2244
|
localDictValueTypes.delete(left.text);
|
|
1867
2245
|
localVarStdlibContracts.delete(left.text);
|
|
2246
|
+
if (branchJoinUnion) {
|
|
2247
|
+
localVarUnionTypes.set(left.text, branchJoinUnion);
|
|
2248
|
+
}
|
|
1868
2249
|
// Python assignments remain constrained by a variable or
|
|
1869
2250
|
// parameter annotation. Constructor/literal inference is
|
|
1870
2251
|
// nearest-assignment only, but a declared contract is
|
|
@@ -1944,6 +2325,20 @@ function findCallsInCode(code, parser) {
|
|
|
1944
2325
|
localDictValueTypes.set(left.text, valueTypes);
|
|
1945
2326
|
}
|
|
1946
2327
|
}
|
|
2328
|
+
if (!typeNode && right?.type === 'subscript') {
|
|
2329
|
+
const base = right.childForFieldName('value');
|
|
2330
|
+
if (base?.type === 'identifier' && base.text !== left.text) {
|
|
2331
|
+
const rootType = localVarTypes.get(base.text);
|
|
2332
|
+
if (rootType) {
|
|
2333
|
+
localSubscriptSources.set(left.text, {
|
|
2334
|
+
root: base.text,
|
|
2335
|
+
rootType,
|
|
2336
|
+
rootTypeQualifier:
|
|
2337
|
+
localVarTypeQualifiers.get(base.text),
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
1947
2342
|
const roundTripSource = pickleRoundTripSource(right);
|
|
1948
2343
|
const roundTripType = roundTripSource
|
|
1949
2344
|
? localVarTypes.get(roundTripSource) : null;
|
|
@@ -1953,6 +2348,11 @@ function findCallsInCode(code, parser) {
|
|
|
1953
2348
|
}
|
|
1954
2349
|
if (right?.type === 'identifier') {
|
|
1955
2350
|
aliases.set(left.text, right.text);
|
|
2351
|
+
const classValue = !typeNode && isDirectScopeAssignment(node)
|
|
2352
|
+
? exactConstructorInfo(right) : null;
|
|
2353
|
+
if (classValue) {
|
|
2354
|
+
classValueAliases.set(left.text, classValue.type);
|
|
2355
|
+
}
|
|
1956
2356
|
}
|
|
1957
2357
|
// Member-access alias (fix #218): append = output.append
|
|
1958
2358
|
else if (right?.type === 'attribute') {
|
|
@@ -2197,13 +2597,31 @@ function findCallsInCode(code, parser) {
|
|
|
2197
2597
|
// Literal receivers carry their builtin type: {}.get() can
|
|
2198
2598
|
// never be a project class method
|
|
2199
2599
|
let subscriptReceiverType;
|
|
2600
|
+
let receiverSubscriptRoot;
|
|
2601
|
+
let receiverSubscriptRootType;
|
|
2602
|
+
let receiverSubscriptRootTypeQualifier;
|
|
2200
2603
|
if (objNode?.type === 'subscript') {
|
|
2201
2604
|
const base = objNode.childForFieldName('value');
|
|
2202
2605
|
const key = literalStringValue(
|
|
2203
2606
|
objNode.childForFieldName('subscript'));
|
|
2204
|
-
if (base?.type === 'identifier'
|
|
2205
|
-
|
|
2206
|
-
|
|
2607
|
+
if (base?.type === 'identifier') {
|
|
2608
|
+
receiverSubscriptRoot = base.text;
|
|
2609
|
+
receiverSubscriptRootType = localVarTypes.get(base.text);
|
|
2610
|
+
receiverSubscriptRootTypeQualifier =
|
|
2611
|
+
localVarTypeQualifiers.get(base.text);
|
|
2612
|
+
if (key != null) {
|
|
2613
|
+
subscriptReceiverType =
|
|
2614
|
+
localDictValueTypes.get(base.text)?.get(key);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
if (!receiverSubscriptRoot && receiver) {
|
|
2619
|
+
const source = localSubscriptSources.get(receiver);
|
|
2620
|
+
if (source) {
|
|
2621
|
+
receiverSubscriptRoot = source.root;
|
|
2622
|
+
receiverSubscriptRootType = source.rootType;
|
|
2623
|
+
receiverSubscriptRootTypeQualifier =
|
|
2624
|
+
source.rootTypeQualifier;
|
|
2207
2625
|
}
|
|
2208
2626
|
}
|
|
2209
2627
|
const receiverType = receiver
|
|
@@ -2212,7 +2630,8 @@ function findCallsInCode(code, parser) {
|
|
|
2212
2630
|
comprehensionReceiverType(
|
|
2213
2631
|
objNode, receiver, localIterableTypes,
|
|
2214
2632
|
callableIterableTypes, instanceFieldContracts) ||
|
|
2215
|
-
localVarTypes.get(receiver)
|
|
2633
|
+
localVarTypes.get(receiver) ||
|
|
2634
|
+
classValueAliases.get(receiver))
|
|
2216
2635
|
|| assignmentRhsReceiverTypes.get(node.id)
|
|
2217
2636
|
: (subscriptReceiverType ||
|
|
2218
2637
|
(objNode ? PY_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
|
|
@@ -2246,6 +2665,13 @@ function findCallsInCode(code, parser) {
|
|
|
2246
2665
|
: undefined;
|
|
2247
2666
|
const receiverRootType = receiverPath
|
|
2248
2667
|
? localVarTypes.get(receiverPath.root) : undefined;
|
|
2668
|
+
const dottedReceiver = receiverPath
|
|
2669
|
+
? [receiverPath.root, ...receiverPath.fields].join('.')
|
|
2670
|
+
: null;
|
|
2671
|
+
const receiverModuleSpecifier = dottedReceiver &&
|
|
2672
|
+
moduleImportSpecifiers.has(dottedReceiver) &&
|
|
2673
|
+
!isShadowedByLocal(objNode, receiverPath.root)
|
|
2674
|
+
? dottedReceiver : undefined;
|
|
2249
2675
|
// Module receiver (httpx.get()) — unless locally shadowed
|
|
2250
2676
|
// by a typed instance binding
|
|
2251
2677
|
const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
|
|
@@ -2270,8 +2696,20 @@ function findCallsInCode(code, parser) {
|
|
|
2270
2696
|
...(receiver && constructedReceiverVars.has(receiver) && { receiverConstructed: true }),
|
|
2271
2697
|
...(receiver && withBindingVars.has(receiver) && { receiverWithBinding: true }),
|
|
2272
2698
|
...(receiverIsModule && { receiverIsModule: true }),
|
|
2699
|
+
...(receiverModuleSpecifier && { receiverModuleSpecifier }),
|
|
2700
|
+
...(receiverSubscriptRoot && { receiverSubscriptRoot }),
|
|
2701
|
+
...(receiverSubscriptRootType && {
|
|
2702
|
+
receiverSubscriptRootType,
|
|
2703
|
+
}),
|
|
2704
|
+
...(receiverSubscriptRootTypeQualifier && {
|
|
2705
|
+
receiverSubscriptRootTypeQualifier,
|
|
2706
|
+
}),
|
|
2273
2707
|
...(receiver && objNode?.type === 'identifier' &&
|
|
2274
2708
|
isShadowedByLocal(objNode, receiver) && { receiverLocalBinding: true }),
|
|
2709
|
+
...(receiver && !receiverType && objNode?.type === 'identifier' &&
|
|
2710
|
+
isPythonUnprovenIterationBindingAt(objNode, receiver) && {
|
|
2711
|
+
receiverUntypedIteration: true,
|
|
2712
|
+
}),
|
|
2275
2713
|
...(receiverPath && {
|
|
2276
2714
|
receiverRoot: receiverPath.root,
|
|
2277
2715
|
receiverField: receiverPath.fields[receiverPath.fields.length - 1],
|
|
@@ -2432,6 +2870,13 @@ function findCallsInCode(code, parser) {
|
|
|
2432
2870
|
localDictValueTypes.set(name, values);
|
|
2433
2871
|
}
|
|
2434
2872
|
}
|
|
2873
|
+
const savedSubscriptSources = localSubscriptSourcesStack.pop();
|
|
2874
|
+
if (savedSubscriptSources) {
|
|
2875
|
+
localSubscriptSources.clear();
|
|
2876
|
+
for (const [name, source] of savedSubscriptSources) {
|
|
2877
|
+
localSubscriptSources.set(name, source);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2435
2880
|
const savedStdlibContracts = localVarStdlibContractsStack.pop();
|
|
2436
2881
|
if (savedStdlibContracts) {
|
|
2437
2882
|
localVarStdlibContracts.clear();
|
|
@@ -2450,6 +2895,25 @@ function findCallsInCode(code, parser) {
|
|
|
2450
2895
|
memberAliases.clear();
|
|
2451
2896
|
for (const [k, v] of savedAliases) memberAliases.set(k, v);
|
|
2452
2897
|
}
|
|
2898
|
+
const savedClassValueAliases = classValueAliasesStack.pop();
|
|
2899
|
+
if (savedClassValueAliases) {
|
|
2900
|
+
classValueAliases.clear();
|
|
2901
|
+
for (const [k, v] of savedClassValueAliases) {
|
|
2902
|
+
classValueAliases.set(k, v);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
const savedCallableAliases = aliasesStack.pop();
|
|
2906
|
+
if (savedCallableAliases) {
|
|
2907
|
+
aliases.clear();
|
|
2908
|
+
for (const [k, v] of savedCallableAliases) aliases.set(k, v);
|
|
2909
|
+
}
|
|
2910
|
+
const savedNonCallableNames = nonCallableNamesStack.pop();
|
|
2911
|
+
if (savedNonCallableNames) {
|
|
2912
|
+
nonCallableNames.clear();
|
|
2913
|
+
for (const name of savedNonCallableNames) {
|
|
2914
|
+
nonCallableNames.add(name);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2453
2917
|
}
|
|
2454
2918
|
}
|
|
2455
2919
|
});
|
|
@@ -2468,10 +2932,24 @@ function findImportsInCode(code, parser) {
|
|
|
2468
2932
|
const imports = [];
|
|
2469
2933
|
let importAliases = null; // {original, local}[] — tracks renamed imports
|
|
2470
2934
|
|
|
2935
|
+
// Imports nested in a function/lambda do not execute during ordinary
|
|
2936
|
+
// module initialization. Preserve that AST fact so dependency-cycle
|
|
2937
|
+
// reporting can distinguish an eager import loop from a deliberate lazy
|
|
2938
|
+
// edge without deleting either edge from the graph.
|
|
2939
|
+
const isDeferredImport = (node) => {
|
|
2940
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
2941
|
+
if (parent.type === 'function_definition' || parent.type === 'lambda') {
|
|
2942
|
+
return true;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
return false;
|
|
2946
|
+
};
|
|
2947
|
+
|
|
2471
2948
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2472
2949
|
// import statement: import os, import sys as system
|
|
2473
2950
|
if (node.type === 'import_statement') {
|
|
2474
2951
|
const line = node.startPosition.row + 1;
|
|
2952
|
+
const deferred = isDeferredImport(node);
|
|
2475
2953
|
|
|
2476
2954
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2477
2955
|
const child = node.namedChild(i);
|
|
@@ -2487,20 +2965,23 @@ function findImportsInCode(code, parser) {
|
|
|
2487
2965
|
module: parts[0],
|
|
2488
2966
|
names: [parts[0]],
|
|
2489
2967
|
type: 'import',
|
|
2490
|
-
line
|
|
2968
|
+
line,
|
|
2969
|
+
...(deferred && { deferred: true })
|
|
2491
2970
|
});
|
|
2492
2971
|
imports.push({
|
|
2493
2972
|
module: child.text,
|
|
2494
2973
|
names: [],
|
|
2495
2974
|
type: 'import-submodule',
|
|
2496
|
-
line
|
|
2975
|
+
line,
|
|
2976
|
+
...(deferred && { deferred: true })
|
|
2497
2977
|
});
|
|
2498
2978
|
} else {
|
|
2499
2979
|
imports.push({
|
|
2500
2980
|
module: child.text,
|
|
2501
2981
|
names: [child.text],
|
|
2502
2982
|
type: 'import',
|
|
2503
|
-
line
|
|
2983
|
+
line,
|
|
2984
|
+
...(deferred && { deferred: true })
|
|
2504
2985
|
});
|
|
2505
2986
|
}
|
|
2506
2987
|
} else if (child.type === 'aliased_import') {
|
|
@@ -2512,7 +2993,8 @@ function findImportsInCode(code, parser) {
|
|
|
2512
2993
|
module: nameNode.text,
|
|
2513
2994
|
names: [aliasNode ? aliasNode.text : nameNode.text.split('.').pop()],
|
|
2514
2995
|
type: 'import',
|
|
2515
|
-
line
|
|
2996
|
+
line,
|
|
2997
|
+
...(deferred && { deferred: true })
|
|
2516
2998
|
});
|
|
2517
2999
|
if (aliasNode && aliasNode.text !== nameNode.text) {
|
|
2518
3000
|
if (!importAliases) importAliases = [];
|
|
@@ -2527,6 +3009,7 @@ function findImportsInCode(code, parser) {
|
|
|
2527
3009
|
// from ... import statement
|
|
2528
3010
|
if (node.type === 'import_from_statement') {
|
|
2529
3011
|
const line = node.startPosition.row + 1;
|
|
3012
|
+
const deferred = isDeferredImport(node);
|
|
2530
3013
|
let modulePath = '';
|
|
2531
3014
|
const names = [];
|
|
2532
3015
|
|
|
@@ -2559,7 +3042,8 @@ function findImportsInCode(code, parser) {
|
|
|
2559
3042
|
module: modulePath,
|
|
2560
3043
|
names,
|
|
2561
3044
|
type: isRelative ? 'relative' : 'from',
|
|
2562
|
-
line
|
|
3045
|
+
line,
|
|
3046
|
+
...(deferred && { deferred: true })
|
|
2563
3047
|
});
|
|
2564
3048
|
}
|
|
2565
3049
|
return true;
|
|
@@ -2574,13 +3058,15 @@ function findImportsInCode(code, parser) {
|
|
|
2574
3058
|
const firstArg = argsNode.namedChild(0);
|
|
2575
3059
|
if ((funcName === 'importlib.import_module' || funcName === '__import__') && firstArg) {
|
|
2576
3060
|
const line = node.startPosition.row + 1;
|
|
3061
|
+
const deferred = isDeferredImport(node);
|
|
2577
3062
|
const isLiteral = firstArg.type === 'string';
|
|
2578
3063
|
imports.push({
|
|
2579
3064
|
module: isLiteral ? firstArg.text.replace(/^['"]|['"]$/g, '') : firstArg.text,
|
|
2580
3065
|
names: [],
|
|
2581
3066
|
type: 'dynamic',
|
|
2582
3067
|
line,
|
|
2583
|
-
dynamic: !isLiteral
|
|
3068
|
+
dynamic: !isLiteral,
|
|
3069
|
+
...(deferred && { deferred: true })
|
|
2584
3070
|
});
|
|
2585
3071
|
}
|
|
2586
3072
|
}
|
|
@@ -2812,6 +3298,50 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2812
3298
|
const explicitContracts = explicitInstanceFieldContracts(tree, parser);
|
|
2813
3299
|
|
|
2814
3300
|
const PRIMITIVE_TYPES = new Set(['int', 'float', 'str', 'bool', 'bytes', 'list', 'dict', 'set', 'tuple', 'None', 'Any', 'object']);
|
|
3301
|
+
const conditionalFieldType = (node, parameterAlternatives) => {
|
|
3302
|
+
let current = node;
|
|
3303
|
+
while (current?.type === 'parenthesized_expression' &&
|
|
3304
|
+
current.namedChildCount === 1) {
|
|
3305
|
+
current = current.namedChild(0);
|
|
3306
|
+
}
|
|
3307
|
+
if (current?.type !== 'conditional_expression' ||
|
|
3308
|
+
current.namedChildCount < 3) return null;
|
|
3309
|
+
const consequence = current.namedChild(0);
|
|
3310
|
+
const condition = current.namedChild(1);
|
|
3311
|
+
const alternative = current.namedChild(2);
|
|
3312
|
+
const parameterBranches = [consequence, alternative]
|
|
3313
|
+
.map((branch, index) => branch?.type === 'identifier' &&
|
|
3314
|
+
parameterAlternatives.has(branch.text)
|
|
3315
|
+
? { index, name: branch.text } : null)
|
|
3316
|
+
.filter(Boolean);
|
|
3317
|
+
if (parameterBranches.length !== 1) return null;
|
|
3318
|
+
const parameterBranch = parameterBranches[0];
|
|
3319
|
+
const positiveTypes = isinstanceTypes(condition, parameterBranch.name);
|
|
3320
|
+
if (positiveTypes.length === 0) return null;
|
|
3321
|
+
const declaredTypes = parameterAlternatives.get(parameterBranch.name);
|
|
3322
|
+
const narrowedTypes = parameterBranch.index === 0
|
|
3323
|
+
? declaredTypes.filter(type => positiveTypes.includes(type))
|
|
3324
|
+
: declaredTypes.filter(type => !positiveTypes.includes(type));
|
|
3325
|
+
if (narrowedTypes.length !== 1) return null;
|
|
3326
|
+
|
|
3327
|
+
const valueBranch = parameterBranch.index === 0
|
|
3328
|
+
? alternative : consequence;
|
|
3329
|
+
if (valueBranch?.type !== 'call') return null;
|
|
3330
|
+
const callable = valueBranch.childForFieldName('function');
|
|
3331
|
+
let valueType = null;
|
|
3332
|
+
if (callable?.type === 'identifier' && /^[A-Z]/.test(callable.text) &&
|
|
3333
|
+
!isPythonConstructorValueShadowedAt(callable, callable.text)) {
|
|
3334
|
+
valueType = callable.text;
|
|
3335
|
+
} else if (callable?.type === 'attribute') {
|
|
3336
|
+
const owner = callable.childForFieldName('object');
|
|
3337
|
+
const method = callable.childForFieldName('attribute');
|
|
3338
|
+
if (owner?.type === 'identifier' && method?.type === 'identifier' &&
|
|
3339
|
+
!isPythonNameShadowedAt(callable, owner.text)) {
|
|
3340
|
+
valueType = options.resolveCallType?.(owner.text, method.text);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
return valueType === narrowedTypes[0] ? valueType : null;
|
|
3344
|
+
};
|
|
2815
3345
|
|
|
2816
3346
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2817
3347
|
if (node.type !== 'class_definition') return true;
|
|
@@ -2866,10 +3396,14 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2866
3396
|
if (!typeNode) continue;
|
|
2867
3397
|
if (!isDataclass && assign.childForFieldName('right')) continue;
|
|
2868
3398
|
|
|
2869
|
-
//
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3399
|
+
// Use the same single-type annotation parser as parameters and
|
|
3400
|
+
// explicit instance fields. This admits compiler-equivalent bare
|
|
3401
|
+
// contracts such as `color: Optional[Color]` and `Color | None`,
|
|
3402
|
+
// while unions with several value types still abstain. Qualified
|
|
3403
|
+
// names need origin metadata that this compact attr map does not
|
|
3404
|
+
// retain, so leave those to declared field symbols.
|
|
3405
|
+
const typeName = typeNameFromAnnotation(typeNode);
|
|
3406
|
+
if (!typeName || typeQualifierFromAnnotation(typeNode)) continue;
|
|
2873
3407
|
|
|
2874
3408
|
// Skip primitives and lowercase types
|
|
2875
3409
|
if (PRIMITIVE_TYPES.has(typeName)) continue;
|
|
@@ -2905,6 +3439,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2905
3439
|
// Build parameter type map from __init__ annotations
|
|
2906
3440
|
// e.g. def __init__(self, market: MarketDataFetcher = None) → {market: MarketDataFetcher}
|
|
2907
3441
|
const paramTypes = new Map();
|
|
3442
|
+
const paramAlternatives = new Map();
|
|
2908
3443
|
const params = child.childForFieldName('parameters');
|
|
2909
3444
|
if (params) {
|
|
2910
3445
|
for (let p = 0; p < params.childCount; p++) {
|
|
@@ -2914,12 +3449,25 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2914
3449
|
const pName = param.childForFieldName('name') || param.child(0);
|
|
2915
3450
|
const pType = param.childForFieldName('type');
|
|
2916
3451
|
if (pName && pType) {
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
3452
|
+
// Use the shared annotation parser so forward
|
|
3453
|
+
// references (`"Environment"`), dotted names,
|
|
3454
|
+
// and Optional/union wrappers participate in the
|
|
3455
|
+
// same conservative receiver-type contract as
|
|
3456
|
+
// ordinary parameter calls.
|
|
3457
|
+
const tn = typeNameFromAnnotation(pType);
|
|
3458
|
+
const directAlternatives = typeNamesFromAnnotation(pType);
|
|
3459
|
+
const aliasAlternatives = directAlternatives.length === 1
|
|
3460
|
+
? options.resolveTypeAliasMembers?.(
|
|
3461
|
+
directAlternatives[0])
|
|
3462
|
+
: null;
|
|
3463
|
+
const alternatives = aliasAlternatives?.length > 1
|
|
3464
|
+
? aliasAlternatives : directAlternatives;
|
|
3465
|
+
if (alternatives.length > 1) {
|
|
3466
|
+
paramAlternatives.set(pName.text, alternatives);
|
|
3467
|
+
}
|
|
3468
|
+
if (tn && !PRIMITIVE_TYPES.has(tn) &&
|
|
3469
|
+
tn[0] >= 'A' && tn[0] <= 'Z') {
|
|
3470
|
+
paramTypes.set(pName.text, tn);
|
|
2923
3471
|
}
|
|
2924
3472
|
}
|
|
2925
3473
|
}
|
|
@@ -2945,7 +3493,8 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2945
3493
|
const rhs = assign.childForFieldName('right');
|
|
2946
3494
|
if (!rhs) return true;
|
|
2947
3495
|
|
|
2948
|
-
const typeName =
|
|
3496
|
+
const typeName = conditionalFieldType(rhs, paramAlternatives) ||
|
|
3497
|
+
extractConstructorName(rhs);
|
|
2949
3498
|
if (typeName) {
|
|
2950
3499
|
attrTypes.set(attrName, typeName);
|
|
2951
3500
|
} else if (rhs.type === 'call') {
|
|
@@ -3000,13 +3549,23 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3000
3549
|
const right = assignment.childForFieldName('right');
|
|
3001
3550
|
const callable = right?.type === 'call'
|
|
3002
3551
|
? right.childForFieldName('function') : null;
|
|
3003
|
-
const
|
|
3004
|
-
? callable.childForFieldName('object')
|
|
3552
|
+
const callableReceiver = callable?.type === 'attribute'
|
|
3553
|
+
? callable.childForFieldName('object') : null;
|
|
3554
|
+
const directConstructor = callable?.type === 'identifier' &&
|
|
3555
|
+
/^[A-Z]/.test(callable.text) &&
|
|
3556
|
+
!isPythonConstructorValueShadowedAt(callable, callable.text)
|
|
3557
|
+
? callable.text : null;
|
|
3558
|
+
const moduleName = callableReceiver?.type === 'identifier'
|
|
3559
|
+
? callableReceiver.text : null;
|
|
3005
3560
|
const functionName = callable?.type === 'attribute'
|
|
3006
3561
|
? callable.childForFieldName('attribute')?.text : null;
|
|
3007
|
-
const
|
|
3008
|
-
|
|
3009
|
-
|
|
3562
|
+
const resolveCallType = options.resolveCallType ||
|
|
3563
|
+
options.resolveBuiltinCallType;
|
|
3564
|
+
const runtimeType = directConstructor ||
|
|
3565
|
+
(moduleName && functionName &&
|
|
3566
|
+
!isPythonNameShadowedAt(callable, moduleName)
|
|
3567
|
+
? resolveCallType?.(moduleName, functionName)
|
|
3568
|
+
: null);
|
|
3010
3569
|
if (!runtimeType) {
|
|
3011
3570
|
// None is a harmless uninitialized state; any other
|
|
3012
3571
|
// unknown write could replace the field with a project
|