ballpython 2.0.0__py3-none-any.whl
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.
- ballpython/__init__.py +9 -0
- ballpython/__main__.py +8 -0
- ballpython/cli.py +7 -0
- ballpython-2.0.0.dist-info/METADATA +322 -0
- ballpython-2.0.0.dist-info/RECORD +24 -0
- ballpython-2.0.0.dist-info/WHEEL +5 -0
- ballpython-2.0.0.dist-info/entry_points.txt +3 -0
- ballpython-2.0.0.dist-info/top_level.txt +2 -0
- pycleaner/__init__.py +53 -0
- pycleaner/__main__.py +8 -0
- pycleaner/cli.py +1548 -0
- pycleaner/complexity_analyzer.py +473 -0
- pycleaner/config.py +254 -0
- pycleaner/dead_code_detector.py +515 -0
- pycleaner/dependency_auditor.py +331 -0
- pycleaner/import_resolver.py +832 -0
- pycleaner/linter_formatter.py +590 -0
- pycleaner/pipeline.py +349 -0
- pycleaner/security_scanner.py +563 -0
- pycleaner/syntax_healer.py +577 -0
- pycleaner/taint_engine.py +720 -0
- pycleaner/test_generator.py +444 -0
- pycleaner/type_checker.py +989 -0
- pycleaner/typeshed_resolver.py +395 -0
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Static bidirectional type inference and type checking engine.
|
|
3
|
+
|
|
4
|
+
Inspects AST expressions, evaluates algebraic types, validates return types
|
|
5
|
+
against declared signatures, checks call argument type compatibility,
|
|
6
|
+
and performs type narrowing across conditional branches.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pycleaner.typeshed_resolver import TypeshedResolver
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Algebraic Type System
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PyType:
|
|
23
|
+
"""Base algebraic type representation."""
|
|
24
|
+
|
|
25
|
+
def is_assignable_to(self, target: PyType) -> bool:
|
|
26
|
+
"""Check if this type can be assigned to target type."""
|
|
27
|
+
if isinstance(target, AnyType) or isinstance(self, AnyType) or target == self:
|
|
28
|
+
return True
|
|
29
|
+
if isinstance(target, UnionType):
|
|
30
|
+
return self._assignable_to_union(target)
|
|
31
|
+
if isinstance(self, UnionType):
|
|
32
|
+
return self._union_assignable_to(target)
|
|
33
|
+
if isinstance(target, CustomClassType) and self._check_custom_target(target):
|
|
34
|
+
return True
|
|
35
|
+
if isinstance(self, CustomClassType) and self._check_custom_source(target):
|
|
36
|
+
return True
|
|
37
|
+
return self._check_numeric_promotions(target)
|
|
38
|
+
|
|
39
|
+
def _assignable_to_union(self, target: PyType) -> bool:
|
|
40
|
+
types = getattr(target, "types", [])
|
|
41
|
+
for u in types:
|
|
42
|
+
if self.is_assignable_to(u):
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
def _union_assignable_to(self, target: PyType) -> bool:
|
|
47
|
+
types = getattr(self, "types", [])
|
|
48
|
+
non_none = [u for u in types if not isinstance(u, NoneType)]
|
|
49
|
+
if non_none:
|
|
50
|
+
all_match = True
|
|
51
|
+
for u in non_none:
|
|
52
|
+
if not u.is_assignable_to(target):
|
|
53
|
+
all_match = False
|
|
54
|
+
break
|
|
55
|
+
if all_match:
|
|
56
|
+
return True
|
|
57
|
+
for u in types:
|
|
58
|
+
if not u.is_assignable_to(target):
|
|
59
|
+
return False
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
def _check_custom_target(self, target: PyType) -> bool:
|
|
63
|
+
if not isinstance(target, CustomClassType):
|
|
64
|
+
return False
|
|
65
|
+
if target.name in ("StrPath", "PathLike", "AnyStr"):
|
|
66
|
+
if isinstance(self, PrimitiveType) and self.name in ("str", "bytes"):
|
|
67
|
+
return True
|
|
68
|
+
if isinstance(self, CustomClassType) and self.name in (
|
|
69
|
+
"Path",
|
|
70
|
+
"PosixPath",
|
|
71
|
+
"WindowsPath",
|
|
72
|
+
"str",
|
|
73
|
+
"bytes",
|
|
74
|
+
):
|
|
75
|
+
return True
|
|
76
|
+
return (
|
|
77
|
+
target.name.startswith("Supports")
|
|
78
|
+
or target.name.startswith("_")
|
|
79
|
+
or target.name in ("T", "Any")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _check_custom_source(self, target: PyType) -> bool:
|
|
83
|
+
if self.name in ("StrPath", "PathLike", "AnyStr"): # type: ignore[attr-defined]
|
|
84
|
+
if isinstance(target, PrimitiveType) and target.name in ("str", "bytes"):
|
|
85
|
+
return True
|
|
86
|
+
if isinstance(target, CustomClassType) and target.name in (
|
|
87
|
+
"Path",
|
|
88
|
+
"PosixPath",
|
|
89
|
+
"WindowsPath",
|
|
90
|
+
"str",
|
|
91
|
+
"bytes",
|
|
92
|
+
):
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def _check_numeric_promotions(self, target: PyType) -> bool:
|
|
97
|
+
if isinstance(self, PrimitiveType) and isinstance(target, PrimitiveType):
|
|
98
|
+
if self.name == "int" and target.name == "float":
|
|
99
|
+
return True
|
|
100
|
+
if self.name == "bool" and target.name in ("int", "float"):
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def __str__(self) -> str:
|
|
105
|
+
return self.__class__.__name__
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AnyType(PyType):
|
|
109
|
+
def __eq__(self, other: object) -> bool:
|
|
110
|
+
return isinstance(other, AnyType)
|
|
111
|
+
|
|
112
|
+
def __str__(self) -> str:
|
|
113
|
+
return "Any"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class NoneType(PyType):
|
|
117
|
+
def __eq__(self, other: object) -> bool:
|
|
118
|
+
return isinstance(other, NoneType)
|
|
119
|
+
|
|
120
|
+
def __str__(self) -> str:
|
|
121
|
+
return "None"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class PrimitiveType(PyType):
|
|
125
|
+
def __init__(self, name: str) -> None:
|
|
126
|
+
self.name = name
|
|
127
|
+
|
|
128
|
+
def __eq__(self, other: object) -> bool:
|
|
129
|
+
return isinstance(other, PrimitiveType) and self.name == other.name
|
|
130
|
+
|
|
131
|
+
def __str__(self) -> str:
|
|
132
|
+
return self.name
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class UnionType(PyType):
|
|
136
|
+
types: list[PyType]
|
|
137
|
+
|
|
138
|
+
def __init__(self, types: list[PyType]) -> None:
|
|
139
|
+
flat: list[PyType] = []
|
|
140
|
+
for t in types:
|
|
141
|
+
if isinstance(t, UnionType):
|
|
142
|
+
flat.extend(t.types)
|
|
143
|
+
elif t not in flat:
|
|
144
|
+
flat.append(t)
|
|
145
|
+
self.types = flat
|
|
146
|
+
|
|
147
|
+
def __eq__(self, other: object) -> bool:
|
|
148
|
+
return isinstance(other, UnionType) and {str(t) for t in self.types} == {
|
|
149
|
+
str(t) for t in other.types
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def __str__(self) -> str:
|
|
153
|
+
return " | ".join(str(t) for t in self.types)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _is_collection_assignable(source_item: PyType, target: PyType) -> bool:
|
|
157
|
+
if isinstance(target, AnyType):
|
|
158
|
+
return True
|
|
159
|
+
if isinstance(target, (ListType, SetType)):
|
|
160
|
+
if isinstance(target.item_type, AnyType) or isinstance(source_item, AnyType):
|
|
161
|
+
return True
|
|
162
|
+
if isinstance(target.item_type, CustomClassType) and (
|
|
163
|
+
target.item_type.name.startswith("Supports")
|
|
164
|
+
or target.item_type.name.startswith("_")
|
|
165
|
+
or target.item_type.name in ("T", "Any")
|
|
166
|
+
):
|
|
167
|
+
return True
|
|
168
|
+
return source_item.is_assignable_to(target.item_type)
|
|
169
|
+
return isinstance(target, CustomClassType) and target.name in (
|
|
170
|
+
"list",
|
|
171
|
+
"List",
|
|
172
|
+
"set",
|
|
173
|
+
"Set",
|
|
174
|
+
"Sequence",
|
|
175
|
+
"Iterable",
|
|
176
|
+
"Collection",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ListType(PyType):
|
|
181
|
+
def __init__(self, item_type: PyType) -> None:
|
|
182
|
+
self.item_type = item_type
|
|
183
|
+
|
|
184
|
+
def is_assignable_to(self, target: PyType) -> bool:
|
|
185
|
+
if _is_collection_assignable(self.item_type, target):
|
|
186
|
+
return True
|
|
187
|
+
return super().is_assignable_to(target)
|
|
188
|
+
|
|
189
|
+
def __eq__(self, other: object) -> bool:
|
|
190
|
+
return isinstance(other, ListType) and self.item_type == other.item_type
|
|
191
|
+
|
|
192
|
+
def __str__(self) -> str:
|
|
193
|
+
return f"list[{self.item_type}]"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class SetType(PyType):
|
|
197
|
+
def __init__(self, item_type: PyType) -> None:
|
|
198
|
+
self.item_type = item_type
|
|
199
|
+
|
|
200
|
+
def is_assignable_to(self, target: PyType) -> bool:
|
|
201
|
+
if _is_collection_assignable(self.item_type, target):
|
|
202
|
+
return True
|
|
203
|
+
return super().is_assignable_to(target)
|
|
204
|
+
|
|
205
|
+
def __eq__(self, other: object) -> bool:
|
|
206
|
+
return isinstance(other, SetType) and self.item_type == other.item_type
|
|
207
|
+
|
|
208
|
+
def __str__(self) -> str:
|
|
209
|
+
return f"set[{self.item_type}]"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class DictType(PyType):
|
|
213
|
+
def __init__(self, key_type: PyType, value_type: PyType) -> None:
|
|
214
|
+
self.key_type = key_type
|
|
215
|
+
self.value_type = value_type
|
|
216
|
+
|
|
217
|
+
def is_assignable_to(self, target: PyType) -> bool:
|
|
218
|
+
if isinstance(target, AnyType):
|
|
219
|
+
return True
|
|
220
|
+
if isinstance(target, DictType):
|
|
221
|
+
return self.key_type.is_assignable_to(
|
|
222
|
+
target.key_type
|
|
223
|
+
) and self.value_type.is_assignable_to(target.value_type)
|
|
224
|
+
if isinstance(target, CustomClassType) and target.name in (
|
|
225
|
+
"dict",
|
|
226
|
+
"Dict",
|
|
227
|
+
"Mapping",
|
|
228
|
+
):
|
|
229
|
+
return True
|
|
230
|
+
return super().is_assignable_to(target)
|
|
231
|
+
|
|
232
|
+
def __eq__(self, other: object) -> bool:
|
|
233
|
+
return (
|
|
234
|
+
isinstance(other, DictType)
|
|
235
|
+
and self.key_type == other.key_type
|
|
236
|
+
and self.value_type == other.value_type
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def __str__(self) -> str:
|
|
240
|
+
return f"dict[{self.key_type}, {self.value_type}]"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class CustomClassType(PyType):
|
|
244
|
+
def __init__(self, name: str) -> None:
|
|
245
|
+
self.name = name
|
|
246
|
+
|
|
247
|
+
def __eq__(self, other: object) -> bool:
|
|
248
|
+
return isinstance(other, CustomClassType) and self.name == other.name
|
|
249
|
+
|
|
250
|
+
def __str__(self) -> str:
|
|
251
|
+
return self.name
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
# Diagnostics & Reports
|
|
256
|
+
# ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass(slots=True)
|
|
260
|
+
class TypeFinding:
|
|
261
|
+
"""A detected type mismatch or type violation."""
|
|
262
|
+
|
|
263
|
+
filepath: str
|
|
264
|
+
lineno: int
|
|
265
|
+
column: int
|
|
266
|
+
symbol: str
|
|
267
|
+
expected_type: str
|
|
268
|
+
actual_type: str
|
|
269
|
+
message: str
|
|
270
|
+
severity: str = "ERROR"
|
|
271
|
+
category: str = "type-mismatch"
|
|
272
|
+
code_snippet: str = ""
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass(slots=True)
|
|
276
|
+
class TypeReport:
|
|
277
|
+
"""Summary of static type checking results."""
|
|
278
|
+
|
|
279
|
+
findings: list[TypeFinding] = field(default_factory=list)
|
|
280
|
+
files_scanned: int = 0
|
|
281
|
+
functions_checked: int = 0
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def count(self) -> int:
|
|
285
|
+
return len(self.findings)
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def has_errors(self) -> bool:
|
|
289
|
+
return any(f.severity == "ERROR" for f in self.findings)
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def error_count(self) -> int:
|
|
293
|
+
return sum(1 for f in self.findings if f.severity == "ERROR")
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def warning_count(self) -> int:
|
|
297
|
+
return sum(1 for f in self.findings if f.severity == "WARNING")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# Type Parser
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _parse_name_annotation(name: str) -> PyType:
|
|
306
|
+
"""Resolve identifier names to primitive, collection, or custom PyTypes."""
|
|
307
|
+
if name in ("int", "str", "float", "bool", "bytes"):
|
|
308
|
+
return PrimitiveType(name)
|
|
309
|
+
if name in ("None", "NoneType"):
|
|
310
|
+
return NoneType()
|
|
311
|
+
if name == "Any":
|
|
312
|
+
return AnyType()
|
|
313
|
+
if name in ("set", "Set"):
|
|
314
|
+
return SetType(AnyType())
|
|
315
|
+
if name in ("list", "List", "Sequence", "Iterable", "Collection", "tuple", "Tuple"):
|
|
316
|
+
return ListType(AnyType())
|
|
317
|
+
if name in ("dict", "Dict", "Mapping"):
|
|
318
|
+
return DictType(AnyType(), AnyType())
|
|
319
|
+
return CustomClassType(name)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _parse_dict_subscript(slice_node: ast.expr) -> PyType:
|
|
323
|
+
if isinstance(slice_node, ast.Tuple) and len(slice_node.elts) == 2:
|
|
324
|
+
return DictType(
|
|
325
|
+
parse_type_annotation(slice_node.elts[0]),
|
|
326
|
+
parse_type_annotation(slice_node.elts[1]),
|
|
327
|
+
)
|
|
328
|
+
return DictType(AnyType(), AnyType())
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _parse_union_subscript(slice_node: ast.expr) -> PyType:
|
|
332
|
+
if isinstance(slice_node, ast.Tuple):
|
|
333
|
+
return UnionType([parse_type_annotation(e) for e in slice_node.elts])
|
|
334
|
+
return parse_type_annotation(slice_node)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _parse_subscript_annotation(node: ast.Subscript) -> PyType:
|
|
338
|
+
"""Resolve subscripted type annotations (generics, optionals, unions)."""
|
|
339
|
+
val = node.value
|
|
340
|
+
base_name = (
|
|
341
|
+
val.id
|
|
342
|
+
if isinstance(val, ast.Name)
|
|
343
|
+
else (val.attr if isinstance(val, ast.Attribute) else "")
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if base_name == "Optional":
|
|
347
|
+
return UnionType([parse_type_annotation(node.slice), NoneType()])
|
|
348
|
+
if base_name == "Union":
|
|
349
|
+
res = _parse_union_subscript(node.slice)
|
|
350
|
+
return res if isinstance(res, UnionType) else UnionType([res])
|
|
351
|
+
if base_name in ("set", "Set"):
|
|
352
|
+
return SetType(parse_type_annotation(node.slice))
|
|
353
|
+
if base_name in (
|
|
354
|
+
"list",
|
|
355
|
+
"List",
|
|
356
|
+
"Sequence",
|
|
357
|
+
"Iterable",
|
|
358
|
+
"Collection",
|
|
359
|
+
"tuple",
|
|
360
|
+
"Tuple",
|
|
361
|
+
):
|
|
362
|
+
return ListType(parse_type_annotation(node.slice))
|
|
363
|
+
if base_name in ("dict", "Dict", "Mapping"):
|
|
364
|
+
return _parse_dict_subscript(node.slice)
|
|
365
|
+
return AnyType()
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _parse_constant_annotation(annotation: ast.Constant) -> PyType:
|
|
369
|
+
if annotation.value is None:
|
|
370
|
+
return NoneType()
|
|
371
|
+
if isinstance(annotation.value, str):
|
|
372
|
+
return parse_type_annotation(annotation.value)
|
|
373
|
+
return AnyType()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def parse_type_annotation(annotation: ast.AST | str | None) -> PyType:
|
|
377
|
+
"""Parse an AST node or type string into a PyType representation."""
|
|
378
|
+
if annotation is None:
|
|
379
|
+
return AnyType()
|
|
380
|
+
|
|
381
|
+
if isinstance(annotation, str):
|
|
382
|
+
try:
|
|
383
|
+
return parse_type_annotation(ast.parse(annotation, mode="eval").body)
|
|
384
|
+
except SyntaxError:
|
|
385
|
+
return AnyType()
|
|
386
|
+
|
|
387
|
+
if isinstance(annotation, ast.Name):
|
|
388
|
+
return _parse_name_annotation(annotation.id)
|
|
389
|
+
|
|
390
|
+
if isinstance(annotation, ast.Constant):
|
|
391
|
+
return _parse_constant_annotation(annotation)
|
|
392
|
+
|
|
393
|
+
if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
|
|
394
|
+
return UnionType(
|
|
395
|
+
[
|
|
396
|
+
parse_type_annotation(annotation.left),
|
|
397
|
+
parse_type_annotation(annotation.right),
|
|
398
|
+
]
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if isinstance(annotation, ast.Subscript):
|
|
402
|
+
return _parse_subscript_annotation(annotation)
|
|
403
|
+
|
|
404
|
+
return AnyType()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ---------------------------------------------------------------------------
|
|
408
|
+
# Type Checker Engine
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@dataclass(slots=True)
|
|
413
|
+
class _FileContext:
|
|
414
|
+
filepath: str
|
|
415
|
+
source_lines: list[str]
|
|
416
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]] = field(
|
|
417
|
+
default_factory=dict
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class TypeChecker:
|
|
422
|
+
"""Type checker verifying annotations, return statements, and call sites."""
|
|
423
|
+
|
|
424
|
+
IGNORE_DIRS: frozenset[str] = frozenset(
|
|
425
|
+
{
|
|
426
|
+
".git",
|
|
427
|
+
".venv",
|
|
428
|
+
"venv",
|
|
429
|
+
"env",
|
|
430
|
+
"__pycache__",
|
|
431
|
+
"build",
|
|
432
|
+
"dist",
|
|
433
|
+
".tox",
|
|
434
|
+
".mypy_cache",
|
|
435
|
+
".pytest_cache",
|
|
436
|
+
".ruff_cache",
|
|
437
|
+
"site-packages",
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def __init__(
|
|
442
|
+
self, typeshed: TypeshedResolver | None = None, strict: bool = False
|
|
443
|
+
) -> None:
|
|
444
|
+
self.typeshed = typeshed or TypeshedResolver()
|
|
445
|
+
self.strict = strict
|
|
446
|
+
|
|
447
|
+
def check_file(self, filepath: str | Path) -> list[TypeFinding]:
|
|
448
|
+
"""Check all functions and statements in a file."""
|
|
449
|
+
path = Path(filepath)
|
|
450
|
+
try:
|
|
451
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
452
|
+
tree = ast.parse(content, filename=str(path))
|
|
453
|
+
except SyntaxError:
|
|
454
|
+
return []
|
|
455
|
+
|
|
456
|
+
return self.check_ast(tree, filepath=str(path), source=content)
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def _collect_local_signatures(
|
|
460
|
+
tree: ast.Module,
|
|
461
|
+
) -> dict[str, tuple[dict[str, PyType], PyType]]:
|
|
462
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]] = {}
|
|
463
|
+
for node in ast.walk(tree):
|
|
464
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
465
|
+
params: dict[str, PyType] = {}
|
|
466
|
+
all_args = node.args.posonlyargs + node.args.args + node.args.kwonlyargs
|
|
467
|
+
for a in all_args:
|
|
468
|
+
params[a.arg] = (
|
|
469
|
+
parse_type_annotation(a.annotation)
|
|
470
|
+
if a.annotation
|
|
471
|
+
else AnyType()
|
|
472
|
+
)
|
|
473
|
+
ret_type = (
|
|
474
|
+
parse_type_annotation(node.returns) if node.returns else AnyType()
|
|
475
|
+
)
|
|
476
|
+
local_functions[node.name] = (params, ret_type)
|
|
477
|
+
return local_functions
|
|
478
|
+
|
|
479
|
+
def check_ast(
|
|
480
|
+
self,
|
|
481
|
+
tree: ast.Module,
|
|
482
|
+
filepath: str = "<unknown>",
|
|
483
|
+
source: str = "",
|
|
484
|
+
) -> list[TypeFinding]:
|
|
485
|
+
"""Perform static type verification on an AST module."""
|
|
486
|
+
findings: list[TypeFinding] = []
|
|
487
|
+
ctx = _FileContext(
|
|
488
|
+
filepath=filepath,
|
|
489
|
+
source_lines=source.splitlines() if source else [],
|
|
490
|
+
local_functions=self._collect_local_signatures(tree),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
for node in ast.walk(tree):
|
|
494
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
495
|
+
findings.extend(self._check_function(node, ctx))
|
|
496
|
+
elif isinstance(node, ast.AnnAssign):
|
|
497
|
+
findings.extend(self._check_ann_assign(node, ctx))
|
|
498
|
+
|
|
499
|
+
return findings
|
|
500
|
+
|
|
501
|
+
def _discover_python_files(self, root: Path) -> list[Path]:
|
|
502
|
+
files: list[Path] = []
|
|
503
|
+
walker = (
|
|
504
|
+
Path(root).walk() if hasattr(Path, "walk") else self._fallback_walk(root)
|
|
505
|
+
)
|
|
506
|
+
for current_root, dirs, filenames in walker:
|
|
507
|
+
dirs[:] = [
|
|
508
|
+
d for d in dirs if d not in self.IGNORE_DIRS and not d.startswith(".")
|
|
509
|
+
]
|
|
510
|
+
for fname in filenames:
|
|
511
|
+
if fname.endswith(".py"):
|
|
512
|
+
files.append(Path(current_root) / fname)
|
|
513
|
+
return files
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def _count_functions(fpath: Path) -> int:
|
|
517
|
+
try:
|
|
518
|
+
tree = ast.parse(fpath.read_text(encoding="utf-8", errors="replace"))
|
|
519
|
+
return sum(
|
|
520
|
+
1
|
|
521
|
+
for n in ast.walk(tree)
|
|
522
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
523
|
+
)
|
|
524
|
+
except SyntaxError:
|
|
525
|
+
return 0
|
|
526
|
+
|
|
527
|
+
def check_project(self, root_dir: str | Path) -> TypeReport:
|
|
528
|
+
"""Type-check all Python files across an entire project directory."""
|
|
529
|
+
root = Path(root_dir).resolve()
|
|
530
|
+
findings: list[TypeFinding] = []
|
|
531
|
+
functions_checked = 0
|
|
532
|
+
py_files = self._discover_python_files(root)
|
|
533
|
+
|
|
534
|
+
for fpath in py_files:
|
|
535
|
+
findings.extend(self.check_file(fpath))
|
|
536
|
+
functions_checked += self._count_functions(fpath)
|
|
537
|
+
|
|
538
|
+
return TypeReport(
|
|
539
|
+
findings=findings,
|
|
540
|
+
files_scanned=len(py_files),
|
|
541
|
+
functions_checked=functions_checked,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def _fallback_walk(self, root: Path):
|
|
545
|
+
import os
|
|
546
|
+
|
|
547
|
+
for r, d, f in os.walk(root):
|
|
548
|
+
yield Path(r), d, f
|
|
549
|
+
|
|
550
|
+
@staticmethod
|
|
551
|
+
def _init_param_scope(
|
|
552
|
+
func: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
553
|
+
) -> dict[str, PyType]:
|
|
554
|
+
scope: dict[str, PyType] = {}
|
|
555
|
+
all_args = func.args.posonlyargs + func.args.args + func.args.kwonlyargs
|
|
556
|
+
for a in all_args:
|
|
557
|
+
scope[a.arg] = (
|
|
558
|
+
parse_type_annotation(a.annotation) if a.annotation else AnyType()
|
|
559
|
+
)
|
|
560
|
+
return scope
|
|
561
|
+
|
|
562
|
+
def _check_return_paths(
|
|
563
|
+
self,
|
|
564
|
+
func: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
565
|
+
declared_return: PyType,
|
|
566
|
+
scope: dict[str, PyType],
|
|
567
|
+
ctx: _FileContext,
|
|
568
|
+
) -> list[TypeFinding]:
|
|
569
|
+
if isinstance(declared_return, AnyType):
|
|
570
|
+
return []
|
|
571
|
+
|
|
572
|
+
findings: list[TypeFinding] = []
|
|
573
|
+
for child in ast.walk(func):
|
|
574
|
+
if not isinstance(child, ast.Return):
|
|
575
|
+
continue
|
|
576
|
+
if self._is_nested_in_other_func(child, func):
|
|
577
|
+
continue
|
|
578
|
+
|
|
579
|
+
actual_type = self._infer_expr_type(child.value, scope, ctx.local_functions)
|
|
580
|
+
if not actual_type.is_assignable_to(declared_return):
|
|
581
|
+
snippet = (
|
|
582
|
+
ctx.source_lines[child.lineno - 1]
|
|
583
|
+
if 0 <= child.lineno - 1 < len(ctx.source_lines)
|
|
584
|
+
else ""
|
|
585
|
+
)
|
|
586
|
+
findings.append(
|
|
587
|
+
TypeFinding(
|
|
588
|
+
filepath=ctx.filepath,
|
|
589
|
+
lineno=child.lineno,
|
|
590
|
+
column=child.col_offset,
|
|
591
|
+
symbol=func.name,
|
|
592
|
+
expected_type=str(declared_return),
|
|
593
|
+
actual_type=str(actual_type),
|
|
594
|
+
message=(
|
|
595
|
+
f"Incompatible return type: function '{func.name}' declared to return "
|
|
596
|
+
f"'{declared_return}' but returned '{actual_type}'"
|
|
597
|
+
),
|
|
598
|
+
severity="ERROR",
|
|
599
|
+
code_snippet=snippet.strip(),
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
return findings
|
|
604
|
+
|
|
605
|
+
def _check_function(
|
|
606
|
+
self,
|
|
607
|
+
func: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
608
|
+
ctx: _FileContext,
|
|
609
|
+
) -> list[TypeFinding]:
|
|
610
|
+
"""Check return statement types and local assignments within a function."""
|
|
611
|
+
declared_return = (
|
|
612
|
+
parse_type_annotation(func.returns) if func.returns else AnyType()
|
|
613
|
+
)
|
|
614
|
+
scope = self._init_param_scope(func)
|
|
615
|
+
findings = self._check_return_paths(func, declared_return, scope, ctx)
|
|
616
|
+
|
|
617
|
+
for child in ast.walk(func):
|
|
618
|
+
if isinstance(child, ast.Call):
|
|
619
|
+
findings.extend(self._check_call_arguments(child, scope, ctx))
|
|
620
|
+
|
|
621
|
+
return findings
|
|
622
|
+
|
|
623
|
+
def _check_ann_assign(
|
|
624
|
+
self,
|
|
625
|
+
node: ast.AnnAssign,
|
|
626
|
+
ctx: _FileContext,
|
|
627
|
+
) -> list[TypeFinding]:
|
|
628
|
+
"""Check explicit variable annotation vs assigned value."""
|
|
629
|
+
if not node.value or not isinstance(node.target, ast.Name):
|
|
630
|
+
return []
|
|
631
|
+
|
|
632
|
+
declared_type = parse_type_annotation(node.annotation)
|
|
633
|
+
if isinstance(declared_type, AnyType):
|
|
634
|
+
return []
|
|
635
|
+
|
|
636
|
+
actual_type = self._infer_expr_type(node.value, {}, ctx.local_functions)
|
|
637
|
+
if isinstance(actual_type, AnyType) or actual_type.is_assignable_to(
|
|
638
|
+
declared_type
|
|
639
|
+
):
|
|
640
|
+
return []
|
|
641
|
+
|
|
642
|
+
snippet = (
|
|
643
|
+
ctx.source_lines[node.lineno - 1]
|
|
644
|
+
if 0 <= node.lineno - 1 < len(ctx.source_lines)
|
|
645
|
+
else ""
|
|
646
|
+
)
|
|
647
|
+
return [
|
|
648
|
+
TypeFinding(
|
|
649
|
+
filepath=ctx.filepath,
|
|
650
|
+
lineno=node.lineno,
|
|
651
|
+
column=node.col_offset,
|
|
652
|
+
symbol=node.target.id,
|
|
653
|
+
expected_type=str(declared_type),
|
|
654
|
+
actual_type=str(actual_type),
|
|
655
|
+
message=(
|
|
656
|
+
f"Variable '{node.target.id}' declared as '{declared_type}' "
|
|
657
|
+
f"assigned incompatible type '{actual_type}'"
|
|
658
|
+
),
|
|
659
|
+
severity="ERROR",
|
|
660
|
+
code_snippet=snippet.strip(),
|
|
661
|
+
)
|
|
662
|
+
]
|
|
663
|
+
|
|
664
|
+
@staticmethod
|
|
665
|
+
def _resolve_call_names(call: ast.Call) -> tuple[str, str]:
|
|
666
|
+
"""Extract the short function name and dotted name from a Call node."""
|
|
667
|
+
func = call.func
|
|
668
|
+
if isinstance(func, ast.Name):
|
|
669
|
+
return func.id, ""
|
|
670
|
+
if isinstance(func, ast.Attribute):
|
|
671
|
+
func_name = func.attr
|
|
672
|
+
dotted_parts: list[str] = []
|
|
673
|
+
curr: ast.expr = func
|
|
674
|
+
while isinstance(curr, ast.Attribute):
|
|
675
|
+
dotted_parts.append(curr.attr)
|
|
676
|
+
curr = curr.value
|
|
677
|
+
if isinstance(curr, ast.Name):
|
|
678
|
+
dotted_parts.append(curr.id)
|
|
679
|
+
return func_name, ".".join(reversed(dotted_parts))
|
|
680
|
+
return func_name, ""
|
|
681
|
+
return "", ""
|
|
682
|
+
|
|
683
|
+
@staticmethod
|
|
684
|
+
def _clean_receiver_params(
|
|
685
|
+
param_types: dict[str, PyType], call: ast.Call
|
|
686
|
+
) -> dict[str, PyType]:
|
|
687
|
+
if isinstance(call.func, ast.Attribute) and param_types:
|
|
688
|
+
first_param = next(iter(param_types))
|
|
689
|
+
if first_param in ("self", "cls"):
|
|
690
|
+
param_types.pop(first_param, None)
|
|
691
|
+
return param_types
|
|
692
|
+
|
|
693
|
+
def _resolve_call_param_types(
|
|
694
|
+
self,
|
|
695
|
+
call: ast.Call,
|
|
696
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]],
|
|
697
|
+
) -> tuple[str, dict[str, PyType]]:
|
|
698
|
+
"""Resolve function name and parameter types for a Call node."""
|
|
699
|
+
func_name, dotted_name = self._resolve_call_names(call)
|
|
700
|
+
if func_name in local_functions:
|
|
701
|
+
cleaned = self._clean_receiver_params(
|
|
702
|
+
dict(local_functions[func_name][0]), call
|
|
703
|
+
)
|
|
704
|
+
return func_name, cleaned
|
|
705
|
+
|
|
706
|
+
target_lookup = dotted_name or func_name
|
|
707
|
+
if not target_lookup:
|
|
708
|
+
return func_name, {}
|
|
709
|
+
|
|
710
|
+
sig = self.typeshed.resolve_function(target_lookup)
|
|
711
|
+
if not sig or not sig.param_types:
|
|
712
|
+
return func_name, {}
|
|
713
|
+
|
|
714
|
+
param_types = {k: parse_type_annotation(v) for k, v in sig.param_types.items()}
|
|
715
|
+
if (sig.is_method and not sig.is_static) or sig.is_class_method:
|
|
716
|
+
param_types.pop(next(iter(param_types), ""), None)
|
|
717
|
+
return func_name, param_types
|
|
718
|
+
|
|
719
|
+
return func_name, self._clean_receiver_params(param_types, call)
|
|
720
|
+
|
|
721
|
+
@staticmethod
|
|
722
|
+
def _make_call_arg_finding(
|
|
723
|
+
ctx: _FileContext,
|
|
724
|
+
call: ast.Call,
|
|
725
|
+
func_param: tuple[str, str],
|
|
726
|
+
exp_act: tuple[PyType, PyType],
|
|
727
|
+
) -> TypeFinding:
|
|
728
|
+
func_name, param_name = func_param
|
|
729
|
+
expected, actual = exp_act
|
|
730
|
+
snippet = (
|
|
731
|
+
ctx.source_lines[call.lineno - 1].strip()
|
|
732
|
+
if 0 <= call.lineno - 1 < len(ctx.source_lines)
|
|
733
|
+
else ""
|
|
734
|
+
)
|
|
735
|
+
return TypeFinding(
|
|
736
|
+
filepath=ctx.filepath,
|
|
737
|
+
lineno=call.lineno,
|
|
738
|
+
column=call.col_offset,
|
|
739
|
+
symbol=func_name,
|
|
740
|
+
expected_type=str(expected),
|
|
741
|
+
actual_type=str(actual),
|
|
742
|
+
message=(
|
|
743
|
+
f"Argument '{param_name}' to '{func_name}' expects '{expected}' "
|
|
744
|
+
f"but received '{actual}'"
|
|
745
|
+
),
|
|
746
|
+
severity="ERROR",
|
|
747
|
+
code_snippet=snippet,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
def _check_call_arguments(
|
|
751
|
+
self,
|
|
752
|
+
call: ast.Call,
|
|
753
|
+
scope: dict[str, PyType],
|
|
754
|
+
ctx: _FileContext,
|
|
755
|
+
) -> list[TypeFinding]:
|
|
756
|
+
"""Validate argument types against function parameter annotations."""
|
|
757
|
+
func_name, param_types = self._resolve_call_param_types(
|
|
758
|
+
call, ctx.local_functions
|
|
759
|
+
)
|
|
760
|
+
if not param_types:
|
|
761
|
+
return []
|
|
762
|
+
|
|
763
|
+
param_names = list(param_types.keys())
|
|
764
|
+
findings: list[TypeFinding] = []
|
|
765
|
+
|
|
766
|
+
for idx, arg_expr in enumerate(call.args):
|
|
767
|
+
if idx >= len(param_names):
|
|
768
|
+
break
|
|
769
|
+
param_name = param_names[idx]
|
|
770
|
+
expected = param_types[param_name]
|
|
771
|
+
if isinstance(expected, AnyType):
|
|
772
|
+
continue
|
|
773
|
+
|
|
774
|
+
actual = self._infer_expr_type(arg_expr, scope, ctx.local_functions)
|
|
775
|
+
if isinstance(actual, AnyType) or actual.is_assignable_to(expected):
|
|
776
|
+
continue
|
|
777
|
+
|
|
778
|
+
findings.append(
|
|
779
|
+
self._make_call_arg_finding(
|
|
780
|
+
ctx, call, (func_name, param_name), (expected, actual)
|
|
781
|
+
)
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
return findings
|
|
785
|
+
|
|
786
|
+
def _infer_constant(self, expr: ast.Constant) -> PyType:
|
|
787
|
+
if expr.value is None:
|
|
788
|
+
return NoneType()
|
|
789
|
+
if isinstance(expr.value, bool):
|
|
790
|
+
return PrimitiveType("bool")
|
|
791
|
+
if isinstance(expr.value, int):
|
|
792
|
+
return PrimitiveType("int")
|
|
793
|
+
if isinstance(expr.value, float):
|
|
794
|
+
return PrimitiveType("float")
|
|
795
|
+
if isinstance(expr.value, str):
|
|
796
|
+
return PrimitiveType("str")
|
|
797
|
+
if isinstance(expr.value, bytes):
|
|
798
|
+
return PrimitiveType("bytes")
|
|
799
|
+
return AnyType()
|
|
800
|
+
|
|
801
|
+
def _infer_collection(
|
|
802
|
+
self,
|
|
803
|
+
expr: ast.List | ast.Set | ast.Dict,
|
|
804
|
+
scope: dict[str, PyType],
|
|
805
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]],
|
|
806
|
+
) -> PyType:
|
|
807
|
+
if isinstance(expr, ast.List):
|
|
808
|
+
if not expr.elts:
|
|
809
|
+
return ListType(AnyType())
|
|
810
|
+
return ListType(self._infer_expr_type(expr.elts[0], scope, local_functions))
|
|
811
|
+
if isinstance(expr, ast.Set):
|
|
812
|
+
if not expr.elts:
|
|
813
|
+
return SetType(AnyType())
|
|
814
|
+
return SetType(self._infer_expr_type(expr.elts[0], scope, local_functions))
|
|
815
|
+
if not expr.keys or not expr.values:
|
|
816
|
+
return DictType(AnyType(), AnyType())
|
|
817
|
+
k_type = (
|
|
818
|
+
self._infer_expr_type(expr.keys[0], scope, local_functions)
|
|
819
|
+
if expr.keys[0]
|
|
820
|
+
else AnyType()
|
|
821
|
+
)
|
|
822
|
+
v_type = self._infer_expr_type(expr.values[0], scope, local_functions)
|
|
823
|
+
return DictType(k_type, v_type)
|
|
824
|
+
|
|
825
|
+
@staticmethod
|
|
826
|
+
def _infer_numeric_binop(op: ast.operator, left: str, right: str) -> PyType:
|
|
827
|
+
if isinstance(op, ast.Div):
|
|
828
|
+
return PrimitiveType("float")
|
|
829
|
+
if "float" in (left, right):
|
|
830
|
+
return PrimitiveType("float")
|
|
831
|
+
return PrimitiveType("int")
|
|
832
|
+
|
|
833
|
+
def _infer_binop(
|
|
834
|
+
self,
|
|
835
|
+
expr: ast.BinOp,
|
|
836
|
+
scope: dict[str, PyType],
|
|
837
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]],
|
|
838
|
+
) -> PyType:
|
|
839
|
+
left_type = self._infer_expr_type(expr.left, scope, local_functions)
|
|
840
|
+
right_type = self._infer_expr_type(expr.right, scope, local_functions)
|
|
841
|
+
if isinstance(left_type, PrimitiveType) and isinstance(
|
|
842
|
+
right_type, PrimitiveType
|
|
843
|
+
):
|
|
844
|
+
if (
|
|
845
|
+
left_type.name == "str"
|
|
846
|
+
and right_type.name == "str"
|
|
847
|
+
and isinstance(expr.op, ast.Add)
|
|
848
|
+
):
|
|
849
|
+
return PrimitiveType("str")
|
|
850
|
+
if left_type.name in ("int", "float") and right_type.name in (
|
|
851
|
+
"int",
|
|
852
|
+
"float",
|
|
853
|
+
):
|
|
854
|
+
return self._infer_numeric_binop(
|
|
855
|
+
expr.op, left_type.name, right_type.name
|
|
856
|
+
)
|
|
857
|
+
return AnyType()
|
|
858
|
+
|
|
859
|
+
def _extract_iterable_item_type(
|
|
860
|
+
self,
|
|
861
|
+
arg: ast.expr,
|
|
862
|
+
scope: dict[str, PyType],
|
|
863
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]],
|
|
864
|
+
) -> PyType:
|
|
865
|
+
arg_t = self._infer_expr_type(arg, scope, local_functions)
|
|
866
|
+
if isinstance(arg_t, (ListType, SetType)):
|
|
867
|
+
return getattr(arg_t, "item_type", AnyType())
|
|
868
|
+
if isinstance(arg_t, PrimitiveType) and arg_t.name == "str":
|
|
869
|
+
return PrimitiveType("str")
|
|
870
|
+
return AnyType()
|
|
871
|
+
|
|
872
|
+
def _infer_constructor_call(
|
|
873
|
+
self,
|
|
874
|
+
func_name: str,
|
|
875
|
+
args: list[ast.expr],
|
|
876
|
+
scope: dict[str, PyType],
|
|
877
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]],
|
|
878
|
+
) -> PyType | None:
|
|
879
|
+
if func_name in ("set", "Set"):
|
|
880
|
+
item_t = (
|
|
881
|
+
self._extract_iterable_item_type(args[0], scope, local_functions)
|
|
882
|
+
if args
|
|
883
|
+
else AnyType()
|
|
884
|
+
)
|
|
885
|
+
return SetType(item_t)
|
|
886
|
+
|
|
887
|
+
if func_name in ("list", "List", "sorted"):
|
|
888
|
+
item_t = (
|
|
889
|
+
self._extract_iterable_item_type(args[0], scope, local_functions)
|
|
890
|
+
if args
|
|
891
|
+
else AnyType()
|
|
892
|
+
)
|
|
893
|
+
return ListType(item_t)
|
|
894
|
+
|
|
895
|
+
if func_name == "sum":
|
|
896
|
+
return PrimitiveType("int")
|
|
897
|
+
|
|
898
|
+
return None
|
|
899
|
+
|
|
900
|
+
def _infer_attribute_method(
|
|
901
|
+
self,
|
|
902
|
+
func: ast.Attribute,
|
|
903
|
+
scope: dict[str, PyType],
|
|
904
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]],
|
|
905
|
+
) -> PyType | None:
|
|
906
|
+
obj_type = self._infer_expr_type(func.value, scope, local_functions)
|
|
907
|
+
if isinstance(obj_type, PrimitiveType):
|
|
908
|
+
m_ret = self.typeshed.get_builtin_method_return_type(
|
|
909
|
+
obj_type.name, func.attr
|
|
910
|
+
)
|
|
911
|
+
if m_ret:
|
|
912
|
+
return parse_type_annotation(m_ret)
|
|
913
|
+
elif isinstance(obj_type, CustomClassType):
|
|
914
|
+
cls_sig = self.typeshed.resolve_class("builtins", obj_type.name)
|
|
915
|
+
if cls_sig and func.attr in cls_sig.methods:
|
|
916
|
+
return parse_type_annotation(cls_sig.methods[func.attr].return_type)
|
|
917
|
+
return None
|
|
918
|
+
|
|
919
|
+
def _infer_typeshed_lookup(self, target: str, func_name: str) -> PyType:
|
|
920
|
+
if target:
|
|
921
|
+
sig = self.typeshed.resolve_function(target)
|
|
922
|
+
if sig and sig.return_type and sig.return_type != "Any":
|
|
923
|
+
return parse_type_annotation(sig.return_type)
|
|
924
|
+
|
|
925
|
+
builtin_ret = self.typeshed.get_builtin_return_type(func_name)
|
|
926
|
+
if builtin_ret:
|
|
927
|
+
return parse_type_annotation(builtin_ret)
|
|
928
|
+
|
|
929
|
+
if func_name and self.typeshed.resolve_class("builtins", func_name):
|
|
930
|
+
return CustomClassType(func_name)
|
|
931
|
+
|
|
932
|
+
return AnyType()
|
|
933
|
+
|
|
934
|
+
def _infer_call(
|
|
935
|
+
self,
|
|
936
|
+
expr: ast.Call,
|
|
937
|
+
scope: dict[str, PyType],
|
|
938
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]],
|
|
939
|
+
) -> PyType:
|
|
940
|
+
func_name, dotted_name = self._resolve_call_names(expr)
|
|
941
|
+
ctor = self._infer_constructor_call(
|
|
942
|
+
func_name, expr.args, scope, local_functions
|
|
943
|
+
)
|
|
944
|
+
if ctor is not None:
|
|
945
|
+
return ctor
|
|
946
|
+
|
|
947
|
+
if isinstance(expr.func, ast.Attribute):
|
|
948
|
+
meth = self._infer_attribute_method(expr.func, scope, local_functions)
|
|
949
|
+
if meth is not None:
|
|
950
|
+
return meth
|
|
951
|
+
|
|
952
|
+
if func_name in local_functions:
|
|
953
|
+
return local_functions[func_name][1]
|
|
954
|
+
|
|
955
|
+
return self._infer_typeshed_lookup(dotted_name or func_name, func_name)
|
|
956
|
+
|
|
957
|
+
def _infer_expr_type(
|
|
958
|
+
self,
|
|
959
|
+
expr: ast.AST | None,
|
|
960
|
+
scope: dict[str, PyType],
|
|
961
|
+
local_functions: dict[str, tuple[dict[str, PyType], PyType]],
|
|
962
|
+
) -> PyType:
|
|
963
|
+
"""Infer the PyType of an AST expression."""
|
|
964
|
+
if expr is None:
|
|
965
|
+
return NoneType()
|
|
966
|
+
if isinstance(expr, ast.Constant):
|
|
967
|
+
return self._infer_constant(expr)
|
|
968
|
+
if isinstance(expr, ast.Name):
|
|
969
|
+
return scope.get(expr.id, AnyType())
|
|
970
|
+
if isinstance(expr, (ast.List, ast.Set, ast.Dict)):
|
|
971
|
+
return self._infer_collection(expr, scope, local_functions)
|
|
972
|
+
if isinstance(expr, ast.BinOp):
|
|
973
|
+
return self._infer_binop(expr, scope, local_functions)
|
|
974
|
+
if isinstance(expr, ast.Compare):
|
|
975
|
+
return PrimitiveType("bool")
|
|
976
|
+
if isinstance(expr, ast.Call):
|
|
977
|
+
return self._infer_call(expr, scope, local_functions)
|
|
978
|
+
return AnyType()
|
|
979
|
+
|
|
980
|
+
def _is_nested_in_other_func(self, node: ast.AST, parent_func: ast.AST) -> bool:
|
|
981
|
+
"""Verify if an AST node is contained in an inner/nested function definition."""
|
|
982
|
+
for child in ast.walk(parent_func):
|
|
983
|
+
if child is not parent_func and isinstance(
|
|
984
|
+
child, (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
985
|
+
):
|
|
986
|
+
for sub in ast.walk(child):
|
|
987
|
+
if sub is node:
|
|
988
|
+
return True
|
|
989
|
+
return False
|