python-constricter 0.4.1__cp311-cp311-win32.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.
- constricter/__init__.py +55 -0
- constricter/__main__.py +26 -0
- constricter/_version.py +4 -0
- constricter/cli/__init__.py +2 -0
- constricter/cli/baseline.py +107 -0
- constricter/cli/collecting.py +54 -0
- constricter/cli/command.py +547 -0
- constricter/cli/config.py +322 -0
- constricter/cli/explain.py +104 -0
- constricter/cli/lsp/__init__.py +2 -0
- constricter/cli/lsp/connection.py +475 -0
- constricter/cli/lsp/edits.py +188 -0
- constricter/cli/lsp/guard.py +133 -0
- constricter/cli/lsp/hints.py +391 -0
- constricter/cli/lsp/protocol.py +192 -0
- constricter/cli/lsp/remembered.py +81 -0
- constricter/cli/lsp/stored.py +200 -0
- constricter/cli/options.py +799 -0
- constricter/cli/paths.py +151 -0
- constricter/cli/reading.py +36 -0
- constricter/cli/report.py +397 -0
- constricter/cli/rounds.py +328 -0
- constricter/cli/runs.py +90 -0
- constricter/cli/schedule.py +463 -0
- constricter/cli/traced.py +401 -0
- constricter/cli/workers.py +286 -0
- constricter/empty.py +66 -0
- constricter/fix/__init__.cp311-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp311-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp311-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp311-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp311-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp311-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp311-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp311-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp311-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp311-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp311-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp311-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp311-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp311-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp311-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp311-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp311-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp311-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp311-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp311-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp311-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp311-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp311-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp311-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp311-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp311-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp311-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp311-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp311-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp311-win32.pyd +0 -0
- constricter/fix/libraries/walks.py +49 -0
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +700 -0
- constricter/fix/tables/awaited.json +69 -0
- constricter/fix/tables/awaited_overloads.json +14 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1941 -0
- constricter/fix/tables/elements.json +156 -0
- constricter/fix/tables/functions.json +58 -0
- constricter/fix/tables/generic_attributes.json +254 -0
- constricter/fix/tables/method_overloads.json +461 -0
- constricter/fix/tables/method_signatures.json +954 -0
- constricter/fix/tables/methods.json +954 -0
- constricter/fix/tables/overloads.json +540 -0
- constricter/fix/tables/platform.json +37 -0
- constricter/fix/tables/returns.json +1362 -0
- constricter/fix/tables/scalar_members.json +16 -0
- constricter/fix/tables/scalars.json +2707 -0
- constricter/fix/tables/source.json +28 -0
- constricter/fix/tables/subscriptable.json +77 -0
- constricter/fix/tables/type_parameters.json +178 -0
- constricter/fix/tables/variables.json +2962 -0
- constricter/fix/values/__init__.cp311-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp311-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp311-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp311-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp311-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp311-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp311-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp311-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp311-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp311-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp311-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp311-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp311-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp311-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp311-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp311-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp311-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp311-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp311-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp311-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp311-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp311-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp311-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp311-win32.pyd +0 -0
- constricter/fix/values/unchecked.py +278 -0
- constricter/jsonc.py +50 -0
- constricter/noqa.py +47 -0
- constricter/notebook.py +115 -0
- constricter/offences.py +436 -0
- constricter/overrides.py +24 -0
- constricter/plugins/__init__.py +2 -0
- constricter/plugins/flake8.py +133 -0
- constricter/plugins/pylint.py +186 -0
- constricter/py.typed +0 -0
- constricter/recording.py +660 -0
- constricter/rules/__init__.cp311-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp311-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp311-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp311-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp311-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp311-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp311-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp311-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp311-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp311-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp311-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp311-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp311-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp311-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp311-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp311-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp311-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp311-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp311-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp311-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp311-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp311-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp311-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp311-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp311-win32.pyd +0 -0
- python_constricter-0.4.1.dist-info/METADATA +400 -0
- python_constricter-0.4.1.dist-info/RECORD +261 -0
- python_constricter-0.4.1.dist-info/WHEEL +4 -0
- python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
- python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A module's own tables `--fix` reads, read once for the cross-file index and the check."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Final, NamedTuple
|
|
8
|
+
|
|
9
|
+
from constricter.fix.core.imports import rebound_names, taken_names
|
|
10
|
+
from constricter.fix.core.inherited import Lineage, lineage
|
|
11
|
+
from constricter.fix.core.signatures import AWAIT
|
|
12
|
+
from constricter.fix.values.targets import aliased_unions, named_tuples
|
|
13
|
+
from constricter.rules import walked
|
|
14
|
+
from constricter.rules.declarations import (
|
|
15
|
+
class_methods,
|
|
16
|
+
classes,
|
|
17
|
+
held,
|
|
18
|
+
held_class_methods,
|
|
19
|
+
method_returns,
|
|
20
|
+
partial_method_returns,
|
|
21
|
+
partial_returns,
|
|
22
|
+
returns,
|
|
23
|
+
self_returns,
|
|
24
|
+
)
|
|
25
|
+
from constricter.rules.decorators import Held, Pass, passes
|
|
26
|
+
from constricter.rules.syntax import import_bindings
|
|
27
|
+
|
|
28
|
+
_NEW_TYPES: Final = frozenset({"typing.NewType", "typing_extensions.NewType"})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Tables(NamedTuple):
|
|
32
|
+
"""A module's own tables (see `returns`, `classes`, `method_returns`, `class_methods`).
|
|
33
|
+
|
|
34
|
+
Its functions' declared returns (a `NewType` bound nowhere else returning itself: `Ref(name)` is
|
|
35
|
+
a `Ref`), and its classes' attributes and methods' returns (their classmethods' and
|
|
36
|
+
staticmethods' too, which an instance has as well; an `async` method's as what awaiting its call
|
|
37
|
+
gives, its name after `signatures.AWAIT`), the attributes each takes from the module's
|
|
38
|
+
other classes included (see `Lineage`); `held`, the functions other modules'
|
|
39
|
+
decorators may give back, and `passes`, its own such decorators; `sides`, its classes'
|
|
40
|
+
classmethods' and staticmethods' returns, those each takes from the module's other classes
|
|
41
|
+
included (see `Lineage`), and `held_sides`, those other modules' decorators may give back;
|
|
42
|
+
`side_calls`, `sides` as called on the class (see `_side_calls`). `partial` and
|
|
43
|
+
`partial_methods`: its functions' and methods' returns that are tuples with a vague part.
|
|
44
|
+
`tuples`: its named tuples, each with the tuple unpacking one gives (see `named_tuples`).
|
|
45
|
+
`unions`: its type aliases of a union, each with the union (see `aliased_unions`).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
returns: dict[str, str]
|
|
49
|
+
classes: dict[str, dict[str, str]]
|
|
50
|
+
methods: dict[str, dict[str, str]]
|
|
51
|
+
held: dict[str, Held]
|
|
52
|
+
passes: dict[str, Pass]
|
|
53
|
+
sides: dict[str, dict[str, str]]
|
|
54
|
+
held_sides: dict[str, dict[str, Held]]
|
|
55
|
+
side_calls: dict[str, str]
|
|
56
|
+
partial: dict[str, str]
|
|
57
|
+
partial_methods: dict[str, dict[str, str]]
|
|
58
|
+
order: Lineage # its classes' ancestry, as far as the module alone sees
|
|
59
|
+
tuples: dict[str, str]
|
|
60
|
+
unions: dict[str, str]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def module_tables(tree: ast.Module) -> Tables:
|
|
64
|
+
"""Read the module's own tables, once for the cross-file index and the check (see `parsed.keep`).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Them.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
own: dict[str, dict[str, str]] = class_methods(tree)
|
|
71
|
+
order: Lineage = lineage(tree, self_returns(tree), frozenset())
|
|
72
|
+
sides: dict[str, dict[str, str]] = order.flattened(own)
|
|
73
|
+
awaited: dict[str, dict[str, str]] = method_returns(tree, awaited=True)
|
|
74
|
+
made: list[str] = _new_types(tree)
|
|
75
|
+
rebound: frozenset[str] = frozenset(rebound_names(tree)) if made else frozenset()
|
|
76
|
+
return Tables(
|
|
77
|
+
{**returns(tree), **{name: name for name in made if name not in rebound}},
|
|
78
|
+
order.flattened(classes(tree)),
|
|
79
|
+
{
|
|
80
|
+
owner: {
|
|
81
|
+
**own.get(owner, {}),
|
|
82
|
+
**methods,
|
|
83
|
+
**{AWAIT + name: each for name, each in awaited[owner].items()},
|
|
84
|
+
}
|
|
85
|
+
for owner, methods in method_returns(tree).items()
|
|
86
|
+
},
|
|
87
|
+
held(tree),
|
|
88
|
+
passes(tree),
|
|
89
|
+
sides,
|
|
90
|
+
held_class_methods(tree),
|
|
91
|
+
_side_calls(tree, sides),
|
|
92
|
+
partial_returns(tree),
|
|
93
|
+
partial_method_returns(tree),
|
|
94
|
+
order,
|
|
95
|
+
named_tuples(tree),
|
|
96
|
+
aliased_unions(tree),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _side_calls(tree: ast.Module, sides: Mapping[str, Mapping[str, str]]) -> dict[str, str]:
|
|
101
|
+
"""Spell the calls to the module's classes' classmethods and staticmethods on the class itself.
|
|
102
|
+
|
|
103
|
+
`Box.make()`, for a top-level class defined once whose name nothing in the module binds as a
|
|
104
|
+
value (see `imports.taken_names`): there, the name is the class.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Each call's name as written (`Box.make`), and its declared return.
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
if not any(sides.values()): # most modules: no class-side method declares a return
|
|
111
|
+
return {}
|
|
112
|
+
values: frozenset[str] = taken_names(tree)[1]
|
|
113
|
+
counts: Counter[str] = Counter(node.name for node in walked.classes(tree))
|
|
114
|
+
return {
|
|
115
|
+
f"{stmt.name}.{name}": annotation
|
|
116
|
+
for stmt in tree.body
|
|
117
|
+
if isinstance(stmt, ast.ClassDef) and counts[stmt.name] == 1 and stmt.name not in values
|
|
118
|
+
for name, annotation in sides.get(stmt.name, {}).items()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _new_types(tree: ast.Module) -> list[str]:
|
|
123
|
+
"""Name the module's `NewType`s: `Ref = NewType("Ref", str)`, at its top level.
|
|
124
|
+
|
|
125
|
+
By `typing`'s or `typing_extensions`' `NewType`, however it's imported.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Them.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
made: list[tuple[str, ast.Name | ast.Attribute]] = []
|
|
132
|
+
stmt: ast.stmt
|
|
133
|
+
name: str
|
|
134
|
+
func: ast.expr
|
|
135
|
+
text: str
|
|
136
|
+
for stmt in tree.body:
|
|
137
|
+
match stmt:
|
|
138
|
+
case ast.Assign(
|
|
139
|
+
targets=[ast.Name(id=name)],
|
|
140
|
+
value=ast.Call(
|
|
141
|
+
func=ast.Name() | ast.Attribute() as func,
|
|
142
|
+
args=[ast.Constant(value=str() as text), _],
|
|
143
|
+
),
|
|
144
|
+
) if text == name:
|
|
145
|
+
made.append((name, func))
|
|
146
|
+
case _:
|
|
147
|
+
pass
|
|
148
|
+
if not made: # most modules
|
|
149
|
+
return []
|
|
150
|
+
bound: dict[str, str] = {name: origin for name, origin, _ in import_bindings(tree.body)}
|
|
151
|
+
return [name for name, func in made if _origin(func, bound) in _NEW_TYPES]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _origin(func: ast.Name | ast.Attribute, bound: Mapping[str, str]) -> str | None:
|
|
155
|
+
"""Resolve a callee one or two names long through the module's imports (`bound`).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Its dotted origin (`typing.NewType`), or `None`.
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
name: str
|
|
162
|
+
attr: str
|
|
163
|
+
match func:
|
|
164
|
+
case ast.Name(id=name):
|
|
165
|
+
return bound.get(name)
|
|
166
|
+
case ast.Attribute(value=ast.Name(id=name), attr=attr) if name in bound:
|
|
167
|
+
return f"{bound[name]}.{attr}"
|
|
168
|
+
case _:
|
|
169
|
+
return None
|
|
Binary file
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""One walk of a whole module, shared by every pass over all of it (importing nothing of constricter's)."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Final, TypeAlias, cast
|
|
7
|
+
from weakref import WeakKeyDictionary
|
|
8
|
+
|
|
9
|
+
_CTX: Final = "ctx"
|
|
10
|
+
# What a node's field holds, as far as the walk cares: a node, a list (of nodes, or of names), or else.
|
|
11
|
+
_Value: TypeAlias = ast.AST | list[ast.AST | str] | str | int | None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_Walk: TypeAlias = dict[type[ast.AST], list[ast.AST]] # a module's nodes, by their type
|
|
15
|
+
# Each node class's fields, but its `ctx` (only ever `Load`, `Store` or `Del`: no pass looks at them).
|
|
16
|
+
_FIELDS: dict[type[ast.AST], tuple[str, ...]] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Each module's walk, for as long as its tree lives: the cross-file index's walk of a file is the
|
|
20
|
+
# check's too, when the index kept its tree (`parsed.keep`), however many files came between.
|
|
21
|
+
_WALKS: Final[WeakKeyDictionary[ast.Module, "_Walk"]] = WeakKeyDictionary()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _by_type(tree: ast.Module) -> dict[type[ast.AST], list[ast.AST]]:
|
|
25
|
+
"""Walk a whole module once, sorting its nodes by type, for every pass that looks at all of it.
|
|
26
|
+
|
|
27
|
+
`ast.walk` was most of a check's time (on the standard library, 57%), eight passes each walking
|
|
28
|
+
every module whole. This walk is `ast.walk`'s, breadth first, without its per-node generators:
|
|
29
|
+
a generation of nodes at a time, and each class's fields looked up once.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Each type's nodes, in `ast.walk`'s order (all but the `Load`, `Store` and `Del` markers).
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
found: dict[type[ast.AST], list[ast.AST]] | None
|
|
36
|
+
if (found := _WALKS.get(tree)) is not None:
|
|
37
|
+
return found
|
|
38
|
+
found = _WALKS[tree] = {}
|
|
39
|
+
# A generation at a time (a breadth-first queue's order): each built from the one before.
|
|
40
|
+
# `None` at the bottom ends it: a generation with no children adds none to follow it.
|
|
41
|
+
generations: list[list[ast.AST] | None] = [None, [tree]]
|
|
42
|
+
generation: list[ast.AST]
|
|
43
|
+
for generation in iter(generations.pop, None):
|
|
44
|
+
following: list[ast.AST] = []
|
|
45
|
+
node: ast.AST
|
|
46
|
+
for node in generation:
|
|
47
|
+
kind: type[ast.AST] = type(node)
|
|
48
|
+
found.setdefault(kind, []).append(node)
|
|
49
|
+
field: str
|
|
50
|
+
for field in _fields_of(kind):
|
|
51
|
+
value: _Value = cast("_Value", getattr(node, field, None))
|
|
52
|
+
if isinstance(value, list):
|
|
53
|
+
# A list, not a generator: none's resumed for each item (and it's inlined, 3.12+).
|
|
54
|
+
following.extend([item for item in value if isinstance(item, ast.AST)])
|
|
55
|
+
elif isinstance(value, ast.AST):
|
|
56
|
+
following.append(value)
|
|
57
|
+
if following:
|
|
58
|
+
generations.append(following)
|
|
59
|
+
return found
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def of_type(tree: ast.Module, *kinds: type[ast.AST]) -> Sequence[ast.AST]:
|
|
63
|
+
"""Find a module's nodes of `kinds`: a pass that wants only those needn't look at every node.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Them, each kind's in `ast.walk`'s order, the kinds one after another.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
table: dict[type[ast.AST], list[ast.AST]] = _by_type(tree)
|
|
70
|
+
return [node for kind in kinds for node in table.get(kind, [])]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def classes(tree: ast.Module) -> Sequence[ast.ClassDef]:
|
|
74
|
+
"""Find every class a module defines, however deep.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Them, in `ast.walk`'s order.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
return cast("list[ast.ClassDef]", _by_type(tree).get(ast.ClassDef, []))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def children(node: ast.AST) -> list[ast.AST]:
|
|
84
|
+
"""List a node's children, as `ast.iter_child_nodes` does but for their `ctx`, and without its generator.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Them, in their fields' order.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
found: list[ast.AST] = []
|
|
91
|
+
field: str
|
|
92
|
+
for field in _fields_of(type(node)):
|
|
93
|
+
value: _Value = cast("_Value", getattr(node, field, None))
|
|
94
|
+
if isinstance(value, list):
|
|
95
|
+
found.extend([item for item in value if isinstance(item, ast.AST)])
|
|
96
|
+
elif isinstance(value, ast.AST):
|
|
97
|
+
found.append(value)
|
|
98
|
+
return found
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def walk(node: ast.AST) -> list[ast.AST]:
|
|
102
|
+
"""List a node and everything under it (but `ctx` markers), for a pass that needn't their order.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Them, depth first (not `ast.walk`'s breadth first order).
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
found: list[ast.AST] = []
|
|
109
|
+
waiting: list[ast.AST | None] = [None, node] # `None` at its bottom ends it
|
|
110
|
+
item: ast.AST
|
|
111
|
+
for item in iter(waiting.pop, None):
|
|
112
|
+
found.append(item)
|
|
113
|
+
waiting.extend(children(item))
|
|
114
|
+
return found
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fields_of(kind: type[ast.AST]) -> tuple[str, ...]:
|
|
118
|
+
"""Name a node class's fields but its `ctx`, worked out once per class.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Them.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
fields: tuple[str, ...] | None
|
|
125
|
+
if (fields := _FIELDS.get(kind)) is None:
|
|
126
|
+
fields = _FIELDS[kind] = tuple(field for field in kind._fields if field != _CTX)
|
|
127
|
+
return fields
|
|
Binary file
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Wider types than a value's own, written only where `fix-widen` asks, each line marked (`MARK`).
|
|
3
|
+
|
|
4
|
+
A marked annotation is `--fix`'s own: it isn't LVA005, `--coverage` counts it apart, and a later
|
|
5
|
+
`--fix` replaces it once the value's type is known.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import re
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from collections.abc import Collection, Mapping, Sequence
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from typing import Final, NamedTuple
|
|
14
|
+
|
|
15
|
+
from constricter.fix.core.known import ImportPlan, Inference
|
|
16
|
+
from constricter.fix.values import fills
|
|
17
|
+
from constricter.offences import (
|
|
18
|
+
EMPTY_CONTAINERS,
|
|
19
|
+
MARK,
|
|
20
|
+
MIXED_CONTAINERS,
|
|
21
|
+
UNKNOWN_CALLS,
|
|
22
|
+
UNTYPED_CALLS,
|
|
23
|
+
UNTYPED_PARAMETERS,
|
|
24
|
+
)
|
|
25
|
+
from constricter.rules.annotations import dotted, node_name
|
|
26
|
+
from constricter.rules.syntax import FUNCTION_DEFS, FunctionDef
|
|
27
|
+
from constricter.rules.walked import children, classes, walk
|
|
28
|
+
|
|
29
|
+
ANY: Final = "typing.Any"
|
|
30
|
+
_RECEIVERS: Final = frozenset({"self", "cls"})
|
|
31
|
+
# The decorators that leave a method's call its own `return`s.
|
|
32
|
+
_BINDERS: Final = frozenset({"staticmethod", "classmethod"})
|
|
33
|
+
# The mark, ending its line, with the space before it: what a replaced widening's fix deletes.
|
|
34
|
+
_MARKED: Final = re.compile(rf"[ \t]*{re.escape(MARK)}(?=[ \t]*[\r\n]*$)")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def marks(lines: Sequence[str]) -> dict[int, tuple[int, int]]:
|
|
38
|
+
"""Find the lines a widening marked.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Each one's number (from 1), and the mark's columns in it (UTF-8 bytes, as `ast` counts).
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
found: dict[int, tuple[int, int]] = {}
|
|
45
|
+
number: int
|
|
46
|
+
line: str
|
|
47
|
+
for number, line in enumerate(lines, 1):
|
|
48
|
+
marked: re.Match[str] | None
|
|
49
|
+
if (marked := _MARKED.search(line) if MARK in line else None) is not None:
|
|
50
|
+
start: int = len(line[: marked.start()].encode())
|
|
51
|
+
found[number] = (start, start + len(marked.group().encode()))
|
|
52
|
+
return found
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@lru_cache(maxsize=64) # asked of each of a function's assignments with no fix
|
|
56
|
+
def untyped_parameters(function: FunctionDef) -> frozenset[str]:
|
|
57
|
+
"""Name a function's parameters nothing types: no annotation, no default, never bound again.
|
|
58
|
+
|
|
59
|
+
A checker takes each for anything, wherever it's read. Not `*args` or `**kwargs`, nor a
|
|
60
|
+
method's `self` or `cls`, which is its class's.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Them.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
args: ast.arguments = function.args
|
|
67
|
+
positional: list[ast.arg] = [*args.posonlyargs, *args.args]
|
|
68
|
+
plain: int = len(positional) - len(args.defaults)
|
|
69
|
+
defaulted: set[str] = {arg.arg for arg in positional[plain:]}
|
|
70
|
+
defaulted.update(
|
|
71
|
+
arg.arg for arg, default in zip(args.kwonlyargs, args.kw_defaults, strict=True) if default
|
|
72
|
+
)
|
|
73
|
+
rebound: set[str] = {
|
|
74
|
+
node.id
|
|
75
|
+
for node in walk(function)
|
|
76
|
+
if isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Load)
|
|
77
|
+
}
|
|
78
|
+
return frozenset(
|
|
79
|
+
arg.arg
|
|
80
|
+
for arg in (*positional, *args.kwonlyargs)
|
|
81
|
+
if arg.annotation is None and arg.arg not in defaulted | rebound | _RECEIVERS
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def wider(
|
|
86
|
+
function: FunctionDef | None,
|
|
87
|
+
bound: tuple[ast.Name, ast.expr],
|
|
88
|
+
typed: Collection[str],
|
|
89
|
+
plan: ImportPlan | None,
|
|
90
|
+
) -> Inference | None:
|
|
91
|
+
"""Type a name `bound` to what comes of an untyped parameter of `function` as `Any`.
|
|
92
|
+
|
|
93
|
+
A copy of it (`x = param`), or its method's call (`y = param.read()`), all on the name's line
|
|
94
|
+
(the mark ends it); not one of `typed`, which something gives a type after all. What every
|
|
95
|
+
checker takes it for already.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The inference, or `None`: also where the module (`plan`) can't name `Any`.
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
target: ast.Name
|
|
102
|
+
value: ast.expr
|
|
103
|
+
target, value = bound
|
|
104
|
+
if function is None or plan is None or value.end_lineno != target.lineno:
|
|
105
|
+
return None
|
|
106
|
+
name: str
|
|
107
|
+
match value:
|
|
108
|
+
case ast.Name(id=name) | ast.Call(func=ast.Attribute(value=ast.Name(id=name))) if (
|
|
109
|
+
name in untyped_parameters(function) and name not in typed
|
|
110
|
+
):
|
|
111
|
+
spelled: str | None = plan.spell(ANY)
|
|
112
|
+
reason: str = f"what comes of `{name}`, a parameter no annotation types"
|
|
113
|
+
return None if spelled is None else Inference(spelled, reason, frozenset({UNTYPED_PARAMETERS}))
|
|
114
|
+
case _:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def container(value: ast.expr, plan: ImportPlan, kinds: Collection[str]) -> Inference | None:
|
|
119
|
+
"""Type a container whose elements aren't known with `Any` for them: `list[Any]`, `dict[str, Any]`.
|
|
120
|
+
|
|
121
|
+
An empty one nothing in sight fills (`[]`, `dict()`: kind `empty-containers`), or a display
|
|
122
|
+
whose elements' types differ or aren't known (`[1, "a"]`, `(x, y)`: `mixed-containers`); a dict
|
|
123
|
+
display's keys are `str` where each is a string. Only the kinds among `kinds`.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The inference, or `None`: for any other value, or where the module (`plan`) can't name `Any`.
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
empty: str | None = fills.empty(value)
|
|
130
|
+
kind: str = EMPTY_CONTAINERS if empty is not None else MIXED_CONTAINERS
|
|
131
|
+
spelled: str | None
|
|
132
|
+
if (spelled := plan.spell(ANY) if kind in kinds else None) is None:
|
|
133
|
+
return None
|
|
134
|
+
keys: list[ast.expr | None]
|
|
135
|
+
annotation: str | None
|
|
136
|
+
match value:
|
|
137
|
+
case ast.List(elts=[_, *_]):
|
|
138
|
+
annotation = f"list[{spelled}]"
|
|
139
|
+
case ast.Set():
|
|
140
|
+
annotation = f"set[{spelled}]"
|
|
141
|
+
case ast.Tuple(elts=[_, *_]):
|
|
142
|
+
annotation = f"tuple[{spelled}, ...]"
|
|
143
|
+
case ast.Dict(keys=keys) if keys:
|
|
144
|
+
texts: bool = all(isinstance(key, ast.Constant) and isinstance(key.value, str) for key in keys)
|
|
145
|
+
annotation = f"dict[{'str' if texts else spelled}, {spelled}]"
|
|
146
|
+
case _:
|
|
147
|
+
annotation = {"list": f"list[{spelled}]", "set": f"set[{spelled}]"}.get(
|
|
148
|
+
empty or "",
|
|
149
|
+
f"dict[{spelled}, {spelled}]" if empty else None,
|
|
150
|
+
)
|
|
151
|
+
if annotation is None:
|
|
152
|
+
return None
|
|
153
|
+
reason: str = "an empty container nothing fills" if empty else "a display of mixed or unknown elements"
|
|
154
|
+
return Inference(annotation, reason, frozenset({kind}))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Own(NamedTuple):
|
|
158
|
+
"""A module's functions and classes a call may name, each defined once: what `called` looks through.
|
|
159
|
+
|
|
160
|
+
`functions`: its top-level functions, by name; `classes`: its classes, anywhere, by name.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
functions: Mapping[str, FunctionDef]
|
|
164
|
+
classes: Mapping[str, ast.ClassDef]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Caller(NamedTuple):
|
|
168
|
+
"""The function a call is in: its class, where it reads a method's `self`, and the names it binds.
|
|
169
|
+
|
|
170
|
+
`untyped`: the other checked files' functions its module calls that declare no return, as it
|
|
171
|
+
spells them (see `Outside.untyped`).
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
owner: str | None
|
|
175
|
+
bound: Collection[str]
|
|
176
|
+
untyped: Collection[str] = ()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@lru_cache(maxsize=2) # asked of each scope of a module in turn
|
|
180
|
+
def own(tree: ast.Module) -> Own:
|
|
181
|
+
"""Read a module's `Own`.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
It.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
defined: list[FunctionDef | ast.ClassDef] = [
|
|
188
|
+
*(stmt for stmt in tree.body if isinstance(stmt, FUNCTION_DEFS)),
|
|
189
|
+
*classes(tree),
|
|
190
|
+
]
|
|
191
|
+
count: Counter[str] = Counter(node.name for node in defined)
|
|
192
|
+
once: list[FunctionDef | ast.ClassDef] = [node for node in defined if count[node.name] == 1]
|
|
193
|
+
return Own(
|
|
194
|
+
{node.name: node for node in once if isinstance(node, FUNCTION_DEFS)},
|
|
195
|
+
{node.name: node for node in once if isinstance(node, ast.ClassDef)},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _method(found: Own, owner: str, name: str) -> FunctionDef | None:
|
|
200
|
+
"""Find the method a class's instance takes by `name`: its own, or the first of its bases'.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Its definition, or `None`: where a class on the way isn't the module's, or binds the name
|
|
204
|
+
more than once (overloads, a property's setter) or otherwise than by a `def`.
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
waiting: list[str | None] = [
|
|
208
|
+
None,
|
|
209
|
+
owner,
|
|
210
|
+
] # each class still to look in, the next one last; `None` ends it
|
|
211
|
+
current: str
|
|
212
|
+
for current in iter(waiting.pop, None):
|
|
213
|
+
node: ast.ClassDef | None
|
|
214
|
+
if (node := found.classes.get(current)) is None:
|
|
215
|
+
return None
|
|
216
|
+
bound: list[ast.AST]
|
|
217
|
+
if bound := _bindings(node, name):
|
|
218
|
+
method: ast.AST = bound[0]
|
|
219
|
+
return method if len(bound) == 1 and isinstance(method, FUNCTION_DEFS) else None
|
|
220
|
+
bases: list[str] = [base.id for base in node.bases if isinstance(base, ast.Name)]
|
|
221
|
+
if len(bases) != len(node.bases):
|
|
222
|
+
return None
|
|
223
|
+
waiting.extend(reversed(bases))
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _bindings(node: ast.ClassDef, name: str) -> list[ast.AST]:
|
|
228
|
+
"""Find what binds `name` in a class's body: not in its methods' bodies, nor a nested class's.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Each `def`, class and store of it.
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
found: list[ast.AST] = []
|
|
235
|
+
waiting: list[ast.AST | None] = [None, *node.body] # each node still to read; `None` ends it
|
|
236
|
+
child: ast.AST
|
|
237
|
+
for child in iter(waiting.pop, None):
|
|
238
|
+
if isinstance(child, (*FUNCTION_DEFS, ast.ClassDef)):
|
|
239
|
+
found.extend([child] if child.name == name else [])
|
|
240
|
+
elif isinstance(child, ast.Name):
|
|
241
|
+
found.extend([child] if child.id == name and isinstance(child.ctx, ast.Store) else [])
|
|
242
|
+
else:
|
|
243
|
+
waiting.extend(children(child))
|
|
244
|
+
return found
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _undeclared(function: FunctionDef, *, awaited: bool) -> bool:
|
|
248
|
+
"""Check whether a function's call is one a checker reading no body takes for anything.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Whether it declares no return, by an annotation or a type comment, is decorated at most as a
|
|
252
|
+
static or class method, and is an `async def` just where its call is `awaited`.
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
return (
|
|
256
|
+
function.returns is None
|
|
257
|
+
and not function.type_comment
|
|
258
|
+
and isinstance(function, ast.AsyncFunctionDef) == awaited
|
|
259
|
+
and all(node_name(decorator) in _BINDERS for decorator in function.decorator_list)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _imported(func: ast.expr, caller: Caller) -> bool:
|
|
264
|
+
"""Check whether a call's function is another checked file's that declares no return.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Whether `func` spells one of `caller.untyped`, through no name its function binds.
|
|
268
|
+
|
|
269
|
+
"""
|
|
270
|
+
spelled: str | None = dotted(func)
|
|
271
|
+
return spelled is not None and spelled in caller.untyped and spelled.partition(".")[0] not in caller.bound
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def called(
|
|
275
|
+
value: ast.expr,
|
|
276
|
+
caller: Caller,
|
|
277
|
+
found: Own,
|
|
278
|
+
plan: ImportPlan,
|
|
279
|
+
kinds: Collection[str],
|
|
280
|
+
) -> Inference | None:
|
|
281
|
+
"""Type a call of no known type as `Any`, awaited or not.
|
|
282
|
+
|
|
283
|
+
Kind `untyped-calls` where it calls a function that declares no return, which a checker
|
|
284
|
+
reading no body takes for anything already: a top-level function of the module's by its name,
|
|
285
|
+
a method of `caller`'s class (or of its bases in the module) on `self` or `cls`, or another
|
|
286
|
+
checked file's function (`caller.untyped`), not awaited. Kind `unknown-calls` for any other.
|
|
287
|
+
Only the kinds among `kinds`.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
The inference, or `None`: for any other value, or where the module (`plan`) can't name `Any`.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
awaited: bool = isinstance(value, ast.Await)
|
|
294
|
+
call: ast.expr = value.value if isinstance(value, ast.Await) else value
|
|
295
|
+
if not isinstance(call, ast.Call):
|
|
296
|
+
return None
|
|
297
|
+
callee: FunctionDef | None = None
|
|
298
|
+
name: str
|
|
299
|
+
receiver: str
|
|
300
|
+
owner: str
|
|
301
|
+
match call.func, caller.owner:
|
|
302
|
+
case ast.Name(id=name), _ if name not in caller.bound:
|
|
303
|
+
callee = found.functions.get(name)
|
|
304
|
+
case ast.Attribute(value=ast.Name(id=receiver), attr=name), str() as owner if receiver in _RECEIVERS:
|
|
305
|
+
callee = _method(found, owner, name)
|
|
306
|
+
case _:
|
|
307
|
+
pass
|
|
308
|
+
untyped: bool = (
|
|
309
|
+
(not awaited and _imported(call.func, caller))
|
|
310
|
+
if callee is None
|
|
311
|
+
else _undeclared(callee, awaited=awaited)
|
|
312
|
+
)
|
|
313
|
+
kind: str = UNTYPED_CALLS if untyped else UNKNOWN_CALLS
|
|
314
|
+
spelled: str | None
|
|
315
|
+
if (spelled := plan.spell(ANY) if kind in kinds else None) is None:
|
|
316
|
+
return None
|
|
317
|
+
reason: str = (
|
|
318
|
+
f"a call of `{ast.unparse(call.func)}`, which declares no return"
|
|
319
|
+
if untyped
|
|
320
|
+
else "a call of no known type"
|
|
321
|
+
)
|
|
322
|
+
return Inference(spelled, reason, frozenset({kind}))
|