ucn 5.2.1 → 5.3.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 +49 -3
- package/.claude/skills/ucn/references/commands.md +3 -1
- package/README.md +158 -533
- package/cli/index.js +74 -10
- package/core/account.js +36 -8
- package/core/cache.js +167 -21
- package/core/callers.js +1229 -162
- package/core/execute.js +24 -6
- package/core/graph.js +167 -35
- package/core/index-ir.js +17 -12
- package/core/ir.js +56 -8
- package/core/output/graph.js +60 -11
- package/core/output/lines.js +259 -0
- package/core/output/public.js +15 -0
- package/core/output/reporting.js +9 -2
- package/core/output-budget.js +7 -4
- package/core/project.js +103 -5
- package/core/registry.js +7 -6
- package/core/reporting.js +159 -14
- package/languages/c-family.js +19 -17
- package/languages/go.js +170 -42
- package/languages/javascript.js +470 -15
- package/languages/python.js +678 -38
- package/languages/rust.js +1 -0
- package/mcp/server.js +3 -1
- package/package.json +2 -2
- package/assets/demo.svg +0 -31
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
|
});
|
|
@@ -2472,20 +2936,123 @@ function findImportsInCode(code, parser) {
|
|
|
2472
2936
|
// module initialization. Preserve that AST fact so dependency-cycle
|
|
2473
2937
|
// reporting can distinguish an eager import loop from a deliberate lazy
|
|
2474
2938
|
// edge without deleting either edge from the graph.
|
|
2475
|
-
|
|
2939
|
+
// fix #338: `if TYPE_CHECKING:` (bare, `t.TYPE_CHECKING`,
|
|
2940
|
+
// `typing.TYPE_CHECKING`) consequence blocks never execute at runtime —
|
|
2941
|
+
// the import exists for the type checker only. Only the consequence
|
|
2942
|
+
// branch is guarded: `else:` and `if not TYPE_CHECKING:` bodies DO run.
|
|
2943
|
+
let typingBindings = null;
|
|
2944
|
+
let typingWildcard = false;
|
|
2945
|
+
const isNestedScope = node => {
|
|
2946
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
2947
|
+
if (p.type === 'function_definition' || p.type === 'class_definition' || p.type === 'lambda') return true;
|
|
2948
|
+
}
|
|
2949
|
+
return false;
|
|
2950
|
+
};
|
|
2951
|
+
// Which local names prove a runtime-false guard, and where. A guard at
|
|
2952
|
+
// module level reads the module binding: only module-level rebindings
|
|
2953
|
+
// (assignments, loops, def/class names, walrus, `as` targets) and a
|
|
2954
|
+
// second import of the same name can disturb it — a function parameter
|
|
2955
|
+
// or a nested import lives in its own scope. A guard INSIDE a function
|
|
2956
|
+
// can additionally be shadowed by that function's locals, so nested
|
|
2957
|
+
// rebindings poison nested guards (conservative: any nested scope, not
|
|
2958
|
+
// just the enclosing one). Same-spelled user flags/attributes can be
|
|
2959
|
+
// true; only a unique `typing` binding proves the guard false.
|
|
2960
|
+
const typingBindingKind = (name, guardNested) => {
|
|
2961
|
+
if (!typingBindings) {
|
|
2962
|
+
typingBindings = new Map();
|
|
2963
|
+
const entry = local => {
|
|
2964
|
+
let e = typingBindings.get(local);
|
|
2965
|
+
if (!e) { e = { kind: null, imports: 0, moduleShadow: false, nestedShadow: false }; typingBindings.set(local, e); }
|
|
2966
|
+
return e;
|
|
2967
|
+
};
|
|
2968
|
+
const shadow = (pattern, nested) => {
|
|
2969
|
+
if (!pattern) return;
|
|
2970
|
+
traverseTree(pattern, child => {
|
|
2971
|
+
if (child.type === 'identifier') {
|
|
2972
|
+
const e = entry(child.text);
|
|
2973
|
+
if (nested) e.nestedShadow = true; else e.moduleShadow = true;
|
|
2974
|
+
}
|
|
2975
|
+
return true;
|
|
2976
|
+
});
|
|
2977
|
+
};
|
|
2978
|
+
traverseTree(tree.rootNode, n => {
|
|
2979
|
+
const nested = isNestedScope(n);
|
|
2980
|
+
if (n.type === 'import_statement' || n.type === 'import_from_statement') {
|
|
2981
|
+
if (n.namedChildren.some(item => item.type === 'wildcard_import')) typingWildcard = true;
|
|
2982
|
+
const moduleNode = n.childForFieldName('module_name');
|
|
2983
|
+
for (const item of n.namedChildren) {
|
|
2984
|
+
if (moduleNode && sameNode(moduleNode, item)) continue;
|
|
2985
|
+
const imported = item.type === 'aliased_import' ? item.childForFieldName('name')?.text : item.text;
|
|
2986
|
+
const local = item.type === 'aliased_import' ? item.childForFieldName('alias')?.text : imported?.split('.')[0];
|
|
2987
|
+
if (!local) continue;
|
|
2988
|
+
const e = entry(local);
|
|
2989
|
+
if (nested) { e.nestedShadow = true; continue; }
|
|
2990
|
+
const kind = n.type === 'import_statement'
|
|
2991
|
+
? (imported === 'typing' ? 'module' : null)
|
|
2992
|
+
: (moduleNode?.text === 'typing' && imported === 'TYPE_CHECKING' ? 'flag' : null);
|
|
2993
|
+
e.imports++;
|
|
2994
|
+
e.kind = e.imports === 1 ? kind : null;
|
|
2995
|
+
}
|
|
2996
|
+
} else if (n.type === 'assignment' || n.type === 'augmented_assignment' || n.type === 'for_statement' || n.type === 'for_in_clause') {
|
|
2997
|
+
shadow(n.childForFieldName('left'), nested);
|
|
2998
|
+
} else if (n.type === 'parameters' || n.type === 'lambda_parameters') {
|
|
2999
|
+
// Parameter NAMES rebind; annotations (`x: t.Any`) and
|
|
3000
|
+
// default values are expressions that READ the outer
|
|
3001
|
+
// binding. Walking the whole subtree treated every
|
|
3002
|
+
// `t.`-annotated parameter as a rebinding of `t` and
|
|
3003
|
+
// reverted click's TYPE_CHECKING guards to eager.
|
|
3004
|
+
for (const param of n.namedChildren) {
|
|
3005
|
+
if (param.type === 'default_parameter' || param.type === 'typed_default_parameter') {
|
|
3006
|
+
shadow(param.childForFieldName('name'), true);
|
|
3007
|
+
} else if (param.type === 'typed_parameter') {
|
|
3008
|
+
shadow(param.namedChild(0), true);
|
|
3009
|
+
} else if (param.type !== 'keyword_separator' && param.type !== 'positional_separator') {
|
|
3010
|
+
shadow(param, true);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
} else if (n.type === 'named_expression') shadow(n.childForFieldName('name'), nested);
|
|
3014
|
+
else if (n.type === 'function_definition' || n.type === 'class_definition') shadow(n.childForFieldName('name'), nested);
|
|
3015
|
+
else if (n.type === 'as_pattern') shadow(n.childForFieldName('alias'), nested);
|
|
3016
|
+
return true;
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
if (typingWildcard) return null;
|
|
3020
|
+
const e = typingBindings.get(name);
|
|
3021
|
+
if (!e || e.imports !== 1 || e.moduleShadow) return null;
|
|
3022
|
+
if (guardNested && e.nestedShadow) return null;
|
|
3023
|
+
return e.kind;
|
|
3024
|
+
};
|
|
3025
|
+
const isTypeCheckingGuard = (condition, guardNested = true) => {
|
|
3026
|
+
if (!condition) return false;
|
|
3027
|
+
if (condition.type === 'parenthesized_expression') return isTypeCheckingGuard(condition.namedChild(0), guardNested);
|
|
3028
|
+
if (condition.type === 'identifier') return typingBindingKind(condition.text, guardNested) === 'flag';
|
|
3029
|
+
if (condition.type === 'attribute') {
|
|
3030
|
+
const object = condition.childForFieldName('object');
|
|
3031
|
+
return condition.childForFieldName('attribute')?.text === 'TYPE_CHECKING' &&
|
|
3032
|
+
object?.type === 'identifier' && typingBindingKind(object.text, guardNested) === 'module';
|
|
3033
|
+
}
|
|
3034
|
+
return false;
|
|
3035
|
+
};
|
|
3036
|
+
const importDeferral = (node) => {
|
|
2476
3037
|
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
2477
3038
|
if (parent.type === 'function_definition' || parent.type === 'lambda') {
|
|
2478
|
-
return
|
|
3039
|
+
return 'function-local';
|
|
3040
|
+
}
|
|
3041
|
+
if (parent.type === 'block' && parent.parent &&
|
|
3042
|
+
(parent.parent.type === 'if_statement' || parent.parent.type === 'elif_clause') &&
|
|
3043
|
+
sameNode(parent.parent.childForFieldName('consequence'), parent) &&
|
|
3044
|
+
isTypeCheckingGuard(parent.parent.childForFieldName('condition'), isNestedScope(parent.parent))) {
|
|
3045
|
+
return 'type-checking';
|
|
2479
3046
|
}
|
|
2480
3047
|
}
|
|
2481
|
-
return
|
|
3048
|
+
return null;
|
|
2482
3049
|
};
|
|
2483
3050
|
|
|
2484
3051
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2485
3052
|
// import statement: import os, import sys as system
|
|
2486
3053
|
if (node.type === 'import_statement') {
|
|
2487
3054
|
const line = node.startPosition.row + 1;
|
|
2488
|
-
const
|
|
3055
|
+
const deferral = importDeferral(node);
|
|
2489
3056
|
|
|
2490
3057
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2491
3058
|
const child = node.namedChild(i);
|
|
@@ -2502,14 +3069,14 @@ function findImportsInCode(code, parser) {
|
|
|
2502
3069
|
names: [parts[0]],
|
|
2503
3070
|
type: 'import',
|
|
2504
3071
|
line,
|
|
2505
|
-
...(
|
|
3072
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2506
3073
|
});
|
|
2507
3074
|
imports.push({
|
|
2508
3075
|
module: child.text,
|
|
2509
3076
|
names: [],
|
|
2510
3077
|
type: 'import-submodule',
|
|
2511
3078
|
line,
|
|
2512
|
-
...(
|
|
3079
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2513
3080
|
});
|
|
2514
3081
|
} else {
|
|
2515
3082
|
imports.push({
|
|
@@ -2517,7 +3084,7 @@ function findImportsInCode(code, parser) {
|
|
|
2517
3084
|
names: [child.text],
|
|
2518
3085
|
type: 'import',
|
|
2519
3086
|
line,
|
|
2520
|
-
...(
|
|
3087
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2521
3088
|
});
|
|
2522
3089
|
}
|
|
2523
3090
|
} else if (child.type === 'aliased_import') {
|
|
@@ -2530,7 +3097,7 @@ function findImportsInCode(code, parser) {
|
|
|
2530
3097
|
names: [aliasNode ? aliasNode.text : nameNode.text.split('.').pop()],
|
|
2531
3098
|
type: 'import',
|
|
2532
3099
|
line,
|
|
2533
|
-
...(
|
|
3100
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2534
3101
|
});
|
|
2535
3102
|
if (aliasNode && aliasNode.text !== nameNode.text) {
|
|
2536
3103
|
if (!importAliases) importAliases = [];
|
|
@@ -2545,7 +3112,7 @@ function findImportsInCode(code, parser) {
|
|
|
2545
3112
|
// from ... import statement
|
|
2546
3113
|
if (node.type === 'import_from_statement') {
|
|
2547
3114
|
const line = node.startPosition.row + 1;
|
|
2548
|
-
const
|
|
3115
|
+
const deferral = importDeferral(node);
|
|
2549
3116
|
let modulePath = '';
|
|
2550
3117
|
const names = [];
|
|
2551
3118
|
|
|
@@ -2579,7 +3146,7 @@ function findImportsInCode(code, parser) {
|
|
|
2579
3146
|
names,
|
|
2580
3147
|
type: isRelative ? 'relative' : 'from',
|
|
2581
3148
|
line,
|
|
2582
|
-
...(
|
|
3149
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2583
3150
|
});
|
|
2584
3151
|
}
|
|
2585
3152
|
return true;
|
|
@@ -2594,7 +3161,7 @@ function findImportsInCode(code, parser) {
|
|
|
2594
3161
|
const firstArg = argsNode.namedChild(0);
|
|
2595
3162
|
if ((funcName === 'importlib.import_module' || funcName === '__import__') && firstArg) {
|
|
2596
3163
|
const line = node.startPosition.row + 1;
|
|
2597
|
-
const
|
|
3164
|
+
const deferral = importDeferral(node);
|
|
2598
3165
|
const isLiteral = firstArg.type === 'string';
|
|
2599
3166
|
imports.push({
|
|
2600
3167
|
module: isLiteral ? firstArg.text.replace(/^['"]|['"]$/g, '') : firstArg.text,
|
|
@@ -2602,7 +3169,7 @@ function findImportsInCode(code, parser) {
|
|
|
2602
3169
|
type: 'dynamic',
|
|
2603
3170
|
line,
|
|
2604
3171
|
dynamic: !isLiteral,
|
|
2605
|
-
...(
|
|
3172
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2606
3173
|
});
|
|
2607
3174
|
}
|
|
2608
3175
|
}
|
|
@@ -2834,6 +3401,50 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2834
3401
|
const explicitContracts = explicitInstanceFieldContracts(tree, parser);
|
|
2835
3402
|
|
|
2836
3403
|
const PRIMITIVE_TYPES = new Set(['int', 'float', 'str', 'bool', 'bytes', 'list', 'dict', 'set', 'tuple', 'None', 'Any', 'object']);
|
|
3404
|
+
const conditionalFieldType = (node, parameterAlternatives) => {
|
|
3405
|
+
let current = node;
|
|
3406
|
+
while (current?.type === 'parenthesized_expression' &&
|
|
3407
|
+
current.namedChildCount === 1) {
|
|
3408
|
+
current = current.namedChild(0);
|
|
3409
|
+
}
|
|
3410
|
+
if (current?.type !== 'conditional_expression' ||
|
|
3411
|
+
current.namedChildCount < 3) return null;
|
|
3412
|
+
const consequence = current.namedChild(0);
|
|
3413
|
+
const condition = current.namedChild(1);
|
|
3414
|
+
const alternative = current.namedChild(2);
|
|
3415
|
+
const parameterBranches = [consequence, alternative]
|
|
3416
|
+
.map((branch, index) => branch?.type === 'identifier' &&
|
|
3417
|
+
parameterAlternatives.has(branch.text)
|
|
3418
|
+
? { index, name: branch.text } : null)
|
|
3419
|
+
.filter(Boolean);
|
|
3420
|
+
if (parameterBranches.length !== 1) return null;
|
|
3421
|
+
const parameterBranch = parameterBranches[0];
|
|
3422
|
+
const positiveTypes = isinstanceTypes(condition, parameterBranch.name);
|
|
3423
|
+
if (positiveTypes.length === 0) return null;
|
|
3424
|
+
const declaredTypes = parameterAlternatives.get(parameterBranch.name);
|
|
3425
|
+
const narrowedTypes = parameterBranch.index === 0
|
|
3426
|
+
? declaredTypes.filter(type => positiveTypes.includes(type))
|
|
3427
|
+
: declaredTypes.filter(type => !positiveTypes.includes(type));
|
|
3428
|
+
if (narrowedTypes.length !== 1) return null;
|
|
3429
|
+
|
|
3430
|
+
const valueBranch = parameterBranch.index === 0
|
|
3431
|
+
? alternative : consequence;
|
|
3432
|
+
if (valueBranch?.type !== 'call') return null;
|
|
3433
|
+
const callable = valueBranch.childForFieldName('function');
|
|
3434
|
+
let valueType = null;
|
|
3435
|
+
if (callable?.type === 'identifier' && /^[A-Z]/.test(callable.text) &&
|
|
3436
|
+
!isPythonConstructorValueShadowedAt(callable, callable.text)) {
|
|
3437
|
+
valueType = callable.text;
|
|
3438
|
+
} else if (callable?.type === 'attribute') {
|
|
3439
|
+
const owner = callable.childForFieldName('object');
|
|
3440
|
+
const method = callable.childForFieldName('attribute');
|
|
3441
|
+
if (owner?.type === 'identifier' && method?.type === 'identifier' &&
|
|
3442
|
+
!isPythonNameShadowedAt(callable, owner.text)) {
|
|
3443
|
+
valueType = options.resolveCallType?.(owner.text, method.text);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
return valueType === narrowedTypes[0] ? valueType : null;
|
|
3447
|
+
};
|
|
2837
3448
|
|
|
2838
3449
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2839
3450
|
if (node.type !== 'class_definition') return true;
|
|
@@ -2888,10 +3499,14 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2888
3499
|
if (!typeNode) continue;
|
|
2889
3500
|
if (!isDataclass && assign.childForFieldName('right')) continue;
|
|
2890
3501
|
|
|
2891
|
-
//
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
3502
|
+
// Use the same single-type annotation parser as parameters and
|
|
3503
|
+
// explicit instance fields. This admits compiler-equivalent bare
|
|
3504
|
+
// contracts such as `color: Optional[Color]` and `Color | None`,
|
|
3505
|
+
// while unions with several value types still abstain. Qualified
|
|
3506
|
+
// names need origin metadata that this compact attr map does not
|
|
3507
|
+
// retain, so leave those to declared field symbols.
|
|
3508
|
+
const typeName = typeNameFromAnnotation(typeNode);
|
|
3509
|
+
if (!typeName || typeQualifierFromAnnotation(typeNode)) continue;
|
|
2895
3510
|
|
|
2896
3511
|
// Skip primitives and lowercase types
|
|
2897
3512
|
if (PRIMITIVE_TYPES.has(typeName)) continue;
|
|
@@ -2927,6 +3542,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2927
3542
|
// Build parameter type map from __init__ annotations
|
|
2928
3543
|
// e.g. def __init__(self, market: MarketDataFetcher = None) → {market: MarketDataFetcher}
|
|
2929
3544
|
const paramTypes = new Map();
|
|
3545
|
+
const paramAlternatives = new Map();
|
|
2930
3546
|
const params = child.childForFieldName('parameters');
|
|
2931
3547
|
if (params) {
|
|
2932
3548
|
for (let p = 0; p < params.childCount; p++) {
|
|
@@ -2936,12 +3552,25 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2936
3552
|
const pName = param.childForFieldName('name') || param.child(0);
|
|
2937
3553
|
const pType = param.childForFieldName('type');
|
|
2938
3554
|
if (pName && pType) {
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
3555
|
+
// Use the shared annotation parser so forward
|
|
3556
|
+
// references (`"Environment"`), dotted names,
|
|
3557
|
+
// and Optional/union wrappers participate in the
|
|
3558
|
+
// same conservative receiver-type contract as
|
|
3559
|
+
// ordinary parameter calls.
|
|
3560
|
+
const tn = typeNameFromAnnotation(pType);
|
|
3561
|
+
const directAlternatives = typeNamesFromAnnotation(pType);
|
|
3562
|
+
const aliasAlternatives = directAlternatives.length === 1
|
|
3563
|
+
? options.resolveTypeAliasMembers?.(
|
|
3564
|
+
directAlternatives[0])
|
|
3565
|
+
: null;
|
|
3566
|
+
const alternatives = aliasAlternatives?.length > 1
|
|
3567
|
+
? aliasAlternatives : directAlternatives;
|
|
3568
|
+
if (alternatives.length > 1) {
|
|
3569
|
+
paramAlternatives.set(pName.text, alternatives);
|
|
3570
|
+
}
|
|
3571
|
+
if (tn && !PRIMITIVE_TYPES.has(tn) &&
|
|
3572
|
+
tn[0] >= 'A' && tn[0] <= 'Z') {
|
|
3573
|
+
paramTypes.set(pName.text, tn);
|
|
2945
3574
|
}
|
|
2946
3575
|
}
|
|
2947
3576
|
}
|
|
@@ -2967,7 +3596,8 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
2967
3596
|
const rhs = assign.childForFieldName('right');
|
|
2968
3597
|
if (!rhs) return true;
|
|
2969
3598
|
|
|
2970
|
-
const typeName =
|
|
3599
|
+
const typeName = conditionalFieldType(rhs, paramAlternatives) ||
|
|
3600
|
+
extractConstructorName(rhs);
|
|
2971
3601
|
if (typeName) {
|
|
2972
3602
|
attrTypes.set(attrName, typeName);
|
|
2973
3603
|
} else if (rhs.type === 'call') {
|
|
@@ -3022,13 +3652,23 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3022
3652
|
const right = assignment.childForFieldName('right');
|
|
3023
3653
|
const callable = right?.type === 'call'
|
|
3024
3654
|
? right.childForFieldName('function') : null;
|
|
3025
|
-
const
|
|
3026
|
-
? callable.childForFieldName('object')
|
|
3655
|
+
const callableReceiver = callable?.type === 'attribute'
|
|
3656
|
+
? callable.childForFieldName('object') : null;
|
|
3657
|
+
const directConstructor = callable?.type === 'identifier' &&
|
|
3658
|
+
/^[A-Z]/.test(callable.text) &&
|
|
3659
|
+
!isPythonConstructorValueShadowedAt(callable, callable.text)
|
|
3660
|
+
? callable.text : null;
|
|
3661
|
+
const moduleName = callableReceiver?.type === 'identifier'
|
|
3662
|
+
? callableReceiver.text : null;
|
|
3027
3663
|
const functionName = callable?.type === 'attribute'
|
|
3028
3664
|
? callable.childForFieldName('attribute')?.text : null;
|
|
3029
|
-
const
|
|
3030
|
-
|
|
3031
|
-
|
|
3665
|
+
const resolveCallType = options.resolveCallType ||
|
|
3666
|
+
options.resolveBuiltinCallType;
|
|
3667
|
+
const runtimeType = directConstructor ||
|
|
3668
|
+
(moduleName && functionName &&
|
|
3669
|
+
!isPythonNameShadowedAt(callable, moduleName)
|
|
3670
|
+
? resolveCallType?.(moduleName, functionName)
|
|
3671
|
+
: null);
|
|
3032
3672
|
if (!runtimeType) {
|
|
3033
3673
|
// None is a harmless uninitialized state; any other
|
|
3034
3674
|
// unknown write could replace the field with a project
|