ucn 4.2.3 → 5.0.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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/languages/python.js
CHANGED
|
@@ -38,6 +38,68 @@ function extractReturnType(node) {
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
const PY_FUNCTION_SCOPE_NODES = new Set([
|
|
42
|
+
'function_definition', 'async_function_definition', 'lambda',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Concrete constructor calls returned by one Python function.
|
|
47
|
+
*
|
|
48
|
+
* This is deliberately data-only parser evidence. Query-time analysis still
|
|
49
|
+
* resolves each qualifier against the producer file before deciding whether
|
|
50
|
+
* the runtime class is project-owned or external. Recording the full
|
|
51
|
+
* qualifier is what makes aliases such as
|
|
52
|
+
* `Event = Union[asyncio.Event, trio.Event]` usable without treating the
|
|
53
|
+
* annotation's unresolved terminal name as type identity.
|
|
54
|
+
*/
|
|
55
|
+
function extractReturnedConstructors(node) {
|
|
56
|
+
const body = node.childForFieldName('body');
|
|
57
|
+
if (!body) return null;
|
|
58
|
+
const constructors = [];
|
|
59
|
+
let incomplete = false;
|
|
60
|
+
const unwrapValue = value => {
|
|
61
|
+
let current = value;
|
|
62
|
+
while (current && ['parenthesized_expression', 'await'].includes(current.type) &&
|
|
63
|
+
current.namedChildCount === 1) {
|
|
64
|
+
current = current.namedChild(0);
|
|
65
|
+
}
|
|
66
|
+
return current;
|
|
67
|
+
};
|
|
68
|
+
const stack = [body];
|
|
69
|
+
while (stack.length > 0) {
|
|
70
|
+
const current = stack.pop();
|
|
71
|
+
if (current !== body && PY_FUNCTION_SCOPE_NODES.has(current.type)) continue;
|
|
72
|
+
if (current.type === 'class_definition') continue;
|
|
73
|
+
if (current.type === 'return_statement') {
|
|
74
|
+
const value = unwrapValue(current.namedChild(0));
|
|
75
|
+
if (value?.type !== 'call') {
|
|
76
|
+
incomplete = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const callable = value.childForFieldName('function');
|
|
80
|
+
let type, qualifier;
|
|
81
|
+
if (callable?.type === 'identifier') {
|
|
82
|
+
type = callable.text;
|
|
83
|
+
} else if (callable?.type === 'attribute') {
|
|
84
|
+
type = callable.childForFieldName('attribute')?.text;
|
|
85
|
+
qualifier = callable.childForFieldName('object')?.text;
|
|
86
|
+
}
|
|
87
|
+
if (!type || !/^[A-Z]/.test(type) || (qualifier && !/^[\w.]+$/.test(qualifier))) {
|
|
88
|
+
incomplete = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
constructors.push({ type, ...(qualifier && { qualifier }) });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
for (let i = current.namedChildCount - 1; i >= 0; i--) {
|
|
95
|
+
stack.push(current.namedChild(i));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (incomplete || constructors.length === 0 ||
|
|
99
|
+
new Set(constructors.map(item => item.type)).size !== 1) return null;
|
|
100
|
+
return constructors;
|
|
101
|
+
}
|
|
102
|
+
|
|
41
103
|
/**
|
|
42
104
|
* Find the actual def line (not decorator) for docstring extraction
|
|
43
105
|
*/
|
|
@@ -110,6 +172,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
110
172
|
const endLine = node.endPosition.row + 1;
|
|
111
173
|
const indent = getIndent(node, code);
|
|
112
174
|
const returnType = extractReturnType(node);
|
|
175
|
+
const returnedConstructors = extractReturnedConstructors(node);
|
|
113
176
|
const defLine = getDefLine(node);
|
|
114
177
|
const docstring = extractPythonDocstring(lines, defLine);
|
|
115
178
|
|
|
@@ -135,6 +198,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
135
198
|
isAsync,
|
|
136
199
|
modifiers: isAsync ? ['async'] : [],
|
|
137
200
|
...(returnType && { returnType }),
|
|
201
|
+
...(returnedConstructors && { returnedConstructors }),
|
|
138
202
|
...(paramTypes && { paramTypes }),
|
|
139
203
|
...(docstring && { docstring }),
|
|
140
204
|
...(decorators.length > 0 && { decorators }),
|
|
@@ -317,30 +381,64 @@ function extractDecorators(node) {
|
|
|
317
381
|
|
|
318
382
|
|
|
319
383
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
384
|
+
* Return the runtime/type-identity head of a Python alias expression.
|
|
385
|
+
* `typing.Dict[str, int]` and `dict[str, int]` both alias `dict`; a project
|
|
386
|
+
* generic such as `Page[T]` aliases `Page`. Unions deliberately have no
|
|
387
|
+
* single identity and therefore return null.
|
|
388
|
+
*/
|
|
389
|
+
const PY_ALIAS_RUNTIME_TYPES = new Map([
|
|
390
|
+
['Dict', 'dict'], ['List', 'list'], ['Set', 'set'], ['Tuple', 'tuple'],
|
|
391
|
+
['FrozenSet', 'frozenset'], ['Text', 'str'],
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
function pythonAliasBase(node) {
|
|
395
|
+
let current = unwrapTypeNode(node);
|
|
396
|
+
while (current?.type === 'parenthesized_expression' &&
|
|
397
|
+
current.namedChildCount === 1) {
|
|
398
|
+
current = unwrapTypeNode(current.namedChild(0));
|
|
399
|
+
}
|
|
400
|
+
if (!current) return null;
|
|
401
|
+
if (current.type === 'binary_operator') return null;
|
|
402
|
+
if (current.type === 'subscript' || current.type === 'generic_type') {
|
|
403
|
+
const parts = genericTypeParts(current);
|
|
404
|
+
if (!parts?.base) return null;
|
|
405
|
+
return PY_ALIAS_RUNTIME_TYPES.get(parts.base) || parts.base;
|
|
406
|
+
}
|
|
407
|
+
const base = typeNameFromExpr(current);
|
|
408
|
+
return base ? (PY_ALIAS_RUNTIME_TYPES.get(base) || base) : null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Python type aliases: PEP 695 `type X = int`, annotated
|
|
413
|
+
* `X: TypeAlias = ...`, and module-scope static aliases such as
|
|
414
|
+
* `Scope = typing.Dict[str, Any]` become `type` symbols. The last form is
|
|
415
|
+
* accepted only for conventional type-style names and generic type
|
|
416
|
+
* expressions, avoiding ordinary runtime assignments.
|
|
323
417
|
*/
|
|
324
418
|
function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
325
419
|
let name;
|
|
326
|
-
let
|
|
420
|
+
let valueNode;
|
|
327
421
|
if (node.type === 'type_alias_statement') {
|
|
328
422
|
// grammar shape: type <left> = <right>
|
|
329
423
|
const left = node.namedChild(0);
|
|
330
424
|
const right = node.namedChild(1);
|
|
331
425
|
if (!left) return false;
|
|
332
426
|
name = left.text.replace(/\[.*\]$/, ''); // strip PEP 695 type params
|
|
333
|
-
|
|
427
|
+
valueNode = right || null;
|
|
334
428
|
} else if (node.type === 'expression_statement') {
|
|
335
429
|
const child = node.namedChild(0);
|
|
336
430
|
if (!child || child.type !== 'assignment') return false;
|
|
337
431
|
const typeNode = child.childForFieldName('type');
|
|
338
|
-
if (!typeNode || !/\bTypeAlias\b/.test(typeNode.text)) return false;
|
|
339
432
|
const leftNode = child.childForFieldName('left');
|
|
340
433
|
const rightNode = child.childForFieldName('right');
|
|
341
434
|
if (!leftNode || leftNode.type !== 'identifier') return false;
|
|
435
|
+
const explicit = !!typeNode && /\bTypeAlias\b/.test(typeNode.text);
|
|
436
|
+
const implicit = !typeNode && node.parent?.type === 'module' &&
|
|
437
|
+
/^[A-Z][A-Za-z0-9_]*$/.test(leftNode.text) &&
|
|
438
|
+
['subscript', 'generic_type'].includes(unwrapTypeNode(rightNode)?.type);
|
|
439
|
+
if (!explicit && !implicit) return false;
|
|
342
440
|
name = leftNode.text;
|
|
343
|
-
|
|
441
|
+
valueNode = rightNode || null;
|
|
344
442
|
} else {
|
|
345
443
|
return false;
|
|
346
444
|
}
|
|
@@ -349,6 +447,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
|
349
447
|
if (processedRanges.has(rangeKey)) return true;
|
|
350
448
|
processedRanges.add(rangeKey);
|
|
351
449
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
450
|
+
const aliasOf = pythonAliasBase(valueNode);
|
|
352
451
|
classes.push({
|
|
353
452
|
name,
|
|
354
453
|
type: 'type',
|
|
@@ -356,7 +455,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
|
356
455
|
endLine,
|
|
357
456
|
methods: [],
|
|
358
457
|
members: [],
|
|
359
|
-
...(
|
|
458
|
+
...(aliasOf && { aliasOf }),
|
|
360
459
|
});
|
|
361
460
|
return true;
|
|
362
461
|
}
|
|
@@ -464,6 +563,7 @@ function extractClassMembers(classNode, code) {
|
|
|
464
563
|
|
|
465
564
|
const isAsync = funcNode.text.trimStart().startsWith('async ');
|
|
466
565
|
const returnType = extractReturnType(funcNode);
|
|
566
|
+
const returnedConstructors = extractReturnedConstructors(funcNode);
|
|
467
567
|
const defLine = getDefLine(funcNode);
|
|
468
568
|
const docstring = extractPythonDocstring(code, defLine);
|
|
469
569
|
// nameLine: where the name identifier lives (differs from startLine when decorated)
|
|
@@ -483,6 +583,7 @@ function extractClassMembers(classNode, code) {
|
|
|
483
583
|
// Match top-level Python functions: `async def` → ['async'] modifiers.
|
|
484
584
|
modifiers: isAsync ? ['async'] : [],
|
|
485
585
|
...(returnType && { returnType }),
|
|
586
|
+
...(returnedConstructors && { returnedConstructors }),
|
|
486
587
|
...(paramTypes && { paramTypes }),
|
|
487
588
|
...(docstring && { docstring }),
|
|
488
589
|
...(memberDecorators.length > 0 && { decorators: memberDecorators }),
|
|
@@ -585,6 +686,263 @@ function typeNameFromAnnotation(typeNode) {
|
|
|
585
686
|
return typeNameFromExpr(inner);
|
|
586
687
|
}
|
|
587
688
|
|
|
689
|
+
function typeNamesFromAnnotation(typeNode) {
|
|
690
|
+
if (!typeNode) return [];
|
|
691
|
+
const inner = typeNode.namedChildCount > 0 ? typeNode.namedChild(0) : null;
|
|
692
|
+
const collect = node => {
|
|
693
|
+
if (!node) return [];
|
|
694
|
+
if (node.type === 'binary_operator' && node.text.includes('|')) {
|
|
695
|
+
return [...collect(node.namedChild(0)), ...collect(node.namedChild(1))];
|
|
696
|
+
}
|
|
697
|
+
if (node.type === 'identifier') return node.text === 'None' ? [] : [node.text];
|
|
698
|
+
if (node.type === 'none') return [];
|
|
699
|
+
if (node.type === 'attribute') {
|
|
700
|
+
const attr = node.childForFieldName('attribute');
|
|
701
|
+
return attr ? [attr.text] : [];
|
|
702
|
+
}
|
|
703
|
+
return [];
|
|
704
|
+
};
|
|
705
|
+
return [...new Set(collect(inner))];
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function unwrapTypeNode(node) {
|
|
709
|
+
let current = node;
|
|
710
|
+
while (current?.type === 'type' && current.namedChildCount === 1) {
|
|
711
|
+
current = current.namedChild(0);
|
|
712
|
+
}
|
|
713
|
+
return current;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function genericTypeParts(node) {
|
|
717
|
+
const current = unwrapTypeNode(node);
|
|
718
|
+
if (!current || !['generic_type', 'subscript'].includes(current.type)) return null;
|
|
719
|
+
const baseNode = current.type === 'subscript'
|
|
720
|
+
? (current.childForFieldName('value') || current.namedChild(0))
|
|
721
|
+
: current.namedChild(0);
|
|
722
|
+
const base = typeNameFromExpr(baseNode);
|
|
723
|
+
if (!base) return null;
|
|
724
|
+
let args;
|
|
725
|
+
if (current.type === 'subscript') {
|
|
726
|
+
args = [];
|
|
727
|
+
for (let i = 0; i < current.namedChildCount; i++) {
|
|
728
|
+
const child = current.namedChild(i);
|
|
729
|
+
if (child.id !== baseNode.id) args.push(unwrapTypeNode(child));
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
const params = current.namedChild(1);
|
|
733
|
+
args = [];
|
|
734
|
+
if (params) {
|
|
735
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
736
|
+
args.push(unwrapTypeNode(params.namedChild(i)));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return { base, args };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const PY_ITERABLE_ANNOTATIONS = new Set([
|
|
744
|
+
'list', 'set', 'tuple', 'Iterable', 'Iterator', 'Sequence',
|
|
745
|
+
'Collection', 'Generator', 'List', 'Set', 'Tuple',
|
|
746
|
+
]);
|
|
747
|
+
|
|
748
|
+
function iterableBindingTypes(typeNode) {
|
|
749
|
+
const outer = genericTypeParts(typeNode);
|
|
750
|
+
if (!outer || !PY_ITERABLE_ANNOTATIONS.has(outer.base) ||
|
|
751
|
+
outer.args.length === 0) return [];
|
|
752
|
+
const item = outer.args[0];
|
|
753
|
+
const tuple = genericTypeParts(item);
|
|
754
|
+
if (['tuple', 'Tuple'].includes(tuple?.base) && tuple.args.length > 0) {
|
|
755
|
+
return tuple.args.map(arg => typeNameFromExpr(unwrapTypeNode(arg)));
|
|
756
|
+
}
|
|
757
|
+
const typeName = typeNameFromExpr(unwrapTypeNode(item));
|
|
758
|
+
return typeName ? [typeName] : [];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function patternIdentifiers(node, out = []) {
|
|
762
|
+
if (!node) return out;
|
|
763
|
+
if (node.type === 'identifier') {
|
|
764
|
+
out.push(node.text);
|
|
765
|
+
return out;
|
|
766
|
+
}
|
|
767
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
768
|
+
patternIdentifiers(node.namedChild(i), out);
|
|
769
|
+
}
|
|
770
|
+
return out;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function enclosingPythonClassName(node) {
|
|
774
|
+
for (let current = node; current; current = current.parent) {
|
|
775
|
+
if (current.type === 'class_definition') {
|
|
776
|
+
return current.childForFieldName('name')?.text;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function explicitInstanceFieldContracts(tree, parser) {
|
|
783
|
+
const result = new Map();
|
|
784
|
+
const parseCommentType = comment => {
|
|
785
|
+
const text = comment?.text?.trim();
|
|
786
|
+
const prefix = '# type:';
|
|
787
|
+
if (!text?.startsWith(prefix)) return null;
|
|
788
|
+
const annotation = text.slice(prefix.length).trim();
|
|
789
|
+
if (!annotation) return null;
|
|
790
|
+
const annotationTree = parseTree(parser, `_value: ${annotation}`);
|
|
791
|
+
const statement = annotationTree.rootNode.namedChild(0);
|
|
792
|
+
const assignment = statement?.namedChild(0);
|
|
793
|
+
return assignment?.childForFieldName('type') || null;
|
|
794
|
+
};
|
|
795
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
796
|
+
if (node.type !== 'expression_statement') return true;
|
|
797
|
+
const assignment = node.namedChild(0);
|
|
798
|
+
if (assignment?.type !== 'assignment') return true;
|
|
799
|
+
const left = assignment.childForFieldName('left');
|
|
800
|
+
if (left?.type !== 'attribute' ||
|
|
801
|
+
left.childForFieldName('object')?.text !== 'self') return true;
|
|
802
|
+
const field = left.childForFieldName('attribute')?.text;
|
|
803
|
+
const className = enclosingPythonClassName(node);
|
|
804
|
+
if (!field || !className) return true;
|
|
805
|
+
let typeNode = assignment.childForFieldName('type');
|
|
806
|
+
if (!typeNode) {
|
|
807
|
+
const parent = node.parent;
|
|
808
|
+
for (let i = 0; parent && i < parent.namedChildCount; i++) {
|
|
809
|
+
const sibling = parent.namedChild(i);
|
|
810
|
+
if (sibling.type === 'comment' &&
|
|
811
|
+
sibling.startPosition.row === node.endPosition.row) {
|
|
812
|
+
typeNode = parseCommentType(sibling);
|
|
813
|
+
if (typeNode) break;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (!typeNode) return true;
|
|
818
|
+
const type = typeNameFromAnnotation(typeNode);
|
|
819
|
+
const itemTypes = iterableBindingTypes(typeNode);
|
|
820
|
+
if (!type && itemTypes.length === 0) return true;
|
|
821
|
+
if (!result.has(className)) result.set(className, new Map());
|
|
822
|
+
result.get(className).set(field, {
|
|
823
|
+
...(type && { type }),
|
|
824
|
+
...(itemTypes.length > 0 && { itemTypes }),
|
|
825
|
+
});
|
|
826
|
+
return true;
|
|
827
|
+
});
|
|
828
|
+
return result;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function comprehensionReceiverType(
|
|
832
|
+
refNode, receiverName, iterableTypes, callableIterableTypes = new Map(),
|
|
833
|
+
instanceFieldContracts = new Map()) {
|
|
834
|
+
for (let current = refNode?.parent; current; current = current.parent) {
|
|
835
|
+
if (['generator_expression', 'list_comprehension',
|
|
836
|
+
'set_comprehension', 'dictionary_comprehension'].includes(current.type)) {
|
|
837
|
+
for (let i = 0; i < current.namedChildCount; i++) {
|
|
838
|
+
const clause = current.namedChild(i);
|
|
839
|
+
if (clause.type !== 'for_in_clause') continue;
|
|
840
|
+
const names = patternIdentifiers(clause.childForFieldName('left'));
|
|
841
|
+
const index = names.indexOf(receiverName);
|
|
842
|
+
const iterable = clause.childForFieldName('right');
|
|
843
|
+
const types = iterableTypesFromExpr(
|
|
844
|
+
iterable, iterableTypes, callableIterableTypes,
|
|
845
|
+
instanceFieldContracts, clause);
|
|
846
|
+
if (index >= 0 && types?.[index]) return types[index];
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (current.type === 'function_definition' ||
|
|
850
|
+
current.type === 'async_function_definition' ||
|
|
851
|
+
current.type === 'lambda') break;
|
|
852
|
+
}
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function iterableTypesFromExpr(
|
|
857
|
+
node, iterableTypes, callableIterableTypes,
|
|
858
|
+
instanceFieldContracts = new Map(), contextNode = node) {
|
|
859
|
+
const current = unwrapTypeNode(node);
|
|
860
|
+
if (!current) return null;
|
|
861
|
+
if (current.type === 'identifier') return iterableTypes.get(current.text) || null;
|
|
862
|
+
if (current.type === 'call') {
|
|
863
|
+
const fn = current.childForFieldName('function');
|
|
864
|
+
if (fn?.type === 'identifier') return callableIterableTypes.get(fn.text) || null;
|
|
865
|
+
}
|
|
866
|
+
if (current.type === 'attribute' &&
|
|
867
|
+
current.childForFieldName('object')?.text === 'self') {
|
|
868
|
+
const field = current.childForFieldName('attribute')?.text;
|
|
869
|
+
const className = enclosingPythonClassName(contextNode);
|
|
870
|
+
return instanceFieldContracts.get(className)?.get(field)?.itemTypes || null;
|
|
871
|
+
}
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function nodeContains(ancestor, node) {
|
|
876
|
+
return !!ancestor && !!node &&
|
|
877
|
+
node.startIndex >= ancestor.startIndex && node.endIndex <= ancestor.endIndex;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function isinstanceTypes(condition, receiverName) {
|
|
881
|
+
if (!condition || condition.type !== 'call') return [];
|
|
882
|
+
const fn = condition.childForFieldName('function');
|
|
883
|
+
const args = condition.childForFieldName('arguments');
|
|
884
|
+
if (fn?.type !== 'identifier' || fn.text !== 'isinstance' ||
|
|
885
|
+
!args || args.namedChildCount < 2) return [];
|
|
886
|
+
const value = args.namedChild(0);
|
|
887
|
+
const typeExpr = args.namedChild(1);
|
|
888
|
+
if (value?.type !== 'identifier' || value.text !== receiverName) return [];
|
|
889
|
+
const extract = node => {
|
|
890
|
+
if (!node) return [];
|
|
891
|
+
if (node.type === 'identifier') return [node.text];
|
|
892
|
+
if (node.type === 'attribute') {
|
|
893
|
+
const attr = node.childForFieldName('attribute');
|
|
894
|
+
return attr ? [attr.text] : [];
|
|
895
|
+
}
|
|
896
|
+
if (node.type === 'tuple') {
|
|
897
|
+
const out = [];
|
|
898
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
899
|
+
out.push(...extract(node.namedChild(i)));
|
|
900
|
+
}
|
|
901
|
+
return out;
|
|
902
|
+
}
|
|
903
|
+
return [];
|
|
904
|
+
};
|
|
905
|
+
return [...new Set(extract(typeExpr))];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function narrowedReceiverType(refNode, receiverName, declaredUnion) {
|
|
909
|
+
for (let current = refNode; current?.parent; current = current.parent) {
|
|
910
|
+
const parent = current.parent;
|
|
911
|
+
if (parent.type === 'if_statement') {
|
|
912
|
+
const condition = parent.childForFieldName('condition');
|
|
913
|
+
const positive = isinstanceTypes(condition, receiverName);
|
|
914
|
+
if (positive.length === 0) continue;
|
|
915
|
+
const consequence = parent.childForFieldName('consequence');
|
|
916
|
+
const alternative = parent.childForFieldName('alternative');
|
|
917
|
+
if (nodeContains(consequence, refNode) && positive.length === 1) {
|
|
918
|
+
return positive[0];
|
|
919
|
+
}
|
|
920
|
+
if (nodeContains(alternative, refNode) && declaredUnion?.length) {
|
|
921
|
+
const remaining = declaredUnion.filter(type => !positive.includes(type));
|
|
922
|
+
if (remaining.length === 1) return remaining[0];
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (parent.type === 'conditional_expression' &&
|
|
926
|
+
parent.namedChildCount >= 3) {
|
|
927
|
+
const consequence = parent.namedChild(0);
|
|
928
|
+
const condition = parent.namedChild(1);
|
|
929
|
+
const alternative = parent.namedChild(2);
|
|
930
|
+
const positive = isinstanceTypes(condition, receiverName);
|
|
931
|
+
if (nodeContains(consequence, refNode) && positive.length === 1) {
|
|
932
|
+
return positive[0];
|
|
933
|
+
}
|
|
934
|
+
if (nodeContains(alternative, refNode) && declaredUnion?.length) {
|
|
935
|
+
const remaining = declaredUnion.filter(type => !positive.includes(type));
|
|
936
|
+
if (remaining.length === 1) return remaining[0];
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (parent.type === 'function_definition' ||
|
|
940
|
+
parent.type === 'async_function_definition' ||
|
|
941
|
+
parent.type === 'lambda') break;
|
|
942
|
+
}
|
|
943
|
+
return undefined;
|
|
944
|
+
}
|
|
945
|
+
|
|
588
946
|
function typeNameFromExpr(node) {
|
|
589
947
|
if (!node) return undefined;
|
|
590
948
|
switch (node.type) {
|
|
@@ -595,6 +953,9 @@ function typeNameFromExpr(node) {
|
|
|
595
953
|
const attr = node.childForFieldName('attribute');
|
|
596
954
|
return attr?.text;
|
|
597
955
|
}
|
|
956
|
+
case 'parenthesized_expression':
|
|
957
|
+
return node.namedChildCount === 1
|
|
958
|
+
? typeNameFromExpr(node.namedChild(0)) : undefined;
|
|
598
959
|
case 'binary_operator': {
|
|
599
960
|
// PEP 604 union: X | None → X; unions of two real types are ambiguous
|
|
600
961
|
const left = node.namedChild(0);
|
|
@@ -654,6 +1015,18 @@ function assignmentTargetOf(callNode) {
|
|
|
654
1015
|
return undefined;
|
|
655
1016
|
}
|
|
656
1017
|
|
|
1018
|
+
function contextTargetOf(callNode) {
|
|
1019
|
+
const pattern = callNode?.parent;
|
|
1020
|
+
if (pattern?.type !== 'as_pattern' ||
|
|
1021
|
+
pattern.namedChild(0)?.id !== callNode.id) return undefined;
|
|
1022
|
+
const target = pattern.namedChild(pattern.namedChildCount - 1);
|
|
1023
|
+
if (target?.type === 'as_pattern_target') {
|
|
1024
|
+
const identifier = target.namedChild(0);
|
|
1025
|
+
return identifier?.type === 'identifier' ? identifier.text : undefined;
|
|
1026
|
+
}
|
|
1027
|
+
return target?.type === 'identifier' ? target.text : undefined;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
657
1030
|
/**
|
|
658
1031
|
* Type identity hint from a constructor-call callee: ClassName(...) or
|
|
659
1032
|
* pkg.ClassName(...). Preserve the qualifier: dropping `threading` from
|
|
@@ -677,14 +1050,161 @@ function constructorTypeInfo(funcNode) {
|
|
|
677
1050
|
return undefined;
|
|
678
1051
|
}
|
|
679
1052
|
|
|
1053
|
+
function attributeReceiverPath(node) {
|
|
1054
|
+
if (!node || node.type !== 'attribute') return null;
|
|
1055
|
+
const fields = [];
|
|
1056
|
+
let current = node;
|
|
1057
|
+
while (current?.type === 'attribute') {
|
|
1058
|
+
const attribute = current.childForFieldName('attribute');
|
|
1059
|
+
if (!attribute) return null;
|
|
1060
|
+
fields.unshift(attribute.text);
|
|
1061
|
+
current = current.childForFieldName('object');
|
|
1062
|
+
}
|
|
1063
|
+
return current?.type === 'identifier' && fields.length > 0
|
|
1064
|
+
? { root: current.text, fields }
|
|
1065
|
+
: null;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function iterableAttributeSource(node, localVarTypes) {
|
|
1069
|
+
const current = unwrapTypeNode(node);
|
|
1070
|
+
const path = attributeReceiverPath(current);
|
|
1071
|
+
if (!path) return null;
|
|
1072
|
+
return {
|
|
1073
|
+
root: path.root,
|
|
1074
|
+
fields: path.fields,
|
|
1075
|
+
...(localVarTypes.get(path.root) && {
|
|
1076
|
+
rootType: localVarTypes.get(path.root),
|
|
1077
|
+
}),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function literalStringValue(node) {
|
|
1082
|
+
if (node?.type !== 'string') return null;
|
|
1083
|
+
let value = '';
|
|
1084
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1085
|
+
const child = node.namedChild(i);
|
|
1086
|
+
if (child.type === 'interpolation') return null;
|
|
1087
|
+
if (child.type === 'string_content') value += child.text;
|
|
1088
|
+
}
|
|
1089
|
+
return value;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Return the method name when a method call is dominated by a positive,
|
|
1094
|
+
* receiver-exact capability check:
|
|
1095
|
+
*
|
|
1096
|
+
* if hasattr(self._stream, "aread"):
|
|
1097
|
+
* await self._stream.aread(...)
|
|
1098
|
+
*
|
|
1099
|
+
* This is not type evidence—the runtime object may implement any matching
|
|
1100
|
+
* contract—but it is useful provenance for the visible dispatch tier. Keep
|
|
1101
|
+
* the recognizer deliberately narrow: a direct hasattr call or an AND term in
|
|
1102
|
+
* the if/elif condition. OR and NOT do not guarantee the capability on entry.
|
|
1103
|
+
*/
|
|
1104
|
+
function receiverCapabilityGuard(callNode, receiverNode, methodName) {
|
|
1105
|
+
if (!callNode || !receiverNode || !methodName) return undefined;
|
|
1106
|
+
const receiverText = receiverNode.text;
|
|
1107
|
+
|
|
1108
|
+
const unwrap = node => {
|
|
1109
|
+
let current = node;
|
|
1110
|
+
while (current?.type === 'parenthesized_expression' &&
|
|
1111
|
+
current.namedChildCount === 1) {
|
|
1112
|
+
current = current.namedChild(0);
|
|
1113
|
+
}
|
|
1114
|
+
return current;
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
const matchesHasattr = node => {
|
|
1118
|
+
const current = unwrap(node);
|
|
1119
|
+
if (current?.type !== 'call') return false;
|
|
1120
|
+
const fn = current.childForFieldName('function');
|
|
1121
|
+
if (fn?.type !== 'identifier' || fn.text !== 'hasattr') return false;
|
|
1122
|
+
const args = current.childForFieldName('arguments');
|
|
1123
|
+
if (!args || args.namedChildCount < 2) return false;
|
|
1124
|
+
return args.namedChild(0)?.text === receiverText &&
|
|
1125
|
+
literalStringValue(args.namedChild(1)) === methodName;
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
const positivelyRequiresCapability = node => {
|
|
1129
|
+
const current = unwrap(node);
|
|
1130
|
+
if (matchesHasattr(current)) return true;
|
|
1131
|
+
if (current?.type !== 'boolean_operator') return false;
|
|
1132
|
+
let hasAnd = false;
|
|
1133
|
+
for (let i = 0; i < current.childCount; i++) {
|
|
1134
|
+
const child = current.child(i);
|
|
1135
|
+
if (!child.isNamed && child.type === 'and') hasAnd = true;
|
|
1136
|
+
if (!child.isNamed && child.type === 'or') return false;
|
|
1137
|
+
}
|
|
1138
|
+
return hasAnd && Array.from(
|
|
1139
|
+
{ length: current.namedChildCount },
|
|
1140
|
+
(_, i) => current.namedChild(i)
|
|
1141
|
+
).some(positivelyRequiresCapability);
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
let child = callNode;
|
|
1145
|
+
for (let current = callNode.parent; current; child = current, current = current.parent) {
|
|
1146
|
+
if (current.type === 'function_definition' || current.type === 'lambda') break;
|
|
1147
|
+
if (current.type !== 'if_statement' && current.type !== 'elif_clause') continue;
|
|
1148
|
+
const consequence = current.childForFieldName('consequence');
|
|
1149
|
+
if (!consequence || child.id !== consequence.id) continue;
|
|
1150
|
+
const condition = current.childForFieldName('condition') || current.namedChild(0);
|
|
1151
|
+
if (positivelyRequiresCapability(condition)) return methodName;
|
|
1152
|
+
}
|
|
1153
|
+
return undefined;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function pickleRoundTripSource(node) {
|
|
1157
|
+
if (node?.type !== 'call') return null;
|
|
1158
|
+
const loads = node.childForFieldName('function');
|
|
1159
|
+
if (loads?.type !== 'attribute' ||
|
|
1160
|
+
loads.childForFieldName('object')?.text !== 'pickle' ||
|
|
1161
|
+
loads.childForFieldName('attribute')?.text !== 'loads') return null;
|
|
1162
|
+
const loadsArgs = node.childForFieldName('arguments');
|
|
1163
|
+
const dumped = loadsArgs?.namedChild(0);
|
|
1164
|
+
if (dumped?.type !== 'call') return null;
|
|
1165
|
+
const dumps = dumped.childForFieldName('function');
|
|
1166
|
+
if (dumps?.type !== 'attribute' ||
|
|
1167
|
+
dumps.childForFieldName('object')?.text !== 'pickle' ||
|
|
1168
|
+
dumps.childForFieldName('attribute')?.text !== 'dumps') return null;
|
|
1169
|
+
const value = dumped.childForFieldName('arguments')?.namedChild(0);
|
|
1170
|
+
return value?.type === 'identifier' ? value.text : null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
680
1173
|
function findCallsInCode(code, parser) {
|
|
681
1174
|
const tree = parseTree(parser, code);
|
|
682
1175
|
const calls = [];
|
|
1176
|
+
const instanceFieldContracts = explicitInstanceFieldContracts(tree, parser);
|
|
1177
|
+
// Same-file callable return contracts are a closed type source for loop
|
|
1178
|
+
// bindings: `for item in parse_items()` can use
|
|
1179
|
+
// `parse_items() -> list[Item]` without guessing about external code.
|
|
1180
|
+
const callableIterableTypes = new Map();
|
|
1181
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
1182
|
+
if (node.type !== 'function_definition') return true;
|
|
1183
|
+
const name = node.childForFieldName('name')?.text;
|
|
1184
|
+
const returnType = node.childForFieldName('return_type');
|
|
1185
|
+
const itemTypes = iterableBindingTypes(returnType);
|
|
1186
|
+
if (!name || itemTypes.length === 0) return true;
|
|
1187
|
+
const previous = callableIterableTypes.get(name);
|
|
1188
|
+
if (!previous) callableIterableTypes.set(name, itemTypes);
|
|
1189
|
+
else if (previous.join('\0') !== itemTypes.join('\0')) {
|
|
1190
|
+
// Overloads/redefinitions must agree before their result can type
|
|
1191
|
+
// a receiver.
|
|
1192
|
+
callableIterableTypes.delete(name);
|
|
1193
|
+
}
|
|
1194
|
+
return true;
|
|
1195
|
+
});
|
|
683
1196
|
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
684
1197
|
const aliases = new Map(); // Track local aliases: aliasName -> originalName
|
|
685
1198
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
686
1199
|
const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
1200
|
+
const declaredVarTypes = new Map(); // Compiler-checked annotations survive later assignments
|
|
687
1201
|
const localVarTypeQualifiers = new Map(); // varName -> imported module alias that owns the inferred type
|
|
1202
|
+
const localVarUnionTypes = new Map(); // varName -> concrete PEP 604 alternatives
|
|
1203
|
+
const localIterableTypes = new Map(); // iterable binding -> loop-variable types
|
|
1204
|
+
const localIterationSources = new Map(); // loop variable -> declared iterable path + tuple index
|
|
1205
|
+
const localDictValueTypes = new Map(); // local dict -> exact string-key value types
|
|
1206
|
+
const localVarStdlibContracts = new Map(); // variable -> stdlib module proving its type flow
|
|
1207
|
+
const assignmentRhsReceiverTypes = new Map(); // call-node id -> pre-assignment receiver type
|
|
688
1208
|
const constructedReceiverVars = new Set(); // exact constructor-result bindings
|
|
689
1209
|
const withBindingVars = new Set(); // names produced by a context-manager as-target
|
|
690
1210
|
// Member-access aliases (fix #218): `append = output.append` makes a later
|
|
@@ -698,7 +1218,13 @@ function findCallsInCode(code, parser) {
|
|
|
698
1218
|
const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
|
|
699
1219
|
const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
|
|
700
1220
|
const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
|
|
1221
|
+
const declaredVarTypesStack = [];
|
|
701
1222
|
const localVarTypeQualifiersStack = [];
|
|
1223
|
+
const localVarUnionTypesStack = [];
|
|
1224
|
+
const localIterableTypesStack = [];
|
|
1225
|
+
const localIterationSourcesStack = [];
|
|
1226
|
+
const localDictValueTypesStack = [];
|
|
1227
|
+
const localVarStdlibContractsStack = [];
|
|
702
1228
|
const constructedReceiverVarsStack = [];
|
|
703
1229
|
const withBindingVarsStack = [];
|
|
704
1230
|
|
|
@@ -779,9 +1305,11 @@ function findCallsInCode(code, parser) {
|
|
|
779
1305
|
|
|
780
1306
|
// Helper to get current enclosing function
|
|
781
1307
|
const getCurrentEnclosingFunction = () => {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1308
|
+
if (functionStack.length === 0) return null;
|
|
1309
|
+
return {
|
|
1310
|
+
...functionStack[functionStack.length - 1],
|
|
1311
|
+
scopeChain: functionStack.map(scope => scope.startLine),
|
|
1312
|
+
};
|
|
785
1313
|
};
|
|
786
1314
|
|
|
787
1315
|
// fix #203: is a bare-identifier function REFERENCE shadowed by a local of
|
|
@@ -894,14 +1422,29 @@ function findCallsInCode(code, parser) {
|
|
|
894
1422
|
if (node.parent && node.parent.type === 'decorated_definition') {
|
|
895
1423
|
startLine = node.parent.startPosition.row + 1;
|
|
896
1424
|
}
|
|
1425
|
+
const returnParts = genericTypeParts(
|
|
1426
|
+
node.childForFieldName('return_type'));
|
|
1427
|
+
const generatorSendType =
|
|
1428
|
+
['Generator', 'AsyncGenerator'].includes(returnParts?.base) &&
|
|
1429
|
+
returnParts.args.length > 1
|
|
1430
|
+
? typeNameFromExpr(returnParts.args[1]) : undefined;
|
|
897
1431
|
functionStack.push({
|
|
898
1432
|
name: extractFunctionName(node),
|
|
899
1433
|
startLine,
|
|
900
|
-
endLine: node.endPosition.row + 1
|
|
1434
|
+
endLine: node.endPosition.row + 1,
|
|
1435
|
+
...(generatorSendType && { generatorSendType }),
|
|
901
1436
|
});
|
|
902
1437
|
// Save localVarTypes so inner declarations don't leak to sibling functions
|
|
903
1438
|
localVarTypesStack.push(new Map(localVarTypes));
|
|
1439
|
+
declaredVarTypesStack.push(new Map(declaredVarTypes));
|
|
904
1440
|
localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
|
|
1441
|
+
localVarUnionTypesStack.push(new Map(localVarUnionTypes));
|
|
1442
|
+
localIterableTypesStack.push(new Map(localIterableTypes));
|
|
1443
|
+
localIterationSourcesStack.push(new Map(localIterationSources));
|
|
1444
|
+
localDictValueTypesStack.push(new Map(
|
|
1445
|
+
[...localDictValueTypes].map(([name, values]) =>
|
|
1446
|
+
[name, new Map(values)])));
|
|
1447
|
+
localVarStdlibContractsStack.push(new Map(localVarStdlibContracts));
|
|
905
1448
|
constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
|
|
906
1449
|
withBindingVarsStack.push(new Set(withBindingVars));
|
|
907
1450
|
memberAliasesStack.push(new Map(memberAliases));
|
|
@@ -910,12 +1453,59 @@ function findCallsInCode(code, parser) {
|
|
|
910
1453
|
// Track parameter type annotations: def foo(x: Foo) → x is Foo
|
|
911
1454
|
if (node.type === 'typed_parameter' || node.type === 'typed_default_parameter') {
|
|
912
1455
|
// typed_default_parameter has 'name' field; typed_parameter does not — use namedChild(0)
|
|
913
|
-
|
|
1456
|
+
let nameNode = node.childForFieldName('name') || node.namedChild(0);
|
|
1457
|
+
const parameterPattern = nameNode;
|
|
1458
|
+
if (nameNode && ['dictionary_splat_pattern', 'list_splat_pattern']
|
|
1459
|
+
.includes(nameNode.type)) {
|
|
1460
|
+
nameNode = nameNode.namedChild(0);
|
|
1461
|
+
}
|
|
914
1462
|
const typeNode = node.childForFieldName('type');
|
|
915
1463
|
if (nameNode?.type === 'identifier' && typeNode) {
|
|
916
1464
|
const typeName = typeNameFromAnnotation(typeNode);
|
|
917
|
-
|
|
918
|
-
|
|
1465
|
+
const unionTypes = typeNamesFromAnnotation(typeNode);
|
|
1466
|
+
const itemTypes = iterableBindingTypes(typeNode);
|
|
1467
|
+
const receiverType = parameterPattern?.type === 'dictionary_splat_pattern'
|
|
1468
|
+
? 'dict'
|
|
1469
|
+
: (parameterPattern?.type === 'list_splat_pattern' ? 'tuple' : typeName);
|
|
1470
|
+
if (receiverType && !['self', 'cls'].includes(nameNode.text)) {
|
|
1471
|
+
localVarTypes.set(nameNode.text, receiverType);
|
|
1472
|
+
declaredVarTypes.set(nameNode.text, receiverType);
|
|
1473
|
+
}
|
|
1474
|
+
if (unionTypes.length > 1) localVarUnionTypes.set(nameNode.text, unionTypes);
|
|
1475
|
+
else localVarUnionTypes.delete(nameNode.text);
|
|
1476
|
+
if (itemTypes.length > 0) localIterableTypes.set(nameNode.text, itemTypes);
|
|
1477
|
+
else localIterableTypes.delete(nameNode.text);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Compiler-declared iterable element flow for ordinary for-loops.
|
|
1482
|
+
// Tuple destructuring is positional: `for name, value in
|
|
1483
|
+
// headers:`, where headers is list[tuple[bytes, bytes]], types both
|
|
1484
|
+
// receivers as bytes. Comprehensions are handled at the call site
|
|
1485
|
+
// because their result expression precedes the for-clause in the AST.
|
|
1486
|
+
if (node.type === 'for_statement') {
|
|
1487
|
+
const left = node.childForFieldName('left');
|
|
1488
|
+
const right = node.childForFieldName('right');
|
|
1489
|
+
const itemTypes = iterableTypesFromExpr(
|
|
1490
|
+
right, localIterableTypes, callableIterableTypes,
|
|
1491
|
+
instanceFieldContracts, node);
|
|
1492
|
+
if (itemTypes?.length) {
|
|
1493
|
+
const names = patternIdentifiers(left);
|
|
1494
|
+
if (names.length === itemTypes.length) {
|
|
1495
|
+
for (let i = 0; i < names.length; i++) {
|
|
1496
|
+
if (itemTypes[i]) localVarTypes.set(names[i], itemTypes[i]);
|
|
1497
|
+
}
|
|
1498
|
+
} else if (names.length === 1 && itemTypes.length === 1) {
|
|
1499
|
+
localVarTypes.set(names[0], itemTypes[0]);
|
|
1500
|
+
}
|
|
1501
|
+
} else {
|
|
1502
|
+
const source = iterableAttributeSource(right, localVarTypes);
|
|
1503
|
+
const names = patternIdentifiers(left);
|
|
1504
|
+
for (let i = 0; source && i < names.length; i++) {
|
|
1505
|
+
localIterationSources.set(names[i], {
|
|
1506
|
+
...source,
|
|
1507
|
+
index: i,
|
|
1508
|
+
});
|
|
919
1509
|
}
|
|
920
1510
|
}
|
|
921
1511
|
}
|
|
@@ -944,6 +1534,13 @@ function findCallsInCode(code, parser) {
|
|
|
944
1534
|
} else {
|
|
945
1535
|
localVarTypeQualifiers.delete(targetId.text);
|
|
946
1536
|
}
|
|
1537
|
+
} else if (ctx.childForFieldName('function')?.type === 'identifier' &&
|
|
1538
|
+
ctx.childForFieldName('function').text === 'open' &&
|
|
1539
|
+
!isShadowedByLocal(ctx.childForFieldName('function'), 'open')) {
|
|
1540
|
+
// Builtin open() is its own context value and always
|
|
1541
|
+
// yields an IO object. The exact text/binary subtype
|
|
1542
|
+
// is irrelevant for method-owner exclusion.
|
|
1543
|
+
localVarTypes.set(targetId.text, 'IO');
|
|
947
1544
|
}
|
|
948
1545
|
}
|
|
949
1546
|
}
|
|
@@ -954,13 +1551,29 @@ function findCallsInCode(code, parser) {
|
|
|
954
1551
|
const left = node.childForFieldName('left');
|
|
955
1552
|
const right = node.childForFieldName('right');
|
|
956
1553
|
if (left?.type === 'identifier') {
|
|
1554
|
+
const previousType = localVarTypes.get(left.text);
|
|
1555
|
+
if (previousType && right?.type === 'call') {
|
|
1556
|
+
const rightFunction = right.childForFieldName('function');
|
|
1557
|
+
if (rightFunction?.type === 'attribute' &&
|
|
1558
|
+
rightFunction.childForFieldName('object')?.type === 'identifier' &&
|
|
1559
|
+
rightFunction.childForFieldName('object').text === left.text) {
|
|
1560
|
+
assignmentRhsReceiverTypes.set(right.id, previousType);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
957
1563
|
// Track type annotation: x: Foo = ... → x is Foo
|
|
958
1564
|
const typeNode = node.childForFieldName('type');
|
|
959
1565
|
if (typeNode) {
|
|
960
1566
|
const typeName = typeNameFromAnnotation(typeNode);
|
|
1567
|
+
const unionTypes = typeNamesFromAnnotation(typeNode);
|
|
1568
|
+
const itemTypes = iterableBindingTypes(typeNode);
|
|
961
1569
|
if (typeName) {
|
|
962
1570
|
localVarTypes.set(left.text, typeName);
|
|
1571
|
+
declaredVarTypes.set(left.text, typeName);
|
|
963
1572
|
}
|
|
1573
|
+
if (unionTypes.length > 1) localVarUnionTypes.set(left.text, unionTypes);
|
|
1574
|
+
else localVarUnionTypes.delete(left.text);
|
|
1575
|
+
if (itemTypes.length > 0) localIterableTypes.set(left.text, itemTypes);
|
|
1576
|
+
else localIterableTypes.delete(left.text);
|
|
964
1577
|
}
|
|
965
1578
|
memberAliases.delete(left.text); // any assignment rebinds the name
|
|
966
1579
|
constructedReceiverVars.delete(left.text);
|
|
@@ -972,11 +1585,59 @@ function findCallsInCode(code, parser) {
|
|
|
972
1585
|
if (!typeNode) {
|
|
973
1586
|
localVarTypes.delete(left.text);
|
|
974
1587
|
localVarTypeQualifiers.delete(left.text);
|
|
1588
|
+
localVarUnionTypes.delete(left.text);
|
|
1589
|
+
localIterableTypes.delete(left.text);
|
|
1590
|
+
localIterationSources.delete(left.text);
|
|
1591
|
+
localDictValueTypes.delete(left.text);
|
|
1592
|
+
localVarStdlibContracts.delete(left.text);
|
|
1593
|
+
// Python assignments remain constrained by a variable or
|
|
1594
|
+
// parameter annotation. Constructor/literal inference is
|
|
1595
|
+
// nearest-assignment only, but a declared contract is
|
|
1596
|
+
// valid for every subsequent assignment in type-correct
|
|
1597
|
+
// code (`boundary: bytes | None; boundary = make()`).
|
|
1598
|
+
const declaredType = declaredVarTypes.get(left.text);
|
|
1599
|
+
if (declaredType) localVarTypes.set(left.text, declaredType);
|
|
975
1600
|
} else {
|
|
976
1601
|
// An annotation is the authoritative type source; a
|
|
977
1602
|
// previous constructor qualifier must not survive it.
|
|
978
1603
|
localVarTypeQualifiers.delete(left.text);
|
|
979
1604
|
}
|
|
1605
|
+
// Preserve a declared collection contract through the common
|
|
1606
|
+
// normalization idiom `x = {} if x is None else x`. The
|
|
1607
|
+
// literal branch is a concrete implementation of the prior
|
|
1608
|
+
// protocol; retaining that protocol is compiler-safe and
|
|
1609
|
+
// keeps later method dispatch out of unrelated project
|
|
1610
|
+
// classes.
|
|
1611
|
+
if (!typeNode && previousType &&
|
|
1612
|
+
right?.type === 'conditional_expression') {
|
|
1613
|
+
const consequence = right.namedChild(0);
|
|
1614
|
+
const alternative = right.namedChild(2);
|
|
1615
|
+
const selfBranch = [consequence, alternative].find(
|
|
1616
|
+
branch => branch?.type === 'identifier' &&
|
|
1617
|
+
branch.text === left.text);
|
|
1618
|
+
const otherBranch = selfBranch === consequence
|
|
1619
|
+
? alternative : consequence;
|
|
1620
|
+
const literalType = otherBranch
|
|
1621
|
+
? PY_LITERAL_RECEIVER_TYPES[otherBranch.type] : null;
|
|
1622
|
+
const compatible = {
|
|
1623
|
+
Mapping: new Set(['dict']),
|
|
1624
|
+
MutableMapping: new Set(['dict']),
|
|
1625
|
+
Sequence: new Set(['list', 'tuple', 'str', 'bytes']),
|
|
1626
|
+
MutableSequence: new Set(['list']),
|
|
1627
|
+
Collection: new Set(['list', 'tuple', 'set', 'dict', 'str', 'bytes']),
|
|
1628
|
+
Iterable: new Set(['list', 'tuple', 'set', 'dict', 'str', 'bytes']),
|
|
1629
|
+
};
|
|
1630
|
+
if (selfBranch && literalType &&
|
|
1631
|
+
(previousType === literalType ||
|
|
1632
|
+
compatible[previousType]?.has(literalType))) {
|
|
1633
|
+
localVarTypes.set(left.text, previousType);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (!typeNode && right?.type === 'yield') {
|
|
1637
|
+
const sendType = functionStack[
|
|
1638
|
+
functionStack.length - 1]?.generatorSendType;
|
|
1639
|
+
if (sendType) localVarTypes.set(left.text, sendType);
|
|
1640
|
+
}
|
|
980
1641
|
// Literal assignment types the variable (fix #218):
|
|
981
1642
|
// ansi_bytes = b"…" → bytes; out = [] → list. Compiler-true,
|
|
982
1643
|
// same trust grade as literal receivers ({}.get() → dict).
|
|
@@ -985,6 +1646,29 @@ function findCallsInCode(code, parser) {
|
|
|
985
1646
|
if (litType === 'str' && /^[rRuU]*[bB]/.test(right.text)) litType = 'bytes';
|
|
986
1647
|
localVarTypes.set(left.text, litType);
|
|
987
1648
|
}
|
|
1649
|
+
if (right?.type === 'dictionary') {
|
|
1650
|
+
const valueTypes = new Map();
|
|
1651
|
+
for (let i = 0; i < right.namedChildCount; i++) {
|
|
1652
|
+
const pair = right.namedChild(i);
|
|
1653
|
+
if (pair.type !== 'pair') continue;
|
|
1654
|
+
const key = literalStringValue(pair.childForFieldName('key'));
|
|
1655
|
+
const value = pair.childForFieldName('value');
|
|
1656
|
+
const valueType = value?.type === 'identifier'
|
|
1657
|
+
? localVarTypes.get(value.text)
|
|
1658
|
+
: PY_LITERAL_RECEIVER_TYPES[value?.type];
|
|
1659
|
+
if (key != null && valueType) valueTypes.set(key, valueType);
|
|
1660
|
+
}
|
|
1661
|
+
if (valueTypes.size > 0) {
|
|
1662
|
+
localDictValueTypes.set(left.text, valueTypes);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
const roundTripSource = pickleRoundTripSource(right);
|
|
1666
|
+
const roundTripType = roundTripSource
|
|
1667
|
+
? localVarTypes.get(roundTripSource) : null;
|
|
1668
|
+
if (roundTripType && moduleAliases.has('pickle')) {
|
|
1669
|
+
localVarTypes.set(left.text, roundTripType);
|
|
1670
|
+
localVarStdlibContracts.set(left.text, 'pickle');
|
|
1671
|
+
}
|
|
988
1672
|
if (right?.type === 'identifier') {
|
|
989
1673
|
aliases.set(left.text, right.text);
|
|
990
1674
|
}
|
|
@@ -1068,7 +1752,8 @@ function findCallsInCode(code, parser) {
|
|
|
1068
1752
|
|
|
1069
1753
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1070
1754
|
let uncertain = false;
|
|
1071
|
-
const
|
|
1755
|
+
const assignedContext = contextTargetOf(node);
|
|
1756
|
+
const assignedTo = assignmentTargetOf(node) || assignedContext;
|
|
1072
1757
|
|
|
1073
1758
|
// Call-site arg count (positional + keyword) for arity pruning.
|
|
1074
1759
|
// *args/**kwargs splats make the count open-ended — flag them so
|
|
@@ -1108,6 +1793,7 @@ function findCallsInCode(code, parser) {
|
|
|
1108
1793
|
...(recvType && { receiverType: recvType }),
|
|
1109
1794
|
...(recvIsModule && { receiverIsModule: true }),
|
|
1110
1795
|
...(assignedTo && { assignedTo }),
|
|
1796
|
+
...(assignedContext && { assignedContext: true }),
|
|
1111
1797
|
argCount,
|
|
1112
1798
|
...(argSpread && { argSpread: true }),
|
|
1113
1799
|
enclosingFunction,
|
|
@@ -1123,6 +1809,7 @@ function findCallsInCode(code, parser) {
|
|
|
1123
1809
|
line: node.startPosition.row + 1,
|
|
1124
1810
|
isMethod: false,
|
|
1125
1811
|
...(assignedTo && { assignedTo }),
|
|
1812
|
+
...(assignedContext && { assignedContext: true }),
|
|
1126
1813
|
argCount,
|
|
1127
1814
|
...(argSpread && { argSpread: true }),
|
|
1128
1815
|
enclosingFunction,
|
|
@@ -1139,6 +1826,7 @@ function findCallsInCode(code, parser) {
|
|
|
1139
1826
|
if (attrNode) {
|
|
1140
1827
|
let receiver = objNode?.type === 'identifier' ? objNode.text : undefined;
|
|
1141
1828
|
let selfAttribute = undefined;
|
|
1829
|
+
const receiverPath = attributeReceiverPath(objNode);
|
|
1142
1830
|
// Chained receiver (fix #219): the receiver IS a call —
|
|
1143
1831
|
// fetch_data().json() — record the producer so findCallers
|
|
1144
1832
|
// can type the receiver from its declared return
|
|
@@ -1156,10 +1844,17 @@ function findCallsInCode(code, parser) {
|
|
|
1156
1844
|
}
|
|
1157
1845
|
{
|
|
1158
1846
|
let recvNode = objNode;
|
|
1159
|
-
|
|
1160
|
-
recvNode.
|
|
1847
|
+
while (recvNode?.type === 'parenthesized_expression' &&
|
|
1848
|
+
recvNode.namedChildCount === 1) {
|
|
1849
|
+
recvNode = recvNode.namedChild(0);
|
|
1850
|
+
}
|
|
1851
|
+
if (recvNode?.type === 'await') {
|
|
1161
1852
|
receiverCallAwaited = true;
|
|
1162
|
-
recvNode = recvNode.namedChild(0)
|
|
1853
|
+
recvNode = recvNode.namedChild(0);
|
|
1854
|
+
while (recvNode?.type === 'parenthesized_expression' &&
|
|
1855
|
+
recvNode.namedChildCount === 1) {
|
|
1856
|
+
recvNode = recvNode.namedChild(0);
|
|
1857
|
+
}
|
|
1163
1858
|
}
|
|
1164
1859
|
if (recvNode?.type === 'call' && receiver !== 'super') {
|
|
1165
1860
|
const prodFunc = recvNode.childForFieldName('function');
|
|
@@ -1196,17 +1891,63 @@ function findCallsInCode(code, parser) {
|
|
|
1196
1891
|
|
|
1197
1892
|
// Literal receivers carry their builtin type: {}.get() can
|
|
1198
1893
|
// never be a project class method
|
|
1894
|
+
let subscriptReceiverType;
|
|
1895
|
+
if (objNode?.type === 'subscript') {
|
|
1896
|
+
const base = objNode.childForFieldName('value');
|
|
1897
|
+
const key = literalStringValue(
|
|
1898
|
+
objNode.childForFieldName('subscript'));
|
|
1899
|
+
if (base?.type === 'identifier' && key != null) {
|
|
1900
|
+
subscriptReceiverType =
|
|
1901
|
+
localDictValueTypes.get(base.text)?.get(key);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1199
1904
|
const receiverType = receiver
|
|
1200
|
-
?
|
|
1201
|
-
|
|
1905
|
+
? (narrowedReceiverType(
|
|
1906
|
+
objNode, receiver, localVarUnionTypes.get(receiver)) ||
|
|
1907
|
+
comprehensionReceiverType(
|
|
1908
|
+
objNode, receiver, localIterableTypes,
|
|
1909
|
+
callableIterableTypes, instanceFieldContracts) ||
|
|
1910
|
+
localVarTypes.get(receiver))
|
|
1911
|
+
|| assignmentRhsReceiverTypes.get(node.id)
|
|
1912
|
+
: (subscriptReceiverType ||
|
|
1913
|
+
(objNode ? PY_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
|
|
1914
|
+
let iterationSource = receiver
|
|
1915
|
+
? localIterationSources.get(receiver) : null;
|
|
1916
|
+
if (receiver && !iterationSource) {
|
|
1917
|
+
for (let current = objNode?.parent; current; current = current.parent) {
|
|
1918
|
+
if (['generator_expression', 'list_comprehension',
|
|
1919
|
+
'set_comprehension', 'dictionary_comprehension']
|
|
1920
|
+
.includes(current.type)) {
|
|
1921
|
+
for (let i = 0; i < current.namedChildCount; i++) {
|
|
1922
|
+
const clause = current.namedChild(i);
|
|
1923
|
+
if (clause.type !== 'for_in_clause') continue;
|
|
1924
|
+
const names = patternIdentifiers(
|
|
1925
|
+
clause.childForFieldName('left'));
|
|
1926
|
+
const index = names.indexOf(receiver);
|
|
1927
|
+
if (index < 0) continue;
|
|
1928
|
+
const source = iterableAttributeSource(
|
|
1929
|
+
clause.childForFieldName('right'),
|
|
1930
|
+
localVarTypes);
|
|
1931
|
+
if (source) iterationSource = {
|
|
1932
|
+
...source, index,
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (isFunctionNode(current)) break;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1202
1939
|
const receiverTypeQualifier = receiver
|
|
1203
1940
|
? localVarTypeQualifiers.get(receiver)
|
|
1204
1941
|
: undefined;
|
|
1942
|
+
const receiverRootType = receiverPath
|
|
1943
|
+
? localVarTypes.get(receiverPath.root) : undefined;
|
|
1205
1944
|
// Module receiver (httpx.get()) — unless locally shadowed
|
|
1206
1945
|
// by a typed instance binding
|
|
1207
1946
|
const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
|
|
1208
1947
|
!localVarTypes.has(receiver);
|
|
1209
1948
|
const firstArg = getFirstStringArg(node);
|
|
1949
|
+
const capabilityGuard = receiverCapabilityGuard(
|
|
1950
|
+
node, objNode, attrNode.text);
|
|
1210
1951
|
calls.push({
|
|
1211
1952
|
name: attrNode.text,
|
|
1212
1953
|
// Multi-line chains (obj.x()\n.y()) must report each
|
|
@@ -1216,18 +1957,40 @@ function findCallsInCode(code, parser) {
|
|
|
1216
1957
|
isMethod: true,
|
|
1217
1958
|
receiver,
|
|
1218
1959
|
...(receiverType && { receiverType }),
|
|
1960
|
+
...(receiver && localVarStdlibContracts.has(receiver) && {
|
|
1961
|
+
receiverTypeStdlibModule:
|
|
1962
|
+
localVarStdlibContracts.get(receiver),
|
|
1963
|
+
}),
|
|
1219
1964
|
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
1220
1965
|
...(receiver && constructedReceiverVars.has(receiver) && { receiverConstructed: true }),
|
|
1221
1966
|
...(receiver && withBindingVars.has(receiver) && { receiverWithBinding: true }),
|
|
1222
1967
|
...(receiverIsModule && { receiverIsModule: true }),
|
|
1223
1968
|
...(receiver && objNode?.type === 'identifier' &&
|
|
1224
1969
|
isShadowedByLocal(objNode, receiver) && { receiverLocalBinding: true }),
|
|
1970
|
+
...(receiverPath && {
|
|
1971
|
+
receiverRoot: receiverPath.root,
|
|
1972
|
+
receiverField: receiverPath.fields[receiverPath.fields.length - 1],
|
|
1973
|
+
receiverFields: receiverPath.fields,
|
|
1974
|
+
}),
|
|
1975
|
+
...(receiverRootType && { receiverRootType }),
|
|
1976
|
+
...(iterationSource && {
|
|
1977
|
+
receiverIterationRoot: iterationSource.root,
|
|
1978
|
+
receiverIterationFields: iterationSource.fields,
|
|
1979
|
+
receiverIterationIndex: iterationSource.index,
|
|
1980
|
+
...(iterationSource.rootType && {
|
|
1981
|
+
receiverIterationRootType: iterationSource.rootType,
|
|
1982
|
+
}),
|
|
1983
|
+
}),
|
|
1225
1984
|
...(selfAttribute && { selfAttribute }),
|
|
1226
1985
|
...(receiverCall && { receiverCall }),
|
|
1227
1986
|
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1228
1987
|
...(receiverCallAwaited && { receiverCallAwaited: true }),
|
|
1229
1988
|
...(receiverCallLine && { receiverCallLine }),
|
|
1989
|
+
...(capabilityGuard && {
|
|
1990
|
+
receiverCapabilityGuard: capabilityGuard,
|
|
1991
|
+
}),
|
|
1230
1992
|
...(assignedTo && { assignedTo }),
|
|
1993
|
+
...(assignedContext && { assignedContext: true }),
|
|
1231
1994
|
argCount,
|
|
1232
1995
|
...(argSpread && { argSpread: true }),
|
|
1233
1996
|
enclosingFunction,
|
|
@@ -1299,11 +2062,47 @@ function findCallsInCode(code, parser) {
|
|
|
1299
2062
|
localVarTypes.clear();
|
|
1300
2063
|
for (const [k, v] of saved) localVarTypes.set(k, v);
|
|
1301
2064
|
}
|
|
2065
|
+
const savedDeclared = declaredVarTypesStack.pop();
|
|
2066
|
+
if (savedDeclared) {
|
|
2067
|
+
declaredVarTypes.clear();
|
|
2068
|
+
for (const [k, v] of savedDeclared) declaredVarTypes.set(k, v);
|
|
2069
|
+
}
|
|
1302
2070
|
const savedQualifiers = localVarTypeQualifiersStack.pop();
|
|
1303
2071
|
if (savedQualifiers) {
|
|
1304
2072
|
localVarTypeQualifiers.clear();
|
|
1305
2073
|
for (const [k, v] of savedQualifiers) localVarTypeQualifiers.set(k, v);
|
|
1306
2074
|
}
|
|
2075
|
+
const savedUnions = localVarUnionTypesStack.pop();
|
|
2076
|
+
if (savedUnions) {
|
|
2077
|
+
localVarUnionTypes.clear();
|
|
2078
|
+
for (const [k, v] of savedUnions) localVarUnionTypes.set(k, v);
|
|
2079
|
+
}
|
|
2080
|
+
const savedIterables = localIterableTypesStack.pop();
|
|
2081
|
+
if (savedIterables) {
|
|
2082
|
+
localIterableTypes.clear();
|
|
2083
|
+
for (const [k, v] of savedIterables) localIterableTypes.set(k, v);
|
|
2084
|
+
}
|
|
2085
|
+
const savedIterationSources = localIterationSourcesStack.pop();
|
|
2086
|
+
if (savedIterationSources) {
|
|
2087
|
+
localIterationSources.clear();
|
|
2088
|
+
for (const [k, v] of savedIterationSources) {
|
|
2089
|
+
localIterationSources.set(k, v);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
const savedDictValueTypes = localDictValueTypesStack.pop();
|
|
2093
|
+
if (savedDictValueTypes) {
|
|
2094
|
+
localDictValueTypes.clear();
|
|
2095
|
+
for (const [name, values] of savedDictValueTypes) {
|
|
2096
|
+
localDictValueTypes.set(name, values);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
const savedStdlibContracts = localVarStdlibContractsStack.pop();
|
|
2100
|
+
if (savedStdlibContracts) {
|
|
2101
|
+
localVarStdlibContracts.clear();
|
|
2102
|
+
for (const [name, moduleName] of savedStdlibContracts) {
|
|
2103
|
+
localVarStdlibContracts.set(name, moduleName);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
1307
2106
|
const savedConstructed = constructedReceiverVarsStack.pop();
|
|
1308
2107
|
constructedReceiverVars.clear();
|
|
1309
2108
|
if (savedConstructed) for (const name of savedConstructed) constructedReceiverVars.add(name);
|
|
@@ -1471,6 +2270,33 @@ function findExportsInCode(code, parser) {
|
|
|
1471
2270
|
const exports = [];
|
|
1472
2271
|
|
|
1473
2272
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2273
|
+
// PEP 484 explicit re-export idiom:
|
|
2274
|
+
// `from .core import public_thing as public_thing`. The redundant
|
|
2275
|
+
// alias is deliberate compiler/tooling evidence that the imported
|
|
2276
|
+
// name belongs to this module's public surface even without __all__.
|
|
2277
|
+
if (node.type === 'import_from_statement') {
|
|
2278
|
+
let source = '';
|
|
2279
|
+
for (let index = 0; index < node.namedChildCount; index++) {
|
|
2280
|
+
const child = node.namedChild(index);
|
|
2281
|
+
if (!source && (child.type === 'dotted_name' ||
|
|
2282
|
+
child.type === 'relative_import')) {
|
|
2283
|
+
source = child.text;
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
if (child.type !== 'aliased_import') continue;
|
|
2287
|
+
const imported = child.namedChild(0);
|
|
2288
|
+
const alias = child.namedChild(1);
|
|
2289
|
+
if (imported?.text && alias?.text === imported.text) {
|
|
2290
|
+
exports.push({
|
|
2291
|
+
name: alias.text,
|
|
2292
|
+
type: 're-export',
|
|
2293
|
+
source,
|
|
2294
|
+
line: node.startPosition.row + 1,
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return true;
|
|
2299
|
+
}
|
|
1474
2300
|
// Look for __all__ = [...]
|
|
1475
2301
|
if (node.type === 'expression_statement') {
|
|
1476
2302
|
const child = node.namedChild(0);
|
|
@@ -1634,9 +2460,10 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
1634
2460
|
* @param {object} parser - Tree-sitter parser instance
|
|
1635
2461
|
* @returns {Map<string, Map<string, string>>} className -> (attrName -> typeName)
|
|
1636
2462
|
*/
|
|
1637
|
-
function findInstanceAttributeTypes(code, parser) {
|
|
2463
|
+
function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
1638
2464
|
const tree = parseTree(parser, code);
|
|
1639
2465
|
const result = new Map(); // className -> Map(attrName -> typeName)
|
|
2466
|
+
const explicitContracts = explicitInstanceFieldContracts(tree, parser);
|
|
1640
2467
|
|
|
1641
2468
|
const PRIMITIVE_TYPES = new Set(['int', 'float', 'str', 'bool', 'bytes', 'list', 'dict', 'set', 'tuple', 'None', 'Any', 'object']);
|
|
1642
2469
|
|
|
@@ -1651,6 +2478,23 @@ function findInstanceAttributeTypes(code, parser) {
|
|
|
1651
2478
|
if (!body) return false;
|
|
1652
2479
|
|
|
1653
2480
|
const attrTypes = new Map();
|
|
2481
|
+
for (const [field, contract] of explicitContracts.get(className) || []) {
|
|
2482
|
+
if (contract.type) attrTypes.set(field, contract.type);
|
|
2483
|
+
}
|
|
2484
|
+
const methodReturns = new Map();
|
|
2485
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2486
|
+
let member = body.namedChild(i);
|
|
2487
|
+
if (member.type === 'decorated_definition') {
|
|
2488
|
+
member = Array.from({ length: member.namedChildCount },
|
|
2489
|
+
(_, index) => member.namedChild(index))
|
|
2490
|
+
.find(child => child.type === 'function_definition') || member;
|
|
2491
|
+
}
|
|
2492
|
+
if (member.type !== 'function_definition') continue;
|
|
2493
|
+
const methodName = member.childForFieldName('name')?.text;
|
|
2494
|
+
const returnType = typeNameFromAnnotation(
|
|
2495
|
+
member.childForFieldName('return_type'));
|
|
2496
|
+
if (methodName && returnType) methodReturns.set(methodName, returnType);
|
|
2497
|
+
}
|
|
1654
2498
|
|
|
1655
2499
|
// Scan annotated class-level fields: name: Type [= ...]. Originally
|
|
1656
2500
|
// @dataclass-only (fix #28); a BARE class-body annotation is the
|
|
@@ -1758,6 +2602,19 @@ function findInstanceAttributeTypes(code, parser) {
|
|
|
1758
2602
|
const typeName = extractConstructorName(rhs);
|
|
1759
2603
|
if (typeName) {
|
|
1760
2604
|
attrTypes.set(attrName, typeName);
|
|
2605
|
+
} else if (rhs.type === 'call') {
|
|
2606
|
+
const fn = rhs.childForFieldName('function');
|
|
2607
|
+
if (fn?.type === 'attribute') {
|
|
2608
|
+
const moduleName = fn.childForFieldName('object')?.text;
|
|
2609
|
+
const functionName = fn.childForFieldName('attribute')?.text;
|
|
2610
|
+
const methodType = moduleName === 'self' && functionName
|
|
2611
|
+
? methodReturns.get(functionName) : null;
|
|
2612
|
+
const builtinType = moduleName && functionName &&
|
|
2613
|
+
options.resolveBuiltinCallType?.(moduleName, functionName);
|
|
2614
|
+
if (methodType || builtinType) {
|
|
2615
|
+
attrTypes.set(attrName, methodType || builtinType);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
1761
2618
|
} else if (rhs.type === 'identifier' && paramTypes.has(rhs.text)) {
|
|
1762
2619
|
// self.X = param where param has type annotation
|
|
1763
2620
|
attrTypes.set(attrName, paramTypes.get(rhs.text));
|
|
@@ -1767,6 +2624,63 @@ function findInstanceAttributeTypes(code, parser) {
|
|
|
1767
2624
|
});
|
|
1768
2625
|
}
|
|
1769
2626
|
|
|
2627
|
+
// Stable external/runtime constructors assigned outside __init__ are
|
|
2628
|
+
// still exact field evidence when every observed contract agrees.
|
|
2629
|
+
// This covers lifecycle-owned fields such as
|
|
2630
|
+
// `self.ready = asyncio.Event()` in serve(), without generalizing the
|
|
2631
|
+
// old constructor-name heuristic to arbitrary methods. Unknown or
|
|
2632
|
+
// conflicting assignments leave the field untyped.
|
|
2633
|
+
const runtimeFieldTypes = new Map();
|
|
2634
|
+
const invalidRuntimeFields = new Set();
|
|
2635
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2636
|
+
let member = body.namedChild(i);
|
|
2637
|
+
if (member.type === 'decorated_definition') {
|
|
2638
|
+
member = Array.from({ length: member.namedChildCount },
|
|
2639
|
+
(_, index) => member.namedChild(index))
|
|
2640
|
+
.find(child => child.type === 'function_definition') || member;
|
|
2641
|
+
}
|
|
2642
|
+
if (member.type !== 'function_definition') continue;
|
|
2643
|
+
const memberBody = member.childForFieldName('body');
|
|
2644
|
+
if (!memberBody) continue;
|
|
2645
|
+
traverseTree(memberBody, stmt => {
|
|
2646
|
+
if (stmt.type !== 'expression_statement') return true;
|
|
2647
|
+
const assignment = stmt.firstChild;
|
|
2648
|
+
if (assignment?.type !== 'assignment') return true;
|
|
2649
|
+
const left = assignment.childForFieldName('left');
|
|
2650
|
+
const field = left?.type === 'attribute' &&
|
|
2651
|
+
left.childForFieldName('object')?.text === 'self'
|
|
2652
|
+
? left.childForFieldName('attribute')?.text : null;
|
|
2653
|
+
if (!field) return true;
|
|
2654
|
+
const right = assignment.childForFieldName('right');
|
|
2655
|
+
const callable = right?.type === 'call'
|
|
2656
|
+
? right.childForFieldName('function') : null;
|
|
2657
|
+
const moduleName = callable?.type === 'attribute'
|
|
2658
|
+
? callable.childForFieldName('object')?.text : null;
|
|
2659
|
+
const functionName = callable?.type === 'attribute'
|
|
2660
|
+
? callable.childForFieldName('attribute')?.text : null;
|
|
2661
|
+
const runtimeType = moduleName && functionName
|
|
2662
|
+
? options.resolveBuiltinCallType?.(moduleName, functionName)
|
|
2663
|
+
: null;
|
|
2664
|
+
if (!runtimeType) {
|
|
2665
|
+
// None is a harmless uninitialized state; any other
|
|
2666
|
+
// unknown write could replace the field with a project
|
|
2667
|
+
// value and therefore invalidates exclusion-grade flow.
|
|
2668
|
+
if (right?.type !== 'none') invalidRuntimeFields.add(field);
|
|
2669
|
+
return true;
|
|
2670
|
+
}
|
|
2671
|
+
if (!runtimeFieldTypes.has(field)) runtimeFieldTypes.set(field, new Set());
|
|
2672
|
+
runtimeFieldTypes.get(field).add(runtimeType);
|
|
2673
|
+
return true;
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
for (const [field, types] of runtimeFieldTypes) {
|
|
2677
|
+
if (invalidRuntimeFields.has(field) || types.size !== 1) continue;
|
|
2678
|
+
const type = [...types][0];
|
|
2679
|
+
if (!attrTypes.has(field) || attrTypes.get(field) === type) {
|
|
2680
|
+
attrTypes.set(field, type);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
|
|
1770
2684
|
if (attrTypes.size > 0) {
|
|
1771
2685
|
result.set(className, attrTypes);
|
|
1772
2686
|
}
|
|
@@ -1866,6 +2780,32 @@ function isEntryPoint(symbol) {
|
|
|
1866
2780
|
return getEntryPointKind(symbol) !== null;
|
|
1867
2781
|
}
|
|
1868
2782
|
|
|
2783
|
+
// Stable CPython/stdlib runtime contracts. These are intentionally small and
|
|
2784
|
+
// language-owned: callers must still prove that the import is external (not a
|
|
2785
|
+
// project module with the same name) before using them as exclusion evidence.
|
|
2786
|
+
const PY_BUILTIN_CALL_RETURNS = Object.freeze({
|
|
2787
|
+
'asyncio.Event': 'AsyncEvent',
|
|
2788
|
+
'base64.b64encode': 'bytes',
|
|
2789
|
+
'base64.b64decode': 'bytes',
|
|
2790
|
+
'json.dumps': 'str',
|
|
2791
|
+
'os.urandom': 'bytes',
|
|
2792
|
+
'urllib.request.getproxies': 'dict',
|
|
2793
|
+
'zlib.compressobj': 'ZlibCompress',
|
|
2794
|
+
'zlib.decompressobj': 'ZlibDecompress',
|
|
2795
|
+
});
|
|
2796
|
+
|
|
2797
|
+
const PY_BUILTIN_FIELD_TYPES = Object.freeze({
|
|
2798
|
+
'os.environ': 'dict',
|
|
2799
|
+
});
|
|
2800
|
+
|
|
2801
|
+
function getBuiltinCallReturnType(moduleName, functionName) {
|
|
2802
|
+
return PY_BUILTIN_CALL_RETURNS[`${moduleName}.${functionName}`] || null;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function getBuiltinFieldType(moduleName, fieldName) {
|
|
2806
|
+
return PY_BUILTIN_FIELD_TYPES[`${moduleName}.${fieldName}`] || null;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
1869
2809
|
module.exports = {
|
|
1870
2810
|
findFunctions,
|
|
1871
2811
|
findClasses,
|
|
@@ -1875,6 +2815,8 @@ module.exports = {
|
|
|
1875
2815
|
findExportsInCode,
|
|
1876
2816
|
findUsagesInCode,
|
|
1877
2817
|
findInstanceAttributeTypes,
|
|
2818
|
+
getBuiltinCallReturnType,
|
|
2819
|
+
getBuiltinFieldType,
|
|
1878
2820
|
isEntryPoint,
|
|
1879
2821
|
getEntryPointKind,
|
|
1880
2822
|
parse
|