python-constricter 0.4.1__cp311-cp311-win_amd64.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-win_amd64.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp311-win_amd64.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-win_amd64.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp311-win_amd64.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-win_amd64.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp311-win_amd64.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp311-win_amd64.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp311-win_amd64.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp311-win_amd64.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp311-win_amd64.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp311-win_amd64.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp311-win_amd64.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp311-win_amd64.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp311-win_amd64.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp311-win_amd64.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp311-win_amd64.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp311-win_amd64.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp311-win_amd64.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp311-win_amd64.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp311-win_amd64.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp311-win_amd64.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp311-win_amd64.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp311-win_amd64.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp311-win_amd64.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp311-win_amd64.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp311-win_amd64.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp311-win_amd64.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp311-win_amd64.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp311-win_amd64.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,969 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A scope's late fixes: those that need the whole scope seen, run once it's checked (see `checker`).
|
|
3
|
+
|
|
4
|
+
`optionals` (`None`, then one type), `rebinds` (a fix refitted to every later value), `extended`
|
|
5
|
+
(a display widened to what's added to it later), `fills` (an empty container typed by what's added
|
|
6
|
+
to it), `shadowed` (a fix naming a value of its own scope,
|
|
7
|
+
dropped), `excused` (a fix of a name a `# type: ignore` line uses, dropped), `unchecked` (a guess
|
|
8
|
+
its function's use of the name makes an error of, dropped), and `finals` (LVA012's `Final`).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import bisect
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
15
|
+
from dataclasses import replace
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from typing import TYPE_CHECKING, Final, TypeAlias, cast
|
|
18
|
+
|
|
19
|
+
from constricter.fix.core.imports import imports_of
|
|
20
|
+
from constricter.fix.core.known import ImportPlan, Inference
|
|
21
|
+
from constricter.fix.values import callables, displays, hinted
|
|
22
|
+
from constricter.fix.values import fills as filling
|
|
23
|
+
from constricter.fix.values import unchecked as misused
|
|
24
|
+
from constricter.fix.values.doubts import contains_inner, narrowing, spelled_self
|
|
25
|
+
from constricter.fix.values.inference import RETURNED
|
|
26
|
+
from constricter.fix.values.narrowed import narrowed_at
|
|
27
|
+
from constricter.offences import (
|
|
28
|
+
CAN_BE_FINAL,
|
|
29
|
+
EMPTY_CONTAINERS,
|
|
30
|
+
MIXED_CONTAINERS,
|
|
31
|
+
UNANNOTATED,
|
|
32
|
+
UNANNOTATED_MEMBER,
|
|
33
|
+
UNIONS,
|
|
34
|
+
UNKNOWN_CALLS,
|
|
35
|
+
UNTYPED_CALLS,
|
|
36
|
+
UNTYPED_TARGET,
|
|
37
|
+
VAGUE_KIND,
|
|
38
|
+
WIDEN_KINDS,
|
|
39
|
+
Edit,
|
|
40
|
+
Fix,
|
|
41
|
+
Offence,
|
|
42
|
+
at,
|
|
43
|
+
)
|
|
44
|
+
from constricter.rules import widened
|
|
45
|
+
from constricter.rules.annotations import node_name, roots
|
|
46
|
+
from constricter.rules.flow import Binding, Lifetime, members
|
|
47
|
+
from constricter.rules.rebinding import Refit, refit
|
|
48
|
+
from constricter.rules.scope import FINAL_KIND, Scope, guesses_in
|
|
49
|
+
from constricter.rules.syntax import FunctionDef
|
|
50
|
+
from constricter.rules.walked import children
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from constricter.rules.scoped import Late, Plain
|
|
54
|
+
|
|
55
|
+
_DISCARD: Final = "_"
|
|
56
|
+
_OPTIONAL: Final = "optional" # the fix kind of a `None` default rebound to one type
|
|
57
|
+
_DECLARING: Final = frozenset({UNANNOTATED, UNTYPED_TARGET}) # the codes whose fix declares a name's type
|
|
58
|
+
_FILLED: Final = "filled" # the fix kind (and guessing mechanism) of an empty container filled later
|
|
59
|
+
_CONTAINER: Final = "container" # the fix kind of a display
|
|
60
|
+
_DICT: Final = "dict"
|
|
61
|
+
_FILLABLE: Final = frozenset({"list", "set", _DICT}) # the displays `extended` widens
|
|
62
|
+
# The builtin numbers, narrowest first: each takes the ones before it, as a type checker has it.
|
|
63
|
+
_NUMBERS: Final = ("bool", "int", "float", "complex")
|
|
64
|
+
_BAR: Final = " | "
|
|
65
|
+
_CALLERS: Final = "callers" # and of a parameter typed by what every call passes it
|
|
66
|
+
_NONE: Final = "None"
|
|
67
|
+
_FINAL: Final = "Final"
|
|
68
|
+
_TYPING_FINAL: Final = "typing.Final"
|
|
69
|
+
_FINALS: Final = frozenset({_TYPING_FINAL, "typing_extensions.Final"})
|
|
70
|
+
_NO_ALIASES: Final = frozenset[str]()
|
|
71
|
+
_INNER: Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda) # what reads a name later
|
|
72
|
+
_IGNORE: Final = re.compile(r"#\s*type:\s*ignore") # a comment excusing its line to a type checker
|
|
73
|
+
_WORD: Final = re.compile(r"[A-Za-z_]\w*")
|
|
74
|
+
# The guesses of a value a type checker takes for anything: held to no class until declared.
|
|
75
|
+
_UNCHECKED: Final = hinted.KINDS | {RETURNED}
|
|
76
|
+
# The wider types `widens` offers, once a scope's other fixes are settled.
|
|
77
|
+
_LATE_WIDENINGS: Final = frozenset(
|
|
78
|
+
{VAGUE_KIND, EMPTY_CONTAINERS, MIXED_CONTAINERS, UNTYPED_CALLS, UNKNOWN_CALLS},
|
|
79
|
+
)
|
|
80
|
+
# The arms a binding is in: each branching statement around it (by `id()`), and which of its arms.
|
|
81
|
+
_Arms: TypeAlias = tuple[tuple[int, int], ...]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def optionals(scope: Scope) -> None:
|
|
85
|
+
"""Offer `T | None` to a name first bound to `None`, then only ever to a known `T`.
|
|
86
|
+
|
|
87
|
+
Every later binding must have a type value flow is sure of (or `--fix` guessed, which makes
|
|
88
|
+
this a guess too), all the same one, not itself allowing `None`, and nothing in another scope
|
|
89
|
+
may write the name (`nonlocal`, `global`), nor read it: a function or lambda inside this one
|
|
90
|
+
reading it sees the declared `T | None`, where a type checker without it would see what the
|
|
91
|
+
name was narrowed to.
|
|
92
|
+
"""
|
|
93
|
+
index: int
|
|
94
|
+
o: Offence
|
|
95
|
+
enclosed: frozenset[str] | None = None
|
|
96
|
+
for index, o in enumerate(scope.offences):
|
|
97
|
+
lifetime: Lifetime | None = scope.flow.get(o.name)
|
|
98
|
+
if o.code != UNANNOTATED or _fixed(o) or lifetime is None or lifetime.escaped:
|
|
99
|
+
continue
|
|
100
|
+
if enclosed is None:
|
|
101
|
+
function: FunctionDef | None = scope.kind.function
|
|
102
|
+
inside: bool = function is not None and contains_inner(function, scope.settings.facts.inner)
|
|
103
|
+
enclosed = _enclosed_reads(function) if function is not None and inside else frozenset()
|
|
104
|
+
if o.name in enclosed:
|
|
105
|
+
continue
|
|
106
|
+
first: Binding
|
|
107
|
+
rest: list[Binding]
|
|
108
|
+
first, *rest = lifetime.bindings
|
|
109
|
+
guesses: list[Late] = [
|
|
110
|
+
binding.guess for binding in rest if binding.value is None and binding.guess is not None
|
|
111
|
+
]
|
|
112
|
+
types: set[str | None] = {binding.value or (binding.guess or (None,))[0] for binding in rest}
|
|
113
|
+
if first.at != (o.line, o.col) or first.value != _NONE or len(types) != 1:
|
|
114
|
+
continue
|
|
115
|
+
found: str | None = types.pop()
|
|
116
|
+
if found is None or _NONE in (members(found) or [found]):
|
|
117
|
+
continue
|
|
118
|
+
origins: frozenset[str] = frozenset[str]().union(*(rests for _, rests in guesses))
|
|
119
|
+
reason: str = f"`None`, then only `{found}`"
|
|
120
|
+
fix: Fix | None = scope.placed(
|
|
121
|
+
o.name,
|
|
122
|
+
Inference(f"{found} | None", reason, frozenset({_OPTIONAL}) | origins),
|
|
123
|
+
origins,
|
|
124
|
+
unsafe=bool(guesses),
|
|
125
|
+
)
|
|
126
|
+
scope.offences[index] = replace(o, edit=fix)
|
|
127
|
+
scope.inferred.late[o.name] = (f"{found} | None", origins)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def rebinds(scope: Scope) -> None:
|
|
131
|
+
"""Refit each first binding's fix to every value the name is bound to later (see `rebinding`).
|
|
132
|
+
|
|
133
|
+
A name something out of sight writes is left alone, and one bound again to a call on itself
|
|
134
|
+
whose type isn't known has no fix: a fifth of those guesses were wrong, on the packages measured.
|
|
135
|
+
Nor has one bound again to an empty display of another class (`flag = any(xs)`, then `flag = {}`),
|
|
136
|
+
or a loop's target bound again past the loop to a value whose type isn't known: another use of
|
|
137
|
+
the name, which the loop's values say nothing of.
|
|
138
|
+
"""
|
|
139
|
+
index: int
|
|
140
|
+
o: Offence
|
|
141
|
+
plan: ImportPlan | None = scope.settings.known.names.plan
|
|
142
|
+
self_type: str | None = None if plan is None else spelled_self(plan)
|
|
143
|
+
for index, o in enumerate(scope.offences):
|
|
144
|
+
lifetime: Lifetime | None = scope.flow.get(o.name)
|
|
145
|
+
fix: Fix | None = o.edit
|
|
146
|
+
if o.code != CAN_BE_FINAL and fix is not None and lifetime is not None and FINAL_KIND in fix.kinds:
|
|
147
|
+
if len(lifetime.bindings) > 1: # a constant's `Final` (see `_constant`): bound again, it's none
|
|
148
|
+
scope.offences[index] = replace(o, edit=None)
|
|
149
|
+
continue
|
|
150
|
+
# A widening's `Any` takes whatever the name is bound to later.
|
|
151
|
+
if (
|
|
152
|
+
o.code not in _DECLARING
|
|
153
|
+
or fix is None
|
|
154
|
+
or fix.kinds & WIDEN_KINDS
|
|
155
|
+
or lifetime is None
|
|
156
|
+
or lifetime.escaped
|
|
157
|
+
):
|
|
158
|
+
continue
|
|
159
|
+
first: Binding
|
|
160
|
+
rest: list[Binding]
|
|
161
|
+
first, *rest = lifetime.bindings
|
|
162
|
+
if first.at != (o.line, o.col) or not rest:
|
|
163
|
+
continue
|
|
164
|
+
function: FunctionDef | None = scope.kind.function
|
|
165
|
+
if function is not None and any(
|
|
166
|
+
binding.value is None
|
|
167
|
+
and binding.guess is None
|
|
168
|
+
and (
|
|
169
|
+
binding.at in _converted(function)
|
|
170
|
+
or (fix.annotation in _NUMBERS and binding.at in _operated(function))
|
|
171
|
+
or _emptied(function).get(binding.at) not in {None, *_heads(fix.annotation)}
|
|
172
|
+
)
|
|
173
|
+
for binding in rest
|
|
174
|
+
):
|
|
175
|
+
# Bound again to a call on itself, or a number by an operator with an operand, of a type
|
|
176
|
+
# that isn't known: a conversion, as often as not (`factor *= other`, a `Quantity` once
|
|
177
|
+
# `other` is one). A text or a container stays one by `+=`. Or to an empty display, whose
|
|
178
|
+
# class the fix doesn't name: a checker holds it to the fix's.
|
|
179
|
+
scope.offences[index] = replace(o, edit=None)
|
|
180
|
+
continue
|
|
181
|
+
if function is not None and any(
|
|
182
|
+
binding.value is None
|
|
183
|
+
and binding.guess is None
|
|
184
|
+
and (
|
|
185
|
+
_apart(function, first.at, binding.at)
|
|
186
|
+
or binding.at[0] > _looped(function).get(first.at, binding.at[0])
|
|
187
|
+
)
|
|
188
|
+
for binding in rest
|
|
189
|
+
):
|
|
190
|
+
# Bound in one arm of an `if` to a known type, in another to one that isn't: a checker
|
|
191
|
+
# that knows the other's holds it to the first. Or a loop's target, past the loop.
|
|
192
|
+
scope.offences[index] = replace(o, edit=None)
|
|
193
|
+
continue
|
|
194
|
+
found: Refit | Fix | None = refit(
|
|
195
|
+
o,
|
|
196
|
+
fix,
|
|
197
|
+
rest,
|
|
198
|
+
scope.settings.hierarchy,
|
|
199
|
+
(self_type, _unites(scope, o.name)),
|
|
200
|
+
)
|
|
201
|
+
refitted: Fix | None = _offered(scope, found) if isinstance(found, Refit) else found
|
|
202
|
+
scope.offences[index] = replace(o, edit=refitted)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _offered(scope: Scope, found: Refit) -> Fix | None:
|
|
206
|
+
"""Offer a refit fix, as the project's fix policy has it (see `offer`).
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The fix, or `None` if it isn't offered.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
return scope.offer(found.found, found.origins, unsafe=found.unsafe, edit=found.edit, span=found.span)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def extended(scope: Scope) -> None:
|
|
216
|
+
"""Widen a display's fix to what its function adds to it later: `xs = [0]`, then `xs.append(0.5)`.
|
|
217
|
+
|
|
218
|
+
A display is typed by its elements, as a type checker types it; what's added later may be more.
|
|
219
|
+
Each added value's type joins the display's: numbers as the widest of them (`int` and `float`,
|
|
220
|
+
a `float`), any others as their union, a guess (`displays.JOINED`); an addition whose type isn't
|
|
221
|
+
known, or one a test around it narrows (see `_narrowed_addition`), leaves the name no fix. Not
|
|
222
|
+
where a nested scope sees it, out of this function's sight.
|
|
223
|
+
"""
|
|
224
|
+
index: int
|
|
225
|
+
o: Offence
|
|
226
|
+
body: filling.Uses | None = None # read once, for the first display to judge
|
|
227
|
+
for index, o in enumerate(scope.offences):
|
|
228
|
+
lifetime: Lifetime | None = scope.flow.get(o.name)
|
|
229
|
+
fix: Fix | None = o.edit
|
|
230
|
+
if o.code != UNANNOTATED or fix is None or _CONTAINER not in fix.kinds:
|
|
231
|
+
continue
|
|
232
|
+
parts: tuple[str, list[str]] | None = _contained(fix.annotation)
|
|
233
|
+
if parts is None or lifetime is None or lifetime.escaped or len(lifetime.bindings) != 1:
|
|
234
|
+
continue
|
|
235
|
+
body = body or filling.uses(scope.kind.body())
|
|
236
|
+
additions: list[filling.Fill] = filling.additions(body, o.name, parts[0])
|
|
237
|
+
if not additions or o.name in body.nested:
|
|
238
|
+
continue
|
|
239
|
+
rows: list[list[Inference | None]] = [
|
|
240
|
+
filling.added(fill, parts[0], scope.settings.known, scope.inferred.types) for fill in additions
|
|
241
|
+
]
|
|
242
|
+
if any(part is None for row in rows for part in row) or any(
|
|
243
|
+
_narrowed_addition(scope, fill, row) for fill, row in zip(additions, rows, strict=True)
|
|
244
|
+
):
|
|
245
|
+
scope.offences[index] = replace(o, edit=None)
|
|
246
|
+
continue
|
|
247
|
+
scope.offences[index] = replace(
|
|
248
|
+
o,
|
|
249
|
+
edit=_widened(scope, o.name, fix, parts, cast("list[list[Inference]]", rows)),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _narrowed_addition(scope: Scope, fill: filling.Fill, row: list[Inference | None]) -> bool:
|
|
254
|
+
"""Check whether an addition is a read a test around it narrows: `d[k] = self.x` in `if self.x:`.
|
|
255
|
+
|
|
256
|
+
A checker adds the narrowed type (`str`, of a `str | None`), not the declared one `row` has.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Whether its key or value is.
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
added: list[ast.expr] = [part for part in (fill.key, fill.value) if part is not None]
|
|
263
|
+
return not fill.spread and any(
|
|
264
|
+
narrowed_at(
|
|
265
|
+
scope.settings.facts.narrowed,
|
|
266
|
+
part,
|
|
267
|
+
part.lineno,
|
|
268
|
+
union=inferred is not None and len(members(inferred.annotation) or ()) > 1,
|
|
269
|
+
)
|
|
270
|
+
for part, inferred in zip(added, row, strict=False)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _widened(
|
|
275
|
+
scope: Scope,
|
|
276
|
+
name: str,
|
|
277
|
+
fix: Fix,
|
|
278
|
+
parts: tuple[str, list[str]],
|
|
279
|
+
rows: list[list[Inference]],
|
|
280
|
+
) -> Fix | None:
|
|
281
|
+
"""Widen a display's `fix` (split into `parts`) to what's added to it: `rows`, one per addition.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
The widened fix, `fix` itself if what's added doesn't widen it, or `None` where the types
|
|
285
|
+
can't be joined.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
found: list[tuple[str, bool] | None] = [
|
|
289
|
+
_joined([own, *(row[at].annotation for row in rows)]) for at, own in enumerate(parts[1])
|
|
290
|
+
]
|
|
291
|
+
if None in found:
|
|
292
|
+
return None
|
|
293
|
+
joins: list[tuple[str, bool]] = cast("list[tuple[str, bool]]", found)
|
|
294
|
+
annotation: str
|
|
295
|
+
if (annotation := f"{parts[0]}[{', '.join(text for text, _ in joins)}]") == fix.annotation:
|
|
296
|
+
return fix
|
|
297
|
+
union: bool = any(each for _, each in joins)
|
|
298
|
+
origins: frozenset[str] = frozenset({displays.JOINED}) if union else frozenset()
|
|
299
|
+
kinds: frozenset[str] = fix.kinds.union(origins, *(part.kinds for row in rows for part in row))
|
|
300
|
+
reason: str = f"its display, and what the function adds to it ({len(rows)})"
|
|
301
|
+
return scope.placed(name, Inference(annotation, reason, kinds), origins, unsafe=fix.unsafe or union)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _contained(annotation: str) -> tuple[str, list[str]] | None:
|
|
305
|
+
"""Split a `list[T]`, `set[T]` or `dict[K, V]` into its container and its arguments.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Them, or `None` for any other annotation.
|
|
309
|
+
|
|
310
|
+
"""
|
|
311
|
+
node: ast.expr = ast.parse(annotation, mode="eval").body
|
|
312
|
+
kind: str
|
|
313
|
+
key: ast.expr
|
|
314
|
+
value: ast.expr
|
|
315
|
+
element: ast.expr
|
|
316
|
+
match node:
|
|
317
|
+
case ast.Subscript(value=ast.Name(id=kind), slice=ast.Tuple(elts=[key, value])) if kind == _DICT:
|
|
318
|
+
return kind, [ast.unparse(key), ast.unparse(value)]
|
|
319
|
+
case ast.Subscript(value=ast.Name(id=kind), slice=element) if kind in _FILLABLE - {_DICT}:
|
|
320
|
+
return kind, [ast.unparse(element)]
|
|
321
|
+
case _:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _joined(types: Sequence[str]) -> tuple[str, bool] | None:
|
|
326
|
+
"""Join a display's type for one argument with those of what's added there.
|
|
327
|
+
|
|
328
|
+
A union among them is its members: so `extended`, run again, keeps what it widened.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
The type, and whether it's a union of several; `None` where they can't be joined (see
|
|
332
|
+
`displays.union`).
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
distinct: list[str] = list(dict.fromkeys(member for each in types for member in each.split(_BAR)))
|
|
336
|
+
if len(distinct) == 1:
|
|
337
|
+
return distinct[0], False
|
|
338
|
+
if all(each in _NUMBERS for each in distinct):
|
|
339
|
+
return max(distinct, key=_NUMBERS.index), False
|
|
340
|
+
found: str | None = displays.union(distinct)
|
|
341
|
+
return None if found is None else (found, True)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def fills(scope: Scope) -> None:
|
|
345
|
+
"""Offer an empty container, bound nowhere else, the type of what its function adds to it.
|
|
346
|
+
|
|
347
|
+
A guess (see `constricter.fix.values.fills`), resting on `filled` for `unsafe-fix-select`. Not
|
|
348
|
+
where it's added a name its function tests (`isinstance(d, C)`, `d is None`, `if d`): a checker
|
|
349
|
+
takes the narrowed type for the elements'.
|
|
350
|
+
"""
|
|
351
|
+
index: int
|
|
352
|
+
o: Offence
|
|
353
|
+
body: filling.Uses | None = None # read once, for the first container to judge
|
|
354
|
+
for index, o in enumerate(scope.offences):
|
|
355
|
+
lifetime: Lifetime | None = scope.flow.get(o.name)
|
|
356
|
+
kind: str | None = scope.assignments.empty.get(o.name)
|
|
357
|
+
if o.code != UNANNOTATED or _fixed(o) or kind is None:
|
|
358
|
+
continue
|
|
359
|
+
if lifetime is None or lifetime.escaped or len(lifetime.bindings) != 1:
|
|
360
|
+
continue
|
|
361
|
+
body = body or filling.uses(scope.kind.body())
|
|
362
|
+
function: FunctionDef | None = scope.kind.function
|
|
363
|
+
if function is not None and filling.added_names(body, o.name, kind) & narrowing(function):
|
|
364
|
+
continue # what's added is narrowed where it's added
|
|
365
|
+
found: Inference | None
|
|
366
|
+
if (
|
|
367
|
+
found := filling.filled(
|
|
368
|
+
body,
|
|
369
|
+
o.name,
|
|
370
|
+
kind,
|
|
371
|
+
scope.settings.known,
|
|
372
|
+
scope.inferred.types,
|
|
373
|
+
)
|
|
374
|
+
) is None:
|
|
375
|
+
continue
|
|
376
|
+
fix: Fix | None = scope.placed(o.name, found, frozenset({_FILLED}), unsafe=True)
|
|
377
|
+
scope.offences[index] = replace(o, edit=fix)
|
|
378
|
+
# What the scope infers from it (its `return`s) knows its type, a guess.
|
|
379
|
+
scope.inferred.late[o.name] = (found.annotation, frozenset({_FILLED}))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def lambdas(scope: Scope) -> None:
|
|
383
|
+
"""Offer a lambda bound once to a name, whose body rests on its parameters, a `Callable[..., R]`.
|
|
384
|
+
|
|
385
|
+
By what every call of it in its function passes them (see `callables.called`): a guess, resting
|
|
386
|
+
on `callers`. Not where a call passes a local bound more than once, whose type there isn't known.
|
|
387
|
+
"""
|
|
388
|
+
function: FunctionDef | None = scope.kind.function
|
|
389
|
+
index: int
|
|
390
|
+
o: Offence
|
|
391
|
+
body: filling.Uses | None = None # read once, for the first lambda to judge
|
|
392
|
+
for index, o in enumerate(scope.offences):
|
|
393
|
+
bound: Plain | None = scope.assignments.plain.get(o.name)
|
|
394
|
+
lifetime: Lifetime | None = scope.flow.get(o.name)
|
|
395
|
+
if function is None or _fixed(o) or bound is None or not isinstance(bound[1], ast.Lambda):
|
|
396
|
+
continue
|
|
397
|
+
if lifetime is None or lifetime.escaped or len(lifetime.bindings) != 1:
|
|
398
|
+
continue
|
|
399
|
+
body = body or filling.uses(function.body)
|
|
400
|
+
found: tuple[Inference, list[ast.expr]] | None = callables.called(
|
|
401
|
+
bound[1],
|
|
402
|
+
o.name,
|
|
403
|
+
body,
|
|
404
|
+
scope.settings.known,
|
|
405
|
+
scope.inferred.types,
|
|
406
|
+
)
|
|
407
|
+
if found is None or any(
|
|
408
|
+
isinstance(node, ast.Name) and len(scope.lifetime(node.id).bindings) > 1
|
|
409
|
+
for arg in found[1]
|
|
410
|
+
for node in ast.walk(arg)
|
|
411
|
+
):
|
|
412
|
+
continue
|
|
413
|
+
origins: frozenset[str] = frozenset({_CALLERS}) | guesses_in(scope, found[1])[1]
|
|
414
|
+
scope.offences[index] = replace(o, edit=scope.placed(o.name, found[0], origins, unsafe=True))
|
|
415
|
+
scope.inferred.late[o.name] = (found[0].annotation, origins)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def widens(scope: Scope, tree: ast.Module) -> None:
|
|
419
|
+
"""Offer a wider type to each name's first binding that still has no fix, where `fix-widen` asks.
|
|
420
|
+
|
|
421
|
+
A type vaguer than `vague` allows, written anyway (`vague`: a guess); `Any` for the elements
|
|
422
|
+
of an empty container nothing fills, or of a display of mixed or unknown ones (see
|
|
423
|
+
`widened.container`); `Any` for a call of no known type (see `widened.called`), whatever the
|
|
424
|
+
name is bound to later. A plain assignment's is marked where its statement ends; a loop's, an
|
|
425
|
+
unpacking's or a `with`'s name, typed vaguely or taken of such a call, is declared before its
|
|
426
|
+
statement.
|
|
427
|
+
"""
|
|
428
|
+
widen: frozenset[str] = scope.settings.checks.fixes.widen
|
|
429
|
+
plan: ImportPlan | None = scope.settings.known.names.plan
|
|
430
|
+
if not widen & _LATE_WIDENINGS or plan is None or not scope.kind.fixable:
|
|
431
|
+
return
|
|
432
|
+
index: int
|
|
433
|
+
o: Offence
|
|
434
|
+
for index, o in enumerate(scope.offences):
|
|
435
|
+
bound: Plain | None = scope.assignments.plain.get(o.name)
|
|
436
|
+
lifetime: Lifetime | None = scope.flow.get(o.name)
|
|
437
|
+
if o.edit is not None or bound is None or at(bound[0]) != (o.line, o.col):
|
|
438
|
+
continue
|
|
439
|
+
if lifetime is None or lifetime.escaped: # written out of sight
|
|
440
|
+
continue
|
|
441
|
+
known: Inference | None = bound[2]
|
|
442
|
+
found: Inference | None = None
|
|
443
|
+
fix: Fix | None
|
|
444
|
+
# Bound again, it may be anything else: a wider type that isn't `Any` could be wrong.
|
|
445
|
+
once: bool = len(lifetime.bindings) == 1
|
|
446
|
+
# What's declared before its statement isn't its value, but taken from it: no container's.
|
|
447
|
+
whole: bool = once and o.name not in scope.assignments.chained
|
|
448
|
+
if once and known is not None and VAGUE_KIND in widen and not scope.writable(known):
|
|
449
|
+
found = known._replace(kinds=known.kinds | {VAGUE_KIND})
|
|
450
|
+
fix = scope.placed(o.name, found, frozenset({VAGUE_KIND}), unsafe=True)
|
|
451
|
+
else:
|
|
452
|
+
if known is None:
|
|
453
|
+
found = (widened.container(bound[1], plan, widen) if whole else None) or widened.called(
|
|
454
|
+
bound[1],
|
|
455
|
+
widened.Caller(
|
|
456
|
+
scope.settings.owners.get(id(scope.kind.function)),
|
|
457
|
+
scope.declared,
|
|
458
|
+
scope.settings.facts.untyped,
|
|
459
|
+
),
|
|
460
|
+
widened.own(tree),
|
|
461
|
+
plan,
|
|
462
|
+
widen,
|
|
463
|
+
)
|
|
464
|
+
fix = None if found is None else scope.placed(o.name, found, frozenset(), unsafe=False)
|
|
465
|
+
if fix is not None:
|
|
466
|
+
scope.offences[index] = replace(o, edit=fix._replace(mark=bound[1].end_lineno or o.line))
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def shadowed(scope: Scope) -> None:
|
|
470
|
+
"""Drop each fix whose annotation names a value its scope binds, which the name then is there.
|
|
471
|
+
|
|
472
|
+
In a function, a parameter or a local (`text: str = ""` under a parameter `str`); in any scope,
|
|
473
|
+
the name the fix annotates (`Row: type[Row] = load()`). A module body's other names may be types.
|
|
474
|
+
"""
|
|
475
|
+
function: FunctionDef | None = scope.kind.function
|
|
476
|
+
values: frozenset[str] = frozenset(
|
|
477
|
+
() if function is None else (*scope.first, *_parameters(function.args)),
|
|
478
|
+
)
|
|
479
|
+
index: int
|
|
480
|
+
o: Offence
|
|
481
|
+
for index, o in enumerate(scope.offences):
|
|
482
|
+
if o.edit is not None and roots(o.edit.annotation) & (values | {o.name}):
|
|
483
|
+
scope.offences[index] = replace(o, edit=None)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def excusing(lines: Sequence[str]) -> tuple[int, ...]:
|
|
487
|
+
"""Find the lines a comment excuses to a type checker (`# type: ignore`), once for a module's scopes.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Where each is, from 0, in order.
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
return tuple(index for index, line in enumerate(lines) if _IGNORE.search(line))
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def excused(scope: Scope) -> None:
|
|
497
|
+
"""Drop each fix of a name a line of its function excuses to a type checker (`# type: ignore`).
|
|
498
|
+
|
|
499
|
+
`return text # type: ignore[no-any-return]`: with `text` declared the line has nothing to
|
|
500
|
+
excuse, and a checker told to report unused comments reports that one. A module's or a class
|
|
501
|
+
body's name, by any line of the file: it's read everywhere.
|
|
502
|
+
"""
|
|
503
|
+
on: tuple[int, ...]
|
|
504
|
+
if not (on := scope.settings.facts.excused): # most modules: none of its lines is
|
|
505
|
+
return
|
|
506
|
+
function: FunctionDef | None = scope.kind.function
|
|
507
|
+
lines: Sequence[str] = scope.settings.lines
|
|
508
|
+
first: int = 0 if function is None else bisect.bisect_left(on, function.lineno - 1)
|
|
509
|
+
last: int = len(on) if function is None else bisect.bisect_left(on, function.end_lineno or len(lines))
|
|
510
|
+
excused_code: list[str]
|
|
511
|
+
if not (excused_code := [lines[line].partition("#")[0] for line in on[first:last]]):
|
|
512
|
+
return
|
|
513
|
+
index: int
|
|
514
|
+
o: Offence
|
|
515
|
+
for index, o in enumerate(scope.offences):
|
|
516
|
+
if o.edit is not None and any(o.name in _WORD.findall(code) for code in excused_code):
|
|
517
|
+
scope.offences[index] = replace(o, edit=None)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def unchecked(scope: Scope, tree: ast.Module) -> None:
|
|
521
|
+
"""Drop each guess that makes an error of what its function does with the name (see `misused`).
|
|
522
|
+
|
|
523
|
+
By an unannotated function's `return`s or a hint, which a checker held to nothing: a union,
|
|
524
|
+
bound once, of which an attribute or an item is taken where no test narrows it; a class of
|
|
525
|
+
the module's of which an attribute it hasn't is taken; and a name compared, formatted or stored
|
|
526
|
+
as its type doesn't allow. A certain fix too: a guess is one once a first pass has declared
|
|
527
|
+
what it rested on.
|
|
528
|
+
"""
|
|
529
|
+
function: FunctionDef | None
|
|
530
|
+
if (function := scope.kind.function) is None:
|
|
531
|
+
return
|
|
532
|
+
index: int
|
|
533
|
+
o: Offence
|
|
534
|
+
for index, o in enumerate(scope.offences):
|
|
535
|
+
fix: Fix | None = o.edit
|
|
536
|
+
if fix is None or not fix.kinds & _UNCHECKED:
|
|
537
|
+
continue
|
|
538
|
+
lifetime: Lifetime | None = scope.flow.get(o.name)
|
|
539
|
+
union: frozenset[str] = members(fix.annotation) or frozenset()
|
|
540
|
+
once: bool = lifetime is not None and len(lifetime.bindings) == 1
|
|
541
|
+
if (
|
|
542
|
+
misused.lacks(tree, function, o.name, fix.annotation)
|
|
543
|
+
or misused.misused(function, o.name, fix.annotation)
|
|
544
|
+
or (
|
|
545
|
+
once
|
|
546
|
+
and len(union) > 1
|
|
547
|
+
and misused.unnarrowed(function, o.name, scope.settings.facts.narrowed)
|
|
548
|
+
)
|
|
549
|
+
):
|
|
550
|
+
scope.offences[index] = replace(o, edit=None)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _parameters(args: ast.arguments) -> Iterator[str]:
|
|
554
|
+
"""Name a function's parameters, `*args` and `**kwargs` too.
|
|
555
|
+
|
|
556
|
+
Yields:
|
|
557
|
+
Each.
|
|
558
|
+
|
|
559
|
+
"""
|
|
560
|
+
arg: ast.arg | None
|
|
561
|
+
for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs, args.vararg, args.kwarg):
|
|
562
|
+
if arg is not None:
|
|
563
|
+
yield arg.arg
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def finals(scope: Scope) -> list[Offence]:
|
|
567
|
+
"""Find the names that could be `Final` (LVA012); run after `value_flow`, which marks escapes.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
An offence for each name bound exactly once, by a plain assignment outside any loop, and not
|
|
571
|
+
declared apart from it or already `Final`, nor written from another scope; with a fix
|
|
572
|
+
(see `_final`) where one is offered.
|
|
573
|
+
|
|
574
|
+
"""
|
|
575
|
+
found: list[Offence] = []
|
|
576
|
+
plan: ImportPlan | None = scope.settings.known.names.plan
|
|
577
|
+
# What else the module calls `Final`: `from typing import Final as F`.
|
|
578
|
+
aliases: frozenset[str] = frozenset(
|
|
579
|
+
() if plan is None else (name for name, origin in plan.bound.items() if origin in _FINALS),
|
|
580
|
+
)
|
|
581
|
+
name: str
|
|
582
|
+
lifetime: Lifetime
|
|
583
|
+
for name, lifetime in scope.flow.items():
|
|
584
|
+
if (
|
|
585
|
+
name == _DISCARD
|
|
586
|
+
or lifetime.escaped
|
|
587
|
+
or scope.kind.unannotated == UNANNOTATED_MEMBER
|
|
588
|
+
or len(lifetime.bindings) != 1
|
|
589
|
+
):
|
|
590
|
+
continue
|
|
591
|
+
where: tuple[int, int] = lifetime.bindings[0].at
|
|
592
|
+
if scope.assignments.found.get(name) == [(where, False)] and (
|
|
593
|
+
lifetime.declared is None
|
|
594
|
+
or (lifetime.declared_at == where and not is_final(lifetime.declared, aliases))
|
|
595
|
+
):
|
|
596
|
+
found.append(Offence(*where, name, CAN_BE_FINAL, _final(scope, name, lifetime)))
|
|
597
|
+
return [scope.evaluated(o) for o in found]
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _final(scope: Scope, name: str, lifetime: Lifetime) -> Fix | None:
|
|
601
|
+
"""Offer LVA012's `Final`: around the annotation there (`Final[int]`), or as one (`: Final`).
|
|
602
|
+
|
|
603
|
+
An unannotated name LVA001 would annotate gets `Final[T]` with LVA001's type instead, and
|
|
604
|
+
LVA001's own fix is dropped: both write where the name is bound, and this one does for both.
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
The fix, or `None` where the scope isn't fixed, the `final` kind isn't selected, an
|
|
608
|
+
annotation spans lines, or the module can't name `Final`.
|
|
609
|
+
|
|
610
|
+
"""
|
|
611
|
+
plan: ImportPlan | None = scope.settings.known.names.plan
|
|
612
|
+
spelled: str | None = None if plan is None else plan.spell(_TYPING_FINAL)
|
|
613
|
+
kinds: frozenset[str] = frozenset({FINAL_KIND})
|
|
614
|
+
if (
|
|
615
|
+
plan is None
|
|
616
|
+
or spelled is None
|
|
617
|
+
or not scope.kind.fixable
|
|
618
|
+
or not scope.settings.checks.fixes.allows(kinds)
|
|
619
|
+
):
|
|
620
|
+
return None
|
|
621
|
+
reason: str = "bound once, and never rebound"
|
|
622
|
+
if lifetime.declared is not None:
|
|
623
|
+
if lifetime.declared_span is None:
|
|
624
|
+
return None
|
|
625
|
+
line: int = lifetime.declared_at[0]
|
|
626
|
+
start: int
|
|
627
|
+
end: int
|
|
628
|
+
start, end = lifetime.declared_span
|
|
629
|
+
text: str = scope.settings.lines[line - 1].encode()[start:end].decode()
|
|
630
|
+
return _final_fix(
|
|
631
|
+
Fix(f"{spelled}[{text}]", reason, edit=Edit.REPLACE, span=(start, end), kinds=kinds),
|
|
632
|
+
plan,
|
|
633
|
+
)
|
|
634
|
+
index: int
|
|
635
|
+
o: Offence
|
|
636
|
+
for index, o in enumerate(scope.offences):
|
|
637
|
+
if o.name == name and o.code == UNANNOTATED and o.edit is not None and o.edit.edit == Edit.ANNOTATE:
|
|
638
|
+
scope.offences[index] = replace(o, edit=None)
|
|
639
|
+
return _final_fix(
|
|
640
|
+
o.edit._replace(annotation=f"{spelled}[{o.edit.annotation}]", kinds=o.edit.kinds | kinds),
|
|
641
|
+
plan,
|
|
642
|
+
)
|
|
643
|
+
return _final_fix(Fix(spelled, reason, kinds=kinds), plan)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def is_final(annotation: str, aliases: frozenset[str] = _NO_ALIASES) -> bool:
|
|
647
|
+
"""Check whether an annotation is `Final` (`Final[T]`, `typing.Final`, ...), or one of `aliases` for it.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Whether it is.
|
|
651
|
+
|
|
652
|
+
"""
|
|
653
|
+
# `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
|
|
654
|
+
root: ast.expr = ast.parse(annotation, mode="eval").body
|
|
655
|
+
return node_name(root.value if isinstance(root, ast.Subscript) else root) in aliases | {_FINAL}
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _final_fix(fix: Fix, plan: ImportPlan) -> Fix:
|
|
659
|
+
"""Give a `Final` fix the imports its annotation needs.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
It, with them.
|
|
663
|
+
|
|
664
|
+
"""
|
|
665
|
+
return fix._replace(imports=imports_of(fix.annotation, plan), after=plan.after)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _unites(scope: Scope, name: str) -> bool:
|
|
669
|
+
"""Check whether `name`'s values of several types may be declared their union (`fix-widen`'s `unions`).
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Whether they may: where it's asked for, in a function, of a name it doesn't change in place
|
|
673
|
+
(`name += more`, which no union of its types need take).
|
|
674
|
+
|
|
675
|
+
"""
|
|
676
|
+
function: FunctionDef | None = scope.kind.function
|
|
677
|
+
return (
|
|
678
|
+
UNIONS in scope.settings.checks.fixes.widen
|
|
679
|
+
and function is not None
|
|
680
|
+
and name not in _augmented(function)
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
@lru_cache(maxsize=64) # asked of each of a function's names bound again
|
|
685
|
+
def _augmented(function: FunctionDef) -> frozenset[str]:
|
|
686
|
+
"""Name what a function changes in place: `name += more`.
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
Each such name.
|
|
690
|
+
|
|
691
|
+
"""
|
|
692
|
+
return frozenset(
|
|
693
|
+
node.target.id
|
|
694
|
+
for node in ast.walk(function)
|
|
695
|
+
if isinstance(node, ast.AugAssign) and isinstance(node.target, ast.Name)
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@lru_cache(maxsize=1024) # asked of each of a function's names bound again
|
|
700
|
+
def _converted(function: FunctionDef) -> frozenset[tuple[int, int]]:
|
|
701
|
+
"""Find where a function binds a name to a call on the name itself.
|
|
702
|
+
|
|
703
|
+
One that takes it (`item = proper(item)`) or is its method (`match = match.groupdict()`), alone
|
|
704
|
+
or as an operand of `or` or of a conditional expression (`code = table.get(code) or SYNTAX`).
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
Where each such name is bound.
|
|
708
|
+
|
|
709
|
+
"""
|
|
710
|
+
found: set[tuple[int, int]] = set()
|
|
711
|
+
node: ast.AST
|
|
712
|
+
target: ast.expr
|
|
713
|
+
for node in ast.walk(function):
|
|
714
|
+
if isinstance(node, ast.Assign):
|
|
715
|
+
for target in node.targets:
|
|
716
|
+
if isinstance(target, ast.Name) and _on_itself(target.id, node.value):
|
|
717
|
+
found.add(at(target))
|
|
718
|
+
return frozenset(found)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _on_itself(name: str, value: ast.expr) -> bool:
|
|
722
|
+
"""Check whether `value` is a call on `name` (see `_converted`), or an operand of one is.
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
Whether it is.
|
|
726
|
+
|
|
727
|
+
"""
|
|
728
|
+
values: list[ast.expr]
|
|
729
|
+
body: ast.expr
|
|
730
|
+
orelse: ast.expr
|
|
731
|
+
func: ast.expr
|
|
732
|
+
args: list[ast.expr]
|
|
733
|
+
keywords: list[ast.keyword]
|
|
734
|
+
match value:
|
|
735
|
+
case ast.BoolOp(values=values):
|
|
736
|
+
return any(_on_itself(name, each) for each in values)
|
|
737
|
+
case ast.IfExp(body=body, orelse=orelse):
|
|
738
|
+
return _on_itself(name, body) or _on_itself(name, orelse)
|
|
739
|
+
case ast.Call(func=func, args=args, keywords=keywords):
|
|
740
|
+
return any(
|
|
741
|
+
isinstance(each, ast.Name) and each.id == name
|
|
742
|
+
for each in (_base(func), *args, *(keyword.value for keyword in keywords))
|
|
743
|
+
)
|
|
744
|
+
case _:
|
|
745
|
+
return False
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _base(node: ast.expr) -> ast.expr:
|
|
749
|
+
"""Find what a chain of attributes is read from: `self` in `self.table.get`.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
It; `node` itself, if it isn't an attribute.
|
|
753
|
+
|
|
754
|
+
"""
|
|
755
|
+
return _base(node.value) if isinstance(node, ast.Attribute) else node
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
# The classes whose call with no arguments is an empty display.
|
|
759
|
+
_EMPTY: Final = frozenset({"dict", "list", "set", "tuple"})
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@lru_cache(maxsize=1024) # asked of each of a function's names bound again
|
|
763
|
+
def _emptied(function: FunctionDef) -> Mapping[tuple[int, int], str]:
|
|
764
|
+
"""Find where a function binds a name to an empty display: `{}`, `[]`, `()` or `set()`, `dict()`.
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Where each such name is bound, and the display's class.
|
|
768
|
+
|
|
769
|
+
"""
|
|
770
|
+
found: dict[tuple[int, int], str] = {}
|
|
771
|
+
node: ast.AST
|
|
772
|
+
target: ast.expr
|
|
773
|
+
kind: str | None
|
|
774
|
+
for node in ast.walk(function):
|
|
775
|
+
if isinstance(node, ast.Assign) and (kind := _empty(node.value)) is not None:
|
|
776
|
+
for target in node.targets:
|
|
777
|
+
if isinstance(target, ast.Name):
|
|
778
|
+
found[at(target)] = kind
|
|
779
|
+
return found
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _empty(value: ast.expr) -> str | None:
|
|
783
|
+
"""Name the class of an empty display, or of a call of its class with no arguments.
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
It, or `None` for any other value.
|
|
787
|
+
|
|
788
|
+
"""
|
|
789
|
+
name: str
|
|
790
|
+
match value:
|
|
791
|
+
case ast.Dict(keys=[]):
|
|
792
|
+
return "dict"
|
|
793
|
+
case ast.List(elts=[]):
|
|
794
|
+
return "list"
|
|
795
|
+
case ast.Tuple(elts=[]):
|
|
796
|
+
return "tuple"
|
|
797
|
+
case ast.Call(func=ast.Name(id=name), args=[], keywords=[]) if name in _EMPTY:
|
|
798
|
+
return name
|
|
799
|
+
case _:
|
|
800
|
+
return None
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _heads(annotation: str) -> frozenset[str]:
|
|
804
|
+
"""Name the classes of an annotation's union's members: `dict` for `dict[str, int] | None`.
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
Them; none, if it can't be read.
|
|
808
|
+
|
|
809
|
+
"""
|
|
810
|
+
return frozenset(member.split("[", 1)[0] for member in members(annotation) or ())
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
@lru_cache(maxsize=1024) # asked of each of a function's names bound again
|
|
814
|
+
def _operated(function: FunctionDef) -> frozenset[tuple[int, int]]:
|
|
815
|
+
"""Find where a function binds a name by an augmented assignment: `factor *= other`.
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
Where each such name is bound.
|
|
819
|
+
|
|
820
|
+
"""
|
|
821
|
+
node: ast.AST
|
|
822
|
+
target: ast.Name
|
|
823
|
+
found: set[tuple[int, int]] = set()
|
|
824
|
+
for node in ast.walk(function):
|
|
825
|
+
match node:
|
|
826
|
+
case ast.AugAssign(target=ast.Name() as target):
|
|
827
|
+
found.add(at(target))
|
|
828
|
+
case _:
|
|
829
|
+
pass
|
|
830
|
+
return frozenset(found)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
@lru_cache(maxsize=1024) # asked of each of a function's names bound again
|
|
834
|
+
def _looped(function: FunctionDef) -> Mapping[tuple[int, int], int]:
|
|
835
|
+
"""Find a function's loops' targets: `for name in names`, `for key, value in pairs`.
|
|
836
|
+
|
|
837
|
+
Returns:
|
|
838
|
+
Where each target name is bound, and its loop's last line.
|
|
839
|
+
|
|
840
|
+
"""
|
|
841
|
+
found: dict[tuple[int, int], int] = {}
|
|
842
|
+
node: ast.AST
|
|
843
|
+
target: ast.AST
|
|
844
|
+
for node in ast.walk(function):
|
|
845
|
+
if isinstance(node, ast.For | ast.AsyncFor):
|
|
846
|
+
for target in ast.walk(node.target):
|
|
847
|
+
if isinstance(target, ast.Name):
|
|
848
|
+
found[at(target)] = node.end_lineno or node.lineno
|
|
849
|
+
return found
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _apart(function: FunctionDef, first: tuple[int, int], later: tuple[int, int]) -> bool:
|
|
853
|
+
"""Check whether two bindings of a function's are in different arms of one statement.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
Whether they are: only one of them runs.
|
|
857
|
+
|
|
858
|
+
"""
|
|
859
|
+
arms: Mapping[tuple[int, int], _Arms] = _arms(function)
|
|
860
|
+
taken: dict[int, int] = dict(arms.get(first, ()))
|
|
861
|
+
return any(taken.get(branch, arm) != arm for branch, arm in arms.get(later, ()))
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
@lru_cache(maxsize=64) # asked of each of a function's names bound again
|
|
865
|
+
def _arms(function: FunctionDef) -> Mapping[tuple[int, int], _Arms]:
|
|
866
|
+
"""Find, for each name a function's branches bind, the arms its binding is in.
|
|
867
|
+
|
|
868
|
+
An `if`'s body and its `else`, a `try`'s body and each handler, a `match`'s cases.
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
Where each such name is bound, and each branching statement around it (by `id()`) with which
|
|
872
|
+
of its arms that's in.
|
|
873
|
+
|
|
874
|
+
"""
|
|
875
|
+
found: dict[tuple[int, int], _Arms] = {}
|
|
876
|
+
# Each statement still to read, and the arms it's in; `None` ends it.
|
|
877
|
+
waiting: list[tuple[ast.stmt, _Arms] | None] = [
|
|
878
|
+
None,
|
|
879
|
+
*((stmt, ()) for stmt in function.body),
|
|
880
|
+
]
|
|
881
|
+
stmt: ast.stmt
|
|
882
|
+
inside: _Arms
|
|
883
|
+
for stmt, inside in iter(waiting.pop, None):
|
|
884
|
+
if isinstance(stmt, (*_INNER, ast.ClassDef)):
|
|
885
|
+
continue
|
|
886
|
+
if inside:
|
|
887
|
+
found.update(
|
|
888
|
+
(at(name), inside)
|
|
889
|
+
for target in _targets(stmt)
|
|
890
|
+
for name in ast.walk(target)
|
|
891
|
+
if isinstance(name, ast.Name)
|
|
892
|
+
)
|
|
893
|
+
blocks: list[list[ast.stmt]] = _blocks(stmt)
|
|
894
|
+
arm: int
|
|
895
|
+
block: list[ast.stmt]
|
|
896
|
+
for arm, block in enumerate(blocks):
|
|
897
|
+
within: tuple[tuple[int, int], ...] = (
|
|
898
|
+
(*inside, (id(stmt), arm)) if _alternatives(stmt) else inside
|
|
899
|
+
)
|
|
900
|
+
waiting.extend([(child, within) for child in block])
|
|
901
|
+
return found
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def _alternatives(stmt: ast.stmt) -> bool: # whether only one of its blocks runs, of those `_blocks` lists
|
|
905
|
+
return isinstance(stmt, ast.If | ast.Match | ast.Try | ast.TryStar)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _targets(stmt: ast.stmt) -> list[ast.expr]: # what a statement assigns to
|
|
909
|
+
match stmt:
|
|
910
|
+
case ast.Assign():
|
|
911
|
+
return stmt.targets
|
|
912
|
+
case ast.AnnAssign() | ast.AugAssign():
|
|
913
|
+
return [stmt.target]
|
|
914
|
+
case _:
|
|
915
|
+
return []
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _blocks(stmt: ast.stmt) -> list[list[ast.stmt]]:
|
|
919
|
+
"""List a statement's blocks, those of which only one runs each as an arm.
|
|
920
|
+
|
|
921
|
+
Returns:
|
|
922
|
+
An `if`'s body and its `else`, a `try`'s body (with its `else`) and each handler's, a
|
|
923
|
+
`match`'s cases; any other statement's blocks.
|
|
924
|
+
|
|
925
|
+
"""
|
|
926
|
+
match stmt:
|
|
927
|
+
case ast.If():
|
|
928
|
+
return [stmt.body, stmt.orelse]
|
|
929
|
+
case ast.Try() | ast.TryStar():
|
|
930
|
+
return [[*stmt.body, *stmt.orelse], *(handler.body for handler in stmt.handlers), stmt.finalbody]
|
|
931
|
+
case ast.Match():
|
|
932
|
+
return [case.body for case in stmt.cases]
|
|
933
|
+
case ast.For() | ast.AsyncFor() | ast.While():
|
|
934
|
+
return [stmt.body, stmt.orelse]
|
|
935
|
+
case ast.With() | ast.AsyncWith():
|
|
936
|
+
return [stmt.body]
|
|
937
|
+
case _:
|
|
938
|
+
return []
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
@lru_cache(maxsize=1024) # a function is finished twice: as it's checked, and with its module
|
|
942
|
+
def _enclosed_reads(function: FunctionDef) -> frozenset[str]:
|
|
943
|
+
"""Name what the functions and lambdas inside a function's body read.
|
|
944
|
+
|
|
945
|
+
Returns:
|
|
946
|
+
Each name read in one, however deep.
|
|
947
|
+
|
|
948
|
+
"""
|
|
949
|
+
found: set[str] = set()
|
|
950
|
+
# Each node of the body, and whether it's inside a function or lambda there; `None` ends it.
|
|
951
|
+
waiting: list[tuple[ast.AST, bool] | None] = [None, *((stmt, False) for stmt in function.body)]
|
|
952
|
+
node: ast.AST
|
|
953
|
+
inside: bool
|
|
954
|
+
for node, inside in iter(waiting.pop, None):
|
|
955
|
+
if inside and isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
|
|
956
|
+
found.add(node.id)
|
|
957
|
+
within: bool = inside or isinstance(node, _INNER)
|
|
958
|
+
waiting.extend([(child, within) for child in children(node)])
|
|
959
|
+
return frozenset(found)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _fixed(offence: Offence) -> bool:
|
|
963
|
+
"""Check whether an offence already has a fix a late one (`optionals`, `fills`) mustn't replace.
|
|
964
|
+
|
|
965
|
+
Returns:
|
|
966
|
+
Whether it has one, other than a hint's (`--infer-with`, `--infer-from`), which one would.
|
|
967
|
+
|
|
968
|
+
"""
|
|
969
|
+
return offence.edit is not None and not hinted.KINDS & offence.edit.kinds
|