python-constricter 0.4.1__cp312-cp312-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__.cp312-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp312-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp312-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp312-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp312-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp312-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp312-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp312-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp312-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp312-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp312-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp312-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp312-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp312-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp312-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp312-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp312-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp312-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp312-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp312-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp312-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp312-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp312-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp312-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp312-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp312-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp312-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp312-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp312-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp312-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp312-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp312-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp312-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp312-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp312-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp312-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp312-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp312-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp312-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__.cp312-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp312-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp312-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp312-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp312-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp312-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp312-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp312-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp312-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp312-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp312-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp312-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp312-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp312-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp312-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp312-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp312-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp312-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp312-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp312-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp312-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp312-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp312-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp312-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp312-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__.cp312-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp312-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp312-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp312-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp312-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp312-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp312-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp312-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp312-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp312-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp312-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp312-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp312-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp312-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp312-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp312-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp312-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp312-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp312-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp312-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp312-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp312-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp312-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp312-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp312-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,168 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a finished function's scope says its `return`s, `yield`s and stores to `self`'s attributes give.
|
|
3
|
+
|
|
4
|
+
Recorded for `constricter.fix.values.returned`, which types the function's calls and its class's attributes
|
|
5
|
+
from them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from typing import TYPE_CHECKING, Final
|
|
10
|
+
|
|
11
|
+
from constricter.fix.core.known import ImportPlan, Inference
|
|
12
|
+
from constricter.fix.values import bodies, entered, fills, stored
|
|
13
|
+
from constricter.fix.values.inference import RETURNED, inference
|
|
14
|
+
from constricter.fix.values.loops import looped
|
|
15
|
+
from constricter.fix.values.targets import iterated
|
|
16
|
+
from constricter.rules.scope import Scope, guesses_in
|
|
17
|
+
from constricter.rules.syntax import FunctionDef, own_nodes
|
|
18
|
+
from constricter.rules.walked import walk
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
|
|
23
|
+
_GENERATOR: Final = "collections.abc.Generator"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def returns(scope: Scope, func: FunctionDef, module: ast.Module) -> list[stored.Recorded]:
|
|
27
|
+
"""Record a finished function's `return` statements; a generator's one type instead (see `_generator`).
|
|
28
|
+
|
|
29
|
+
`module`: the function's, whose `yield`s say which functions to look in for one.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Each one's value.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(func, ast.FunctionDef) and bodies.is_generator(module, func):
|
|
36
|
+
return _generator(scope, func, bare=entered.is_manager(func))
|
|
37
|
+
return [
|
|
38
|
+
(None, frozenset()) if value is None else _recorded(scope, value) for value in scope.inferred.returns
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def assigned(scope: Scope) -> list[stored.Assigned]:
|
|
43
|
+
"""Record a finished function's `self.x = value` assignments, each value as a `return`'s is.
|
|
44
|
+
|
|
45
|
+
But a value reading a local bound more than once is unknown: the scope's type for it is its last
|
|
46
|
+
binding's, not what reaches the assignment (`x = None`, `if c: x = 1`, then `self.x = x`).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Each attribute, and its value's inference and guesses.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
return [
|
|
53
|
+
(attr, (None, frozenset()) if _rebound_in(scope, value) else _recorded(scope, value))
|
|
54
|
+
for attr, value in scope.inferred.assigned
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def used(scope: Scope, func: FunctionDef, module: ast.Module) -> list[stored.Used]:
|
|
59
|
+
"""Record a finished method's reads of the attributes its class binds to an empty container.
|
|
60
|
+
|
|
61
|
+
Each as `fills.stored` judges it; what one adds is typed as an assigned value is (see `assigned`).
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Each attribute, and what the read adds to it.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
kinds: Mapping[str, str]
|
|
68
|
+
if not (kinds := stored.empties(module, func)):
|
|
69
|
+
return []
|
|
70
|
+
return [
|
|
71
|
+
(attr, _added(scope, use, kinds[attr]) if isinstance(use, fills.Fill) else (() if use else None))
|
|
72
|
+
for attr, use in fills.stored(func.body, kinds)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _added(scope: Scope, use: fills.Fill, kind: str) -> stored.Added:
|
|
77
|
+
"""Record what one fill of an empty container's attribute adds to it.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Each part's inference and guesses (a `dict`'s key and value, any other's element).
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
values: list[ast.expr] = (
|
|
84
|
+
iterated(use.value) if use.spread else [part for part in (use.key, use.value) if part is not None]
|
|
85
|
+
)
|
|
86
|
+
if any(_rebound_in(scope, value) for value in values):
|
|
87
|
+
return ((None, frozenset()),)
|
|
88
|
+
origins: frozenset[str] = guesses_in(scope, values)[1]
|
|
89
|
+
return tuple(
|
|
90
|
+
(part, origins) for part in fills.added(use, kind, scope.settings.known, scope.inferred.types)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _recorded(scope: Scope, value: ast.expr) -> stored.Recorded:
|
|
95
|
+
"""Record a value as its finished function's scope sees it.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Its inference (`None` for an unknown value), and what that rests on if it's a guess.
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
found: Inference | None = inference(value, scope.settings.known, scope.inferred.types)
|
|
102
|
+
# What it rests on counts only for a typed value (see `returned`).
|
|
103
|
+
return found, frozenset() if found is None else guesses_in(scope, [value])[1]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _generator(scope: Scope, func: ast.FunctionDef, *, bare: bool = False) -> list[stored.Recorded]:
|
|
107
|
+
"""Record what calling a generator function gives, if its `yield`s decide it.
|
|
108
|
+
|
|
109
|
+
Each a statement of its own (nothing is sent in), all of one type `T`, and no `return` of a
|
|
110
|
+
value: a `Generator[T, None, None]`, named as the module can; `bare`: `T` itself, what `with`
|
|
111
|
+
gives of one `@contextmanager` makes a manager.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
That type, as a lone `return`'s value; or nothing.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
nodes: list[ast.AST] = list(own_nodes(func.body))
|
|
118
|
+
statements: set[int] = {id(node.value) for node in nodes if isinstance(node, ast.Expr)}
|
|
119
|
+
yields: list[ast.Yield | ast.YieldFrom] = [
|
|
120
|
+
node for node in nodes if isinstance(node, ast.Yield | ast.YieldFrom)
|
|
121
|
+
]
|
|
122
|
+
if any(value is not None for value in scope.inferred.returns) or any(
|
|
123
|
+
id(node) not in statements for node in yields
|
|
124
|
+
):
|
|
125
|
+
return []
|
|
126
|
+
found: list[stored.Recorded] = [_yielded(scope, node) for node in yields]
|
|
127
|
+
types: set[str | None] = {None if part is None else part.annotation for part, _ in found}
|
|
128
|
+
element: str | None = next(iter(types)) if len(types) == 1 else None
|
|
129
|
+
plan: ImportPlan | None = scope.settings.known.names.plan
|
|
130
|
+
spelled: str | None = None
|
|
131
|
+
if not bare and plan is not None and element is not None:
|
|
132
|
+
spelled = plan.spell(_GENERATOR)
|
|
133
|
+
if element is None or not (bare or spelled):
|
|
134
|
+
return []
|
|
135
|
+
text: str = element if bare else f"{spelled}[{element}, None, None]"
|
|
136
|
+
typed: Inference = Inference(text, "its `yield`s", frozenset({RETURNED}))
|
|
137
|
+
return [(typed, frozenset[str]().union(*(origins for _, origins in found)))]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _yielded(scope: Scope, node: ast.Yield | ast.YieldFrom) -> stored.Recorded:
|
|
141
|
+
"""Record what one `yield` gives: its value, or (`yield from`) each element of it.
|
|
142
|
+
|
|
143
|
+
Unknown for a bare `yield`, and a value reading a local bound more than once (see `assigned`).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Its inference, and what that rests on if it's a guess.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
value: ast.expr | None = node.value
|
|
150
|
+
if value is None or _rebound_in(scope, value):
|
|
151
|
+
return None, frozenset()
|
|
152
|
+
if isinstance(node, ast.Yield):
|
|
153
|
+
return _recorded(scope, value)
|
|
154
|
+
found: Inference | None = looped(value, scope.settings.known, scope.inferred.types, inference)
|
|
155
|
+
return found, frozenset() if found is None else guesses_in(scope, iterated(value))[1]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _rebound_in(scope: Scope, value: ast.expr) -> bool:
|
|
159
|
+
"""Check whether `value` reads a local of `scope` bound more than once.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Whether it does.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
return any(
|
|
166
|
+
isinstance(node, ast.Name) and node.id in scope.flow and len(scope.flow[node.id].bindings) > 1
|
|
167
|
+
for node in walk(value)
|
|
168
|
+
)
|
|
Binary file
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""LVA007: a name annotated again with the type it already has, in the same straight-line block."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from collections.abc import Callable, Iterator
|
|
6
|
+
from typing import Final, cast
|
|
7
|
+
|
|
8
|
+
from constricter.offences import REDUNDANT_TYPE, Edit, Fix, FixPolicy, Offence, at
|
|
9
|
+
from constricter.rules.syntax import BRANCHING, child_statements
|
|
10
|
+
from constricter.rules.walked import classes, of_type
|
|
11
|
+
|
|
12
|
+
_KINDS: Final = frozenset({"redundant"})
|
|
13
|
+
# The nodes with blocks of statements: all `_blocks` finds any in.
|
|
14
|
+
_BLOCKED: Final = (
|
|
15
|
+
ast.Module,
|
|
16
|
+
ast.FunctionDef,
|
|
17
|
+
ast.AsyncFunctionDef,
|
|
18
|
+
ast.ClassDef,
|
|
19
|
+
ast.With,
|
|
20
|
+
ast.AsyncWith,
|
|
21
|
+
*BRANCHING,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def redundant(tree: ast.Module, policy: FixPolicy | None = None) -> list[Offence]:
|
|
26
|
+
"""Find a name annotated again with the type it already has, in the same straight-line block.
|
|
27
|
+
|
|
28
|
+
Every block (a function, module or class body; an `if`'s body and its `orelse`; ...) is checked
|
|
29
|
+
on its own: two branches that never run in the same pass typing a name the same way isn't
|
|
30
|
+
redundant, so they're not compared against each other. Each gets a fix, if `policy` allows it,
|
|
31
|
+
dropping the annotation (see `_dropped`).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
One offence (LVA007) per redundant re-annotation.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
offences: list[Offence] = []
|
|
38
|
+
node: ast.AST
|
|
39
|
+
block: list[ast.stmt]
|
|
40
|
+
fixing: bool = (policy or FixPolicy()).allows(_KINDS)
|
|
41
|
+
in_class: set[int] = _class_level(tree)
|
|
42
|
+
for node in of_type(tree, *_BLOCKED):
|
|
43
|
+
for block in _blocks(node):
|
|
44
|
+
offences += _redundant_in(block, lambda stmt: fixing and id(stmt) not in in_class)
|
|
45
|
+
return offences
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _class_level(tree: ast.Module) -> set[int]:
|
|
49
|
+
"""Find the statements of every class body, through `if`/`try`/..., not in its methods.
|
|
50
|
+
|
|
51
|
+
`--fix` never edits them: a dataclass's annotations are its fields.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Their `id`s.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
return {id(stmt) for node in classes(tree) for stmt in _nested(node.body)}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _nested(body: list[ast.stmt]) -> Iterator[ast.stmt]:
|
|
61
|
+
"""Walk `body`'s statements, and theirs through `if`/`try`/... (not a function's or class's).
|
|
62
|
+
|
|
63
|
+
Yields:
|
|
64
|
+
Each statement.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
stmt: ast.stmt
|
|
68
|
+
for stmt in body:
|
|
69
|
+
yield stmt
|
|
70
|
+
yield from _nested(child_statements(stmt))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _blocks(node: ast.AST) -> Iterator[list[ast.stmt]]:
|
|
74
|
+
"""Find the straight-line blocks of statements directly in `node`.
|
|
75
|
+
|
|
76
|
+
Yields:
|
|
77
|
+
Each one (an `if`'s body and its `orelse` separately, and likewise for the other compound
|
|
78
|
+
statements with more than one: they run in different passes, if at all).
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
body: list[ast.stmt]
|
|
82
|
+
orelse: list[ast.stmt]
|
|
83
|
+
handlers: list[ast.ExceptHandler]
|
|
84
|
+
finalbody: list[ast.stmt]
|
|
85
|
+
handler: ast.ExceptHandler
|
|
86
|
+
case: ast.match_case
|
|
87
|
+
match node:
|
|
88
|
+
case (
|
|
89
|
+
ast.Module(body=body)
|
|
90
|
+
| ast.FunctionDef(body=body)
|
|
91
|
+
| ast.AsyncFunctionDef(body=body)
|
|
92
|
+
| ast.ClassDef(body=body)
|
|
93
|
+
| ast.With(body=body)
|
|
94
|
+
| ast.AsyncWith(body=body)
|
|
95
|
+
):
|
|
96
|
+
yield body
|
|
97
|
+
case (
|
|
98
|
+
ast.If(body=body, orelse=orelse)
|
|
99
|
+
| ast.For(body=body, orelse=orelse)
|
|
100
|
+
| ast.AsyncFor(body=body, orelse=orelse)
|
|
101
|
+
| ast.While(body=body, orelse=orelse)
|
|
102
|
+
):
|
|
103
|
+
yield body
|
|
104
|
+
yield orelse # empty when there's no `else`, which is harmless: nothing to find in it
|
|
105
|
+
case (
|
|
106
|
+
ast.Try(body=body, handlers=handlers, orelse=orelse, finalbody=finalbody)
|
|
107
|
+
| ast.TryStar(body=body, handlers=handlers, orelse=orelse, finalbody=finalbody)
|
|
108
|
+
):
|
|
109
|
+
yield body
|
|
110
|
+
for handler in handlers:
|
|
111
|
+
yield handler.body
|
|
112
|
+
yield orelse
|
|
113
|
+
yield finalbody
|
|
114
|
+
case _: # a `match`: the last of the kinds with blocks (`_BLOCKED`), the only ones asked about
|
|
115
|
+
for case in cast("ast.Match", node).cases:
|
|
116
|
+
yield case.body
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _redundant_in(block: list[ast.stmt], fixable: Callable[[ast.stmt], bool]) -> list[Offence]:
|
|
120
|
+
"""Find a name in `block` annotated the same way twice.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
One offence per repeat, at the later statement, with a fix where `fixable` says so.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
offences: list[Offence] = []
|
|
127
|
+
seen: dict[str, str] = {}
|
|
128
|
+
stmt: ast.stmt
|
|
129
|
+
name: str
|
|
130
|
+
annotation: ast.expr
|
|
131
|
+
for stmt in block:
|
|
132
|
+
match stmt:
|
|
133
|
+
case ast.AnnAssign(target=ast.Name(id=name), annotation=annotation):
|
|
134
|
+
text: str = ast.unparse(annotation)
|
|
135
|
+
if seen.get(name) == text:
|
|
136
|
+
edit: Fix | None = _dropped(stmt) if fixable(stmt) else None
|
|
137
|
+
offences.append(Offence(*at(stmt), name, REDUNDANT_TYPE, edit))
|
|
138
|
+
seen[name] = text
|
|
139
|
+
case _:
|
|
140
|
+
pass
|
|
141
|
+
return offences
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _dropped(stmt: ast.AnnAssign) -> Fix | None:
|
|
145
|
+
"""Drop a repeated annotation: `x: int = 2` becomes `x = 2`.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The fix (writing nothing over `: int`), or `None` for a bare declaration (`x: int`, whose
|
|
149
|
+
whole statement would have to go) or one spread over lines.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
if stmt.value is None or stmt.annotation.end_lineno != stmt.target.lineno:
|
|
153
|
+
return None
|
|
154
|
+
return Fix(
|
|
155
|
+
"",
|
|
156
|
+
"the type it already has",
|
|
157
|
+
edit=Edit.REPLACE,
|
|
158
|
+
span=(stmt.target.end_col_offset or 0, stmt.annotation.end_col_offset or 0),
|
|
159
|
+
kinds=_KINDS,
|
|
160
|
+
)
|
|
Binary file
|