ucn 4.2.2 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +445 -300
- 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 -131
- package/core/cache.js +533 -11
- package/core/callers.js +5533 -494
- 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 +421 -20
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +204 -42
- 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 +216 -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 -177
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +371 -116
- 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 +428 -16
- package/languages/javascript.js +452 -49
- package/languages/python.js +1041 -32
- package/languages/rust.js +1415 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +41 -24
- 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
|
|
@@ -671,20 +1044,169 @@ function constructorTypeInfo(funcNode) {
|
|
|
671
1044
|
const attr = funcNode.childForFieldName('attribute');
|
|
672
1045
|
const object = funcNode.childForFieldName('object');
|
|
673
1046
|
return attr && /^[A-Z]/.test(attr.text)
|
|
674
|
-
? { type: attr.text, qualifier: object?.
|
|
1047
|
+
? { type: attr.text, qualifier: object?.text || undefined }
|
|
675
1048
|
: undefined;
|
|
676
1049
|
}
|
|
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
|
|
1208
|
+
const constructedReceiverVars = new Set(); // exact constructor-result bindings
|
|
1209
|
+
const withBindingVars = new Set(); // names produced by a context-manager as-target
|
|
688
1210
|
// Member-access aliases (fix #218): `append = output.append` makes a later
|
|
689
1211
|
// bare `append(part)` a METHOD call on `output` — it must carry the
|
|
690
1212
|
// receiver's evidence, never bind by bare name to a same-file def
|
|
@@ -696,7 +1218,15 @@ function findCallsInCode(code, parser) {
|
|
|
696
1218
|
const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
|
|
697
1219
|
const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
|
|
698
1220
|
const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
|
|
1221
|
+
const declaredVarTypesStack = [];
|
|
699
1222
|
const localVarTypeQualifiersStack = [];
|
|
1223
|
+
const localVarUnionTypesStack = [];
|
|
1224
|
+
const localIterableTypesStack = [];
|
|
1225
|
+
const localIterationSourcesStack = [];
|
|
1226
|
+
const localDictValueTypesStack = [];
|
|
1227
|
+
const localVarStdlibContractsStack = [];
|
|
1228
|
+
const constructedReceiverVarsStack = [];
|
|
1229
|
+
const withBindingVarsStack = [];
|
|
700
1230
|
|
|
701
1231
|
// Helper: extract first string-arg literal from a call node.
|
|
702
1232
|
// Used by route extraction to capture path arg of requests.get('/users'), httpx.get('/users') etc.
|
|
@@ -775,9 +1305,11 @@ function findCallsInCode(code, parser) {
|
|
|
775
1305
|
|
|
776
1306
|
// Helper to get current enclosing function
|
|
777
1307
|
const getCurrentEnclosingFunction = () => {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1308
|
+
if (functionStack.length === 0) return null;
|
|
1309
|
+
return {
|
|
1310
|
+
...functionStack[functionStack.length - 1],
|
|
1311
|
+
scopeChain: functionStack.map(scope => scope.startLine),
|
|
1312
|
+
};
|
|
781
1313
|
};
|
|
782
1314
|
|
|
783
1315
|
// fix #203: is a bare-identifier function REFERENCE shadowed by a local of
|
|
@@ -848,6 +1380,16 @@ function findCallsInCode(code, parser) {
|
|
|
848
1380
|
}
|
|
849
1381
|
}
|
|
850
1382
|
if (p.type === 'function_definition' || p.type === 'async_function_definition') {
|
|
1383
|
+
const params = p.childForFieldName('parameters');
|
|
1384
|
+
if (params) {
|
|
1385
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1386
|
+
const prm = params.namedChild(i);
|
|
1387
|
+
const prmName = prm.type === 'identifier'
|
|
1388
|
+
? prm
|
|
1389
|
+
: (prm.childForFieldName('name') || prm.namedChild(0));
|
|
1390
|
+
if (prmName?.type === 'identifier' && prmName.text === name) return true;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
851
1393
|
const body = p.childForFieldName('body');
|
|
852
1394
|
return body ? _bindsNameInScope(body, name) : false;
|
|
853
1395
|
}
|
|
@@ -880,26 +1422,90 @@ function findCallsInCode(code, parser) {
|
|
|
880
1422
|
if (node.parent && node.parent.type === 'decorated_definition') {
|
|
881
1423
|
startLine = node.parent.startPosition.row + 1;
|
|
882
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;
|
|
883
1431
|
functionStack.push({
|
|
884
1432
|
name: extractFunctionName(node),
|
|
885
1433
|
startLine,
|
|
886
|
-
endLine: node.endPosition.row + 1
|
|
1434
|
+
endLine: node.endPosition.row + 1,
|
|
1435
|
+
...(generatorSendType && { generatorSendType }),
|
|
887
1436
|
});
|
|
888
1437
|
// Save localVarTypes so inner declarations don't leak to sibling functions
|
|
889
1438
|
localVarTypesStack.push(new Map(localVarTypes));
|
|
1439
|
+
declaredVarTypesStack.push(new Map(declaredVarTypes));
|
|
890
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));
|
|
1448
|
+
constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
|
|
1449
|
+
withBindingVarsStack.push(new Set(withBindingVars));
|
|
891
1450
|
memberAliasesStack.push(new Map(memberAliases));
|
|
892
1451
|
}
|
|
893
1452
|
|
|
894
1453
|
// Track parameter type annotations: def foo(x: Foo) → x is Foo
|
|
895
1454
|
if (node.type === 'typed_parameter' || node.type === 'typed_default_parameter') {
|
|
896
1455
|
// typed_default_parameter has 'name' field; typed_parameter does not — use namedChild(0)
|
|
897
|
-
|
|
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
|
+
}
|
|
898
1462
|
const typeNode = node.childForFieldName('type');
|
|
899
1463
|
if (nameNode?.type === 'identifier' && typeNode) {
|
|
900
1464
|
const typeName = typeNameFromAnnotation(typeNode);
|
|
901
|
-
|
|
902
|
-
|
|
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
|
+
});
|
|
903
1509
|
}
|
|
904
1510
|
}
|
|
905
1511
|
}
|
|
@@ -912,15 +1518,29 @@ function findCallsInCode(code, parser) {
|
|
|
912
1518
|
const ctx = value.namedChild(0);
|
|
913
1519
|
const target = value.namedChildCount > 1 ? value.namedChild(value.namedChildCount - 1) : null;
|
|
914
1520
|
const targetId = target?.type === 'as_pattern_target' ? target.namedChild(0) : null;
|
|
1521
|
+
const recordWithTargets = n => {
|
|
1522
|
+
if (!n) return;
|
|
1523
|
+
if (n.type === 'identifier') withBindingVars.add(n.text);
|
|
1524
|
+
for (let i = 0; i < n.namedChildCount; i++) recordWithTargets(n.namedChild(i));
|
|
1525
|
+
};
|
|
1526
|
+
recordWithTargets(targetId || target);
|
|
915
1527
|
if (ctx?.type === 'call' && targetId?.type === 'identifier') {
|
|
916
1528
|
const ctor = constructorTypeInfo(ctx.childForFieldName('function'));
|
|
917
1529
|
if (ctor) {
|
|
918
1530
|
localVarTypes.set(targetId.text, ctor.type);
|
|
919
|
-
|
|
1531
|
+
constructedReceiverVars.add(targetId.text);
|
|
1532
|
+
if (ctor.qualifier && moduleAliases.has(ctor.qualifier.split('.')[0])) {
|
|
920
1533
|
localVarTypeQualifiers.set(targetId.text, ctor.qualifier);
|
|
921
1534
|
} else {
|
|
922
1535
|
localVarTypeQualifiers.delete(targetId.text);
|
|
923
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');
|
|
924
1544
|
}
|
|
925
1545
|
}
|
|
926
1546
|
}
|
|
@@ -931,15 +1551,33 @@ function findCallsInCode(code, parser) {
|
|
|
931
1551
|
const left = node.childForFieldName('left');
|
|
932
1552
|
const right = node.childForFieldName('right');
|
|
933
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
|
+
}
|
|
934
1563
|
// Track type annotation: x: Foo = ... → x is Foo
|
|
935
1564
|
const typeNode = node.childForFieldName('type');
|
|
936
1565
|
if (typeNode) {
|
|
937
1566
|
const typeName = typeNameFromAnnotation(typeNode);
|
|
1567
|
+
const unionTypes = typeNamesFromAnnotation(typeNode);
|
|
1568
|
+
const itemTypes = iterableBindingTypes(typeNode);
|
|
938
1569
|
if (typeName) {
|
|
939
1570
|
localVarTypes.set(left.text, typeName);
|
|
1571
|
+
declaredVarTypes.set(left.text, typeName);
|
|
940
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);
|
|
941
1577
|
}
|
|
942
1578
|
memberAliases.delete(left.text); // any assignment rebinds the name
|
|
1579
|
+
constructedReceiverVars.delete(left.text);
|
|
1580
|
+
withBindingVars.delete(left.text);
|
|
943
1581
|
// Rebinding without a known type makes any previously inferred
|
|
944
1582
|
// type stale — nearest-preceding-assignment semantics (#199's
|
|
945
1583
|
// documented rule). Without this, `x = ""; x = render(); x.m()`
|
|
@@ -947,11 +1585,59 @@ function findCallsInCode(code, parser) {
|
|
|
947
1585
|
if (!typeNode) {
|
|
948
1586
|
localVarTypes.delete(left.text);
|
|
949
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);
|
|
950
1600
|
} else {
|
|
951
1601
|
// An annotation is the authoritative type source; a
|
|
952
1602
|
// previous constructor qualifier must not survive it.
|
|
953
1603
|
localVarTypeQualifiers.delete(left.text);
|
|
954
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
|
+
}
|
|
955
1641
|
// Literal assignment types the variable (fix #218):
|
|
956
1642
|
// ansi_bytes = b"…" → bytes; out = [] → list. Compiler-true,
|
|
957
1643
|
// same trust grade as literal receivers ({}.get() → dict).
|
|
@@ -960,6 +1646,29 @@ function findCallsInCode(code, parser) {
|
|
|
960
1646
|
if (litType === 'str' && /^[rRuU]*[bB]/.test(right.text)) litType = 'bytes';
|
|
961
1647
|
localVarTypes.set(left.text, litType);
|
|
962
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
|
+
}
|
|
963
1672
|
if (right?.type === 'identifier') {
|
|
964
1673
|
aliases.set(left.text, right.text);
|
|
965
1674
|
}
|
|
@@ -1017,7 +1726,8 @@ function findCallsInCode(code, parser) {
|
|
|
1017
1726
|
const ctor = constructorTypeInfo(right.childForFieldName('function'));
|
|
1018
1727
|
if (ctor) {
|
|
1019
1728
|
localVarTypes.set(left.text, ctor.type);
|
|
1020
|
-
|
|
1729
|
+
constructedReceiverVars.add(left.text);
|
|
1730
|
+
if (ctor.qualifier && moduleAliases.has(ctor.qualifier.split('.')[0])) {
|
|
1021
1731
|
localVarTypeQualifiers.set(left.text, ctor.qualifier);
|
|
1022
1732
|
} else {
|
|
1023
1733
|
localVarTypeQualifiers.delete(left.text);
|
|
@@ -1042,7 +1752,8 @@ function findCallsInCode(code, parser) {
|
|
|
1042
1752
|
|
|
1043
1753
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1044
1754
|
let uncertain = false;
|
|
1045
|
-
const
|
|
1755
|
+
const assignedContext = contextTargetOf(node);
|
|
1756
|
+
const assignedTo = assignmentTargetOf(node) || assignedContext;
|
|
1046
1757
|
|
|
1047
1758
|
// Call-site arg count (positional + keyword) for arity pruning.
|
|
1048
1759
|
// *args/**kwargs splats make the count open-ended — flag them so
|
|
@@ -1082,6 +1793,7 @@ function findCallsInCode(code, parser) {
|
|
|
1082
1793
|
...(recvType && { receiverType: recvType }),
|
|
1083
1794
|
...(recvIsModule && { receiverIsModule: true }),
|
|
1084
1795
|
...(assignedTo && { assignedTo }),
|
|
1796
|
+
...(assignedContext && { assignedContext: true }),
|
|
1085
1797
|
argCount,
|
|
1086
1798
|
...(argSpread && { argSpread: true }),
|
|
1087
1799
|
enclosingFunction,
|
|
@@ -1097,10 +1809,12 @@ function findCallsInCode(code, parser) {
|
|
|
1097
1809
|
line: node.startPosition.row + 1,
|
|
1098
1810
|
isMethod: false,
|
|
1099
1811
|
...(assignedTo && { assignedTo }),
|
|
1812
|
+
...(assignedContext && { assignedContext: true }),
|
|
1100
1813
|
argCount,
|
|
1101
1814
|
...(argSpread && { argSpread: true }),
|
|
1102
1815
|
enclosingFunction,
|
|
1103
1816
|
uncertain,
|
|
1817
|
+
...(isShadowedByLocal(funcNode, funcNode.text) && { localShadow: true }),
|
|
1104
1818
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
1105
1819
|
});
|
|
1106
1820
|
}
|
|
@@ -1112,6 +1826,7 @@ function findCallsInCode(code, parser) {
|
|
|
1112
1826
|
if (attrNode) {
|
|
1113
1827
|
let receiver = objNode?.type === 'identifier' ? objNode.text : undefined;
|
|
1114
1828
|
let selfAttribute = undefined;
|
|
1829
|
+
const receiverPath = attributeReceiverPath(objNode);
|
|
1115
1830
|
// Chained receiver (fix #219): the receiver IS a call —
|
|
1116
1831
|
// fetch_data().json() — record the producer so findCallers
|
|
1117
1832
|
// can type the receiver from its declared return
|
|
@@ -1129,10 +1844,17 @@ function findCallsInCode(code, parser) {
|
|
|
1129
1844
|
}
|
|
1130
1845
|
{
|
|
1131
1846
|
let recvNode = objNode;
|
|
1132
|
-
|
|
1133
|
-
recvNode.
|
|
1847
|
+
while (recvNode?.type === 'parenthesized_expression' &&
|
|
1848
|
+
recvNode.namedChildCount === 1) {
|
|
1849
|
+
recvNode = recvNode.namedChild(0);
|
|
1850
|
+
}
|
|
1851
|
+
if (recvNode?.type === 'await') {
|
|
1134
1852
|
receiverCallAwaited = true;
|
|
1135
|
-
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
|
+
}
|
|
1136
1858
|
}
|
|
1137
1859
|
if (recvNode?.type === 'call' && receiver !== 'super') {
|
|
1138
1860
|
const prodFunc = recvNode.childForFieldName('function');
|
|
@@ -1169,17 +1891,63 @@ function findCallsInCode(code, parser) {
|
|
|
1169
1891
|
|
|
1170
1892
|
// Literal receivers carry their builtin type: {}.get() can
|
|
1171
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
|
+
}
|
|
1172
1904
|
const receiverType = receiver
|
|
1173
|
-
?
|
|
1174
|
-
|
|
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
|
+
}
|
|
1175
1939
|
const receiverTypeQualifier = receiver
|
|
1176
1940
|
? localVarTypeQualifiers.get(receiver)
|
|
1177
1941
|
: undefined;
|
|
1942
|
+
const receiverRootType = receiverPath
|
|
1943
|
+
? localVarTypes.get(receiverPath.root) : undefined;
|
|
1178
1944
|
// Module receiver (httpx.get()) — unless locally shadowed
|
|
1179
1945
|
// by a typed instance binding
|
|
1180
1946
|
const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
|
|
1181
1947
|
!localVarTypes.has(receiver);
|
|
1182
1948
|
const firstArg = getFirstStringArg(node);
|
|
1949
|
+
const capabilityGuard = receiverCapabilityGuard(
|
|
1950
|
+
node, objNode, attrNode.text);
|
|
1183
1951
|
calls.push({
|
|
1184
1952
|
name: attrNode.text,
|
|
1185
1953
|
// Multi-line chains (obj.x()\n.y()) must report each
|
|
@@ -1189,14 +1957,40 @@ function findCallsInCode(code, parser) {
|
|
|
1189
1957
|
isMethod: true,
|
|
1190
1958
|
receiver,
|
|
1191
1959
|
...(receiverType && { receiverType }),
|
|
1960
|
+
...(receiver && localVarStdlibContracts.has(receiver) && {
|
|
1961
|
+
receiverTypeStdlibModule:
|
|
1962
|
+
localVarStdlibContracts.get(receiver),
|
|
1963
|
+
}),
|
|
1192
1964
|
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
1965
|
+
...(receiver && constructedReceiverVars.has(receiver) && { receiverConstructed: true }),
|
|
1966
|
+
...(receiver && withBindingVars.has(receiver) && { receiverWithBinding: true }),
|
|
1193
1967
|
...(receiverIsModule && { receiverIsModule: true }),
|
|
1968
|
+
...(receiver && objNode?.type === 'identifier' &&
|
|
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
|
+
}),
|
|
1194
1984
|
...(selfAttribute && { selfAttribute }),
|
|
1195
1985
|
...(receiverCall && { receiverCall }),
|
|
1196
1986
|
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1197
1987
|
...(receiverCallAwaited && { receiverCallAwaited: true }),
|
|
1198
1988
|
...(receiverCallLine && { receiverCallLine }),
|
|
1989
|
+
...(capabilityGuard && {
|
|
1990
|
+
receiverCapabilityGuard: capabilityGuard,
|
|
1991
|
+
}),
|
|
1199
1992
|
...(assignedTo && { assignedTo }),
|
|
1993
|
+
...(assignedContext && { assignedContext: true }),
|
|
1200
1994
|
argCount,
|
|
1201
1995
|
...(argSpread && { argSpread: true }),
|
|
1202
1996
|
enclosingFunction,
|
|
@@ -1268,11 +2062,53 @@ function findCallsInCode(code, parser) {
|
|
|
1268
2062
|
localVarTypes.clear();
|
|
1269
2063
|
for (const [k, v] of saved) localVarTypes.set(k, v);
|
|
1270
2064
|
}
|
|
2065
|
+
const savedDeclared = declaredVarTypesStack.pop();
|
|
2066
|
+
if (savedDeclared) {
|
|
2067
|
+
declaredVarTypes.clear();
|
|
2068
|
+
for (const [k, v] of savedDeclared) declaredVarTypes.set(k, v);
|
|
2069
|
+
}
|
|
1271
2070
|
const savedQualifiers = localVarTypeQualifiersStack.pop();
|
|
1272
2071
|
if (savedQualifiers) {
|
|
1273
2072
|
localVarTypeQualifiers.clear();
|
|
1274
2073
|
for (const [k, v] of savedQualifiers) localVarTypeQualifiers.set(k, v);
|
|
1275
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
|
+
}
|
|
2106
|
+
const savedConstructed = constructedReceiverVarsStack.pop();
|
|
2107
|
+
constructedReceiverVars.clear();
|
|
2108
|
+
if (savedConstructed) for (const name of savedConstructed) constructedReceiverVars.add(name);
|
|
2109
|
+
const savedWithBindings = withBindingVarsStack.pop();
|
|
2110
|
+
withBindingVars.clear();
|
|
2111
|
+
if (savedWithBindings) for (const name of savedWithBindings) withBindingVars.add(name);
|
|
1276
2112
|
const savedAliases = memberAliasesStack.pop();
|
|
1277
2113
|
if (savedAliases) {
|
|
1278
2114
|
memberAliases.clear();
|
|
@@ -1304,13 +2140,33 @@ function findImportsInCode(code, parser) {
|
|
|
1304
2140
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1305
2141
|
const child = node.namedChild(i);
|
|
1306
2142
|
if (child.type === 'dotted_name') {
|
|
1307
|
-
// import
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
2143
|
+
// `import pkg.submodule` binds `pkg`, while also loading
|
|
2144
|
+
// the complete submodule. Record both ownership edges so
|
|
2145
|
+
// `pkg.public_api()` can resolve through pkg/__init__.py
|
|
2146
|
+
// and `pkg.submodule.member` can still resolve to the
|
|
2147
|
+
// imported child module.
|
|
2148
|
+
const parts = child.text.split('.');
|
|
2149
|
+
if (parts.length > 1) {
|
|
2150
|
+
imports.push({
|
|
2151
|
+
module: parts[0],
|
|
2152
|
+
names: [parts[0]],
|
|
2153
|
+
type: 'import',
|
|
2154
|
+
line
|
|
2155
|
+
});
|
|
2156
|
+
imports.push({
|
|
2157
|
+
module: child.text,
|
|
2158
|
+
names: [],
|
|
2159
|
+
type: 'import-submodule',
|
|
2160
|
+
line
|
|
2161
|
+
});
|
|
2162
|
+
} else {
|
|
2163
|
+
imports.push({
|
|
2164
|
+
module: child.text,
|
|
2165
|
+
names: [child.text],
|
|
2166
|
+
type: 'import',
|
|
2167
|
+
line
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
1314
2170
|
} else if (child.type === 'aliased_import') {
|
|
1315
2171
|
// import sys as system
|
|
1316
2172
|
const nameNode = child.namedChild(0);
|
|
@@ -1414,6 +2270,33 @@ function findExportsInCode(code, parser) {
|
|
|
1414
2270
|
const exports = [];
|
|
1415
2271
|
|
|
1416
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
|
+
}
|
|
1417
2300
|
// Look for __all__ = [...]
|
|
1418
2301
|
if (node.type === 'expression_statement') {
|
|
1419
2302
|
const child = node.namedChild(0);
|
|
@@ -1537,6 +2420,16 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
1537
2420
|
usages.push({ line, column, usageType, receiver: object.text });
|
|
1538
2421
|
return true;
|
|
1539
2422
|
}
|
|
2423
|
+
// Constructed receiver: ColorTriplet(...).normalized is a
|
|
2424
|
+
// class-associated property reference, not an unowned bare
|
|
2425
|
+
// name. This matters to class-scoped `tests` queries.
|
|
2426
|
+
if (object && object.type === 'call') {
|
|
2427
|
+
const ctor = object.childForFieldName('function');
|
|
2428
|
+
if (ctor?.type === 'identifier') {
|
|
2429
|
+
usages.push({ line, column, usageType, receiver: ctor.text });
|
|
2430
|
+
return true;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
1540
2433
|
// self.attr receiver (unittest setUp idiom: self.w = Widget(3);
|
|
1541
2434
|
// self.w.render()) — record the ATTR name so the instance-type
|
|
1542
2435
|
// map built from the assignment line ('w' → Widget) matches
|
|
@@ -1567,9 +2460,10 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
1567
2460
|
* @param {object} parser - Tree-sitter parser instance
|
|
1568
2461
|
* @returns {Map<string, Map<string, string>>} className -> (attrName -> typeName)
|
|
1569
2462
|
*/
|
|
1570
|
-
function findInstanceAttributeTypes(code, parser) {
|
|
2463
|
+
function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
1571
2464
|
const tree = parseTree(parser, code);
|
|
1572
2465
|
const result = new Map(); // className -> Map(attrName -> typeName)
|
|
2466
|
+
const explicitContracts = explicitInstanceFieldContracts(tree, parser);
|
|
1573
2467
|
|
|
1574
2468
|
const PRIMITIVE_TYPES = new Set(['int', 'float', 'str', 'bool', 'bytes', 'list', 'dict', 'set', 'tuple', 'None', 'Any', 'object']);
|
|
1575
2469
|
|
|
@@ -1584,6 +2478,23 @@ function findInstanceAttributeTypes(code, parser) {
|
|
|
1584
2478
|
if (!body) return false;
|
|
1585
2479
|
|
|
1586
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
|
+
}
|
|
1587
2498
|
|
|
1588
2499
|
// Scan annotated class-level fields: name: Type [= ...]. Originally
|
|
1589
2500
|
// @dataclass-only (fix #28); a BARE class-body annotation is the
|
|
@@ -1691,6 +2602,19 @@ function findInstanceAttributeTypes(code, parser) {
|
|
|
1691
2602
|
const typeName = extractConstructorName(rhs);
|
|
1692
2603
|
if (typeName) {
|
|
1693
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
|
+
}
|
|
1694
2618
|
} else if (rhs.type === 'identifier' && paramTypes.has(rhs.text)) {
|
|
1695
2619
|
// self.X = param where param has type annotation
|
|
1696
2620
|
attrTypes.set(attrName, paramTypes.get(rhs.text));
|
|
@@ -1700,6 +2624,63 @@ function findInstanceAttributeTypes(code, parser) {
|
|
|
1700
2624
|
});
|
|
1701
2625
|
}
|
|
1702
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
|
+
|
|
1703
2684
|
if (attrTypes.size > 0) {
|
|
1704
2685
|
result.set(className, attrTypes);
|
|
1705
2686
|
}
|
|
@@ -1799,6 +2780,32 @@ function isEntryPoint(symbol) {
|
|
|
1799
2780
|
return getEntryPointKind(symbol) !== null;
|
|
1800
2781
|
}
|
|
1801
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
|
+
|
|
1802
2809
|
module.exports = {
|
|
1803
2810
|
findFunctions,
|
|
1804
2811
|
findClasses,
|
|
@@ -1808,6 +2815,8 @@ module.exports = {
|
|
|
1808
2815
|
findExportsInCode,
|
|
1809
2816
|
findUsagesInCode,
|
|
1810
2817
|
findInstanceAttributeTypes,
|
|
2818
|
+
getBuiltinCallReturnType,
|
|
2819
|
+
getBuiltinFieldType,
|
|
1811
2820
|
isEntryPoint,
|
|
1812
2821
|
getEntryPointKind,
|
|
1813
2822
|
parse
|