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.
Files changed (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp311-win_amd64.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win_amd64.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win_amd64.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win_amd64.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win_amd64.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win_amd64.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win_amd64.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win_amd64.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win_amd64.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win_amd64.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win_amd64.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win_amd64.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win_amd64.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win_amd64.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win_amd64.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win_amd64.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win_amd64.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win_amd64.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win_amd64.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win_amd64.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win_amd64.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win_amd64.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win_amd64.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win_amd64.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win_amd64.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win_amd64.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win_amd64.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win_amd64.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win_amd64.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win_amd64.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win_amd64.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win_amd64.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win_amd64.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win_amd64.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win_amd64.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win_amd64.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win_amd64.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win_amd64.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win_amd64.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win_amd64.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win_amd64.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win_amd64.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win_amd64.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win_amd64.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win_amd64.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win_amd64.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win_amd64.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win_amd64.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-win_amd64.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp311-win_amd64.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win_amd64.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win_amd64.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win_amd64.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win_amd64.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win_amd64.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win_amd64.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win_amd64.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win_amd64.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win_amd64.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win_amd64.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win_amd64.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win_amd64.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win_amd64.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win_amd64.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win_amd64.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win_amd64.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win_amd64.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win_amd64.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win_amd64.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win_amd64.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win_amd64.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win_amd64.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win_amd64.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-win_amd64.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp311-win_amd64.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win_amd64.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win_amd64.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win_amd64.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win_amd64.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win_amd64.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win_amd64.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win_amd64.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win_amd64.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win_amd64.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win_amd64.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win_amd64.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win_amd64.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win_amd64.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win_amd64.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win_amd64.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win_amd64.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win_amd64.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win_amd64.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win_amd64.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win_amd64.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win_amd64.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win_amd64.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win_amd64.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-win_amd64.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. 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