python-constricter 0.4.1__cp314-cp314-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__.cp314-win_amd64.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp314-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__.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp314-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__.cp314-win_amd64.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp314-win_amd64.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp314-win_amd64.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp314-win_amd64.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp314-win_amd64.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp314-win_amd64.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp314-win_amd64.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp314-win_amd64.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp314-win_amd64.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp314-win_amd64.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp314-win_amd64.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp314-win_amd64.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp314-win_amd64.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp314-win_amd64.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp314-win_amd64.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp314-win_amd64.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp314-win_amd64.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp314-win_amd64.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp314-win_amd64.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp314-win_amd64.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp314-win_amd64.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp314-win_amd64.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp314-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,802 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""One scope being checked: what it binds and reports, what `--fix` knows of it, and its late fixes."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
from constricter.fix.core.imports import guarded_imports, imports_of, through, unwritable
|
|
11
|
+
from constricter.fix.core.known import Hints, ImportPlan, Inference, Typed
|
|
12
|
+
from constricter.fix.libraries import stdlib
|
|
13
|
+
from constricter.fix.libraries.library import chains
|
|
14
|
+
from constricter.fix.values import aliased, callables, fills, hinted
|
|
15
|
+
from constricter.fix.values.doubts import (
|
|
16
|
+
Facts,
|
|
17
|
+
Owner,
|
|
18
|
+
as_declared,
|
|
19
|
+
bare,
|
|
20
|
+
corrected,
|
|
21
|
+
doubts,
|
|
22
|
+
is_constant,
|
|
23
|
+
narrowed_first,
|
|
24
|
+
narrowing,
|
|
25
|
+
says_self,
|
|
26
|
+
tested,
|
|
27
|
+
undeclared,
|
|
28
|
+
)
|
|
29
|
+
from constricter.fix.values.guesses import guessed, guessing
|
|
30
|
+
from constricter.fix.values.inference import inference, inferred
|
|
31
|
+
from constricter.fix.values.members import fits, parsed
|
|
32
|
+
from constricter.fix.values.narrowed import narrowed_at, read_narrowed
|
|
33
|
+
from constricter.offences import (
|
|
34
|
+
LONG_TUPLE,
|
|
35
|
+
MEMBER,
|
|
36
|
+
NESTED_TYPE,
|
|
37
|
+
UNANNOTATED,
|
|
38
|
+
UNANNOTATED_MEMBER,
|
|
39
|
+
UNTYPED_PARAMETERS,
|
|
40
|
+
VAGUE_TYPE,
|
|
41
|
+
WIDEN_KINDS,
|
|
42
|
+
Checks,
|
|
43
|
+
Edit,
|
|
44
|
+
Fix,
|
|
45
|
+
FixPolicy,
|
|
46
|
+
Offence,
|
|
47
|
+
at,
|
|
48
|
+
)
|
|
49
|
+
from constricter.rules import widened
|
|
50
|
+
from constricter.rules.annotations import (
|
|
51
|
+
depth,
|
|
52
|
+
is_composite,
|
|
53
|
+
length,
|
|
54
|
+
node_name,
|
|
55
|
+
roots,
|
|
56
|
+
vague_fits,
|
|
57
|
+
)
|
|
58
|
+
from constricter.rules.flow import Finding, Lifetime, findings, members
|
|
59
|
+
from constricter.rules.quoted import newer, quote
|
|
60
|
+
from constricter.rules.scoped import Assignments, Inferred, Kind, Late, Settings
|
|
61
|
+
from constricter.rules.syntax import FunctionDef
|
|
62
|
+
|
|
63
|
+
_SELF: Final = "self"
|
|
64
|
+
_NONE: Final = "None"
|
|
65
|
+
_CLASSMETHOD: Final = "classmethod"
|
|
66
|
+
_STATICMETHOD: Final = "staticmethod"
|
|
67
|
+
_SELF_WORD: Final = re.compile(r"\bSelf\b") # `Self` in an annotation, `typing.Self` too
|
|
68
|
+
FINAL_KIND: Final = "final" # the fix kind of LVA012's `Final`
|
|
69
|
+
_TYPING_FINAL: Final = "typing.Final"
|
|
70
|
+
_LITERAL_DOUBT: Final = frozenset({"literal"}) # a constant's literal, which `doubts` makes a guess
|
|
71
|
+
_READS: Final = (ast.Name, ast.Attribute, ast.Subscript) # a read of a value a type checker may narrow
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Scope:
|
|
75
|
+
"""One function body: names bound so far and offences found."""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
declared: set[str],
|
|
80
|
+
nested: list[FunctionDef],
|
|
81
|
+
settings: Settings,
|
|
82
|
+
kind: "Kind | None" = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Start a scope where `declared` are bound already, collecting nested functions into `nested`."""
|
|
85
|
+
self.declared: set[str] = declared
|
|
86
|
+
self.nested: list[FunctionDef] = nested
|
|
87
|
+
self.settings: Settings = settings
|
|
88
|
+
self.kind: Kind = kind or Kind(UNANNOTATED, fixable=True)
|
|
89
|
+
self.offences: list[Offence] = []
|
|
90
|
+
self.first: list[str] = [] # each first binding the rules cover, typed or not
|
|
91
|
+
# What `--fix` knows of the names bound so far.
|
|
92
|
+
self.inferred: Inferred = Inferred(unions=settings.known.indirect.unions)
|
|
93
|
+
self.flow: dict[str, Lifetime] = {} # every binding of each name, for value flow
|
|
94
|
+
self.assignments: Assignments = Assignments() # for LVA012
|
|
95
|
+
|
|
96
|
+
def lifetime(self, name: str) -> Lifetime:
|
|
97
|
+
"""Find `name`'s value-flow record, starting one if it has none.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
It.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
return self.flow.setdefault(name, Lifetime())
|
|
104
|
+
|
|
105
|
+
def bind(
|
|
106
|
+
self,
|
|
107
|
+
name: str,
|
|
108
|
+
where: tuple[int, int],
|
|
109
|
+
code: str | None,
|
|
110
|
+
fix: Fix | None = None,
|
|
111
|
+
typed: str | Late | None = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Bind `name` to a value value flow can't see; unless it's already bound, report `code`.
|
|
114
|
+
|
|
115
|
+
`code` is reported at `(line, col)` (`None` means typed), offering `fix` if there's one.
|
|
116
|
+
`typed`: the value's type, if `--fix` knows it (a loop's element), which a fix for the name's
|
|
117
|
+
first binding is held to (see `rebinding`): its text where it's certain, which a name
|
|
118
|
+
declared already is then bound to; else the guess and what it rests on, as `assign` binds one.
|
|
119
|
+
"""
|
|
120
|
+
certain: str | None = typed if isinstance(typed, str) else None
|
|
121
|
+
guess: Late | None = (typed, frozenset()) if isinstance(typed, str) else typed
|
|
122
|
+
self.lifetime(name).bind(where, None, guess)
|
|
123
|
+
if name in self.declared:
|
|
124
|
+
self.inferred.rebound(name, certain)
|
|
125
|
+
self._first(name, where, code, fix)
|
|
126
|
+
|
|
127
|
+
def assign(
|
|
128
|
+
self,
|
|
129
|
+
target: ast.Name,
|
|
130
|
+
code: str | None,
|
|
131
|
+
value: ast.expr,
|
|
132
|
+
chained: ast.stmt | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Bind `target` to `value` (`name = value`), offering `--fix`'s annotation for it.
|
|
135
|
+
|
|
136
|
+
One of a `chained` assignment's names (`a = b = 0`), which can't be annotated where it's bound,
|
|
137
|
+
is offered a declaration before it instead (`a: int`); not a `Final` one, which needs its value.
|
|
138
|
+
"""
|
|
139
|
+
name: str = target.id
|
|
140
|
+
if aliased.factory(value, self.settings.known): # a type: nothing to annotate it with
|
|
141
|
+
self.declared.add(name)
|
|
142
|
+
self.opaque([name])
|
|
143
|
+
return
|
|
144
|
+
again: bool = name in self.declared
|
|
145
|
+
fix: Inference | None
|
|
146
|
+
unsafe: bool
|
|
147
|
+
origins: frozenset[str]
|
|
148
|
+
constant: bool = (
|
|
149
|
+
self.kind.function is None
|
|
150
|
+
and self.kind.owner is None
|
|
151
|
+
and is_constant(name)
|
|
152
|
+
and name in self.settings.facts.passed
|
|
153
|
+
and chained is None
|
|
154
|
+
)
|
|
155
|
+
fix, unsafe, origins = self.valued(value, target.lineno, constant=constant)
|
|
156
|
+
if fix is not None and constant and origins == _LITERAL_DOUBT:
|
|
157
|
+
fix = self._constant(name, fix)
|
|
158
|
+
# Value flow's type is `--fix`'s own, if certain: worked out once, here, for both.
|
|
159
|
+
certain: str | None = certain_type(self, value, (None if fix is None else fix.annotation, unsafe))
|
|
160
|
+
if self.kind.owner is not None:
|
|
161
|
+
fix, unsafe, origins = self._member(name, fix, value)
|
|
162
|
+
if fix is None or not self.writable(fix):
|
|
163
|
+
fix, unsafe, origins = self._unvalued(
|
|
164
|
+
target,
|
|
165
|
+
value,
|
|
166
|
+
(fix, unsafe, origins),
|
|
167
|
+
alias=chained is None and not again,
|
|
168
|
+
)
|
|
169
|
+
# Past `vague`, a type holds no other binding's fix to it: unknown there, and to value flow.
|
|
170
|
+
vague: bool = fix is not None and not self.writable(fix)
|
|
171
|
+
guess: Late | None = (fix.annotation, origins) if fix is not None and unsafe else None
|
|
172
|
+
self.lifetime(name).bind(at(target), certain, None if vague else guess)
|
|
173
|
+
self.assigned(name, at(target))
|
|
174
|
+
if again:
|
|
175
|
+
self.inferred.rebound(name, fix.annotation if fix and vague and not unsafe else certain, guess)
|
|
176
|
+
kind: str | None
|
|
177
|
+
if (kind := fills.empty(value)) is not None:
|
|
178
|
+
_ = self.assignments.empty.setdefault(name, kind)
|
|
179
|
+
if chained is not None and not again:
|
|
180
|
+
_ = self.assignments.chained.setdefault(name, (chained.lineno, chained.col_offset))
|
|
181
|
+
# A wider type only for a name first bound here, by a statement of its own.
|
|
182
|
+
self._first(
|
|
183
|
+
name,
|
|
184
|
+
at(target),
|
|
185
|
+
code,
|
|
186
|
+
self._offered(target, (fix, unsafe, origins), None if chained is not None or again else value),
|
|
187
|
+
)
|
|
188
|
+
if fix is not None and aliased.declares(parsed(fix.annotation)):
|
|
189
|
+
self.inferred.aliases.add(name) # `TypeAlias` isn't its value's type
|
|
190
|
+
elif fix is not None:
|
|
191
|
+
self.inferred.learn(name, fix.annotation, origins if unsafe else None, again=again)
|
|
192
|
+
# A call of no type to write still types what's read of the name (`both.out`), bound once.
|
|
193
|
+
self.inferred.types.hold(
|
|
194
|
+
name,
|
|
195
|
+
value if fix is None and not again and chains(value, self.settings.known) else None,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _unvalued(
|
|
199
|
+
self,
|
|
200
|
+
target: ast.Name,
|
|
201
|
+
value: ast.expr,
|
|
202
|
+
vague: tuple[Inference | None, bool, frozenset[str]],
|
|
203
|
+
*,
|
|
204
|
+
alias: bool,
|
|
205
|
+
) -> tuple[Inference | None, bool, frozenset[str]]:
|
|
206
|
+
"""Type a name its value gives no type: a module's type alias (`alias`: it may be one), or a hint.
|
|
207
|
+
|
|
208
|
+
`vague`: what `valued` made of it, a type too vague to write: the name's still, with neither.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The inference, whether it's a guess, and what it rests on, as `valued` does.
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
checks: Checks = self.settings.checks
|
|
215
|
+
found: tuple[Inference, bool, frozenset[str]] | None = (
|
|
216
|
+
aliased.declared(
|
|
217
|
+
target,
|
|
218
|
+
value,
|
|
219
|
+
self.settings.known,
|
|
220
|
+
self.settings.facts,
|
|
221
|
+
(frozenset(self.inferred.aliases), checks.min_python),
|
|
222
|
+
)
|
|
223
|
+
if alias and self.kind.function is None and self.kind.owner is None
|
|
224
|
+
else None
|
|
225
|
+
)
|
|
226
|
+
function: FunctionDef | None = self.kind.function
|
|
227
|
+
owner: str | None = self.kind.owner
|
|
228
|
+
types: Typed = self.inferred.types
|
|
229
|
+
facts: Facts = self.settings.facts
|
|
230
|
+
# In a function; or a module's or a plain class's body, for a name first bound there.
|
|
231
|
+
body: bool = alias and (owner is None or owner in self.settings.known.class_side.plain)
|
|
232
|
+
called: tuple[Inference, ast.expr] | None = (
|
|
233
|
+
None
|
|
234
|
+
if found is not None or vague[0] is not None or not (function is not None or body)
|
|
235
|
+
else callables.aliased(
|
|
236
|
+
target,
|
|
237
|
+
value,
|
|
238
|
+
callables.Site(function, owner, facts.attributed, facts.positional),
|
|
239
|
+
self.settings.known,
|
|
240
|
+
(types, lambda arg: inference(arg, self.settings.known, types)),
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
if called is not None and owner is not None: # a class's variable: a guess (see `_member`)
|
|
244
|
+
found = called[0]._replace(kinds=called[0].kinds | {MEMBER}), True, frozenset({MEMBER})
|
|
245
|
+
elif called is not None:
|
|
246
|
+
found = called[0], *guesses_in(self, [called[1]])
|
|
247
|
+
hint: Inference | None
|
|
248
|
+
if found is None and (hint := self.hint(target, value)) is not None:
|
|
249
|
+
found = hint, True, hint.kinds
|
|
250
|
+
return found or vague
|
|
251
|
+
|
|
252
|
+
def _member(
|
|
253
|
+
self,
|
|
254
|
+
name: str,
|
|
255
|
+
fix: Inference | None,
|
|
256
|
+
value: ast.expr,
|
|
257
|
+
) -> tuple[Inference | None, bool, frozenset[str]]:
|
|
258
|
+
"""Offer a class body's fix only for a plain class's variable typed by its `value`: a guess.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
The inference, whether it's a guess, and what it rests on, as `valued` does; none for any
|
|
262
|
+
other binding of a class body.
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
typed: str | None = self.settings.known.class_side.variables.get(self.kind.owner or "", {}).get(name)
|
|
266
|
+
# The variable's type is its value's, or what a base holds it to (`maxDiff`, an `int | None`).
|
|
267
|
+
held: frozenset[str] = frozenset() if typed is None else self.inferred.members(typed)
|
|
268
|
+
if fix is None and _NONE in held and isinstance(value, ast.Constant) and value.value is None:
|
|
269
|
+
fix = Inference(_NONE, "what a class above it declares it", frozenset())
|
|
270
|
+
# A checked file's class, by name: the index typed it so (`plain.settled`).
|
|
271
|
+
if fix is None and typed is not None and isinstance(value, ast.Name) and typed == f"type[{value.id}]":
|
|
272
|
+
fix = Inference(typed, f"the class `{value.id}` it names", frozenset())
|
|
273
|
+
if fix is None or typed is None or fix.annotation not in held:
|
|
274
|
+
return None, False, frozenset()
|
|
275
|
+
return fix._replace(annotation=typed, kinds=fix.kinds | {MEMBER}), True, frozenset({MEMBER})
|
|
276
|
+
|
|
277
|
+
def valued(
|
|
278
|
+
self,
|
|
279
|
+
value: ast.expr,
|
|
280
|
+
line: int,
|
|
281
|
+
*,
|
|
282
|
+
constant: bool = False,
|
|
283
|
+
) -> tuple[Inference | None, bool, frozenset[str]]:
|
|
284
|
+
"""Infer the annotation of a name bound to `value` on `line`, as a type checker would see it.
|
|
285
|
+
|
|
286
|
+
`constant`: whether the name is an ALL_CAPS module-level one passed to a call (see `doubts`).
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
The inference (`None` where there's none to offer), whether it's a guess, and what a guess
|
|
290
|
+
rests on.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
facts: Facts = self.settings.facts
|
|
294
|
+
function: FunctionDef | None = self.kind.function
|
|
295
|
+
fix: Inference | None
|
|
296
|
+
if (fix := inference(value, self.settings.known, self.inferred.types)) is not None:
|
|
297
|
+
fix = corrected(value, fix, self._owner(), facts.generics, self.settings.known.names.plan)
|
|
298
|
+
# An alias of a union is narrowed as the union it names.
|
|
299
|
+
named: Inference | None = (
|
|
300
|
+
None
|
|
301
|
+
if fix is None
|
|
302
|
+
else fix._replace(annotation=self.inferred.unions.get(fix.annotation, fix.annotation))
|
|
303
|
+
)
|
|
304
|
+
if named is not None and (
|
|
305
|
+
(
|
|
306
|
+
narrowed_first(value, named)
|
|
307
|
+
and not (named.annotation != _NONE and as_declared(value, function, facts.tests))
|
|
308
|
+
)
|
|
309
|
+
or narrowed_at(facts.narrowed, value, line, union=len(members(named.annotation) or ()) > 1)
|
|
310
|
+
or self._takes_narrowed(value, named, line)
|
|
311
|
+
):
|
|
312
|
+
fix = None
|
|
313
|
+
if fix is None or named is None:
|
|
314
|
+
return None, False, frozenset()
|
|
315
|
+
unsafe: bool
|
|
316
|
+
origins: frozenset[str]
|
|
317
|
+
unsafe, origins = guesses_in(self, [value])
|
|
318
|
+
if not unsafe:
|
|
319
|
+
origins = doubts(
|
|
320
|
+
value,
|
|
321
|
+
fix,
|
|
322
|
+
constant=constant,
|
|
323
|
+
narrowed=frozenset() if function is None else tested(function, facts.tests),
|
|
324
|
+
)
|
|
325
|
+
unsafe = bool(origins)
|
|
326
|
+
return fix, unsafe, origins
|
|
327
|
+
|
|
328
|
+
def _takes_narrowed(self, value: ast.expr, fix: Inference, line: int) -> bool:
|
|
329
|
+
"""Check whether a fix takes its type from a read narrowed at `line`: `deepcopy(x)`, in `if x:`.
|
|
330
|
+
|
|
331
|
+
Or holds one in a display (`[x]`, `[self.x]`, there): its elements are the narrowed type's.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Whether it does: the read has another type there than it's declared.
|
|
335
|
+
|
|
336
|
+
"""
|
|
337
|
+
types: Mapping[str, str] = self.inferred.types
|
|
338
|
+
held: list[str] = [
|
|
339
|
+
ast.unparse(element)
|
|
340
|
+
for element in (value.elts if isinstance(value, ast.List | ast.Set | ast.Tuple) else ())
|
|
341
|
+
if isinstance(element, _READS)
|
|
342
|
+
]
|
|
343
|
+
return any(
|
|
344
|
+
read_narrowed(
|
|
345
|
+
self.settings.facts.narrowed,
|
|
346
|
+
read,
|
|
347
|
+
line,
|
|
348
|
+
union=len(self.inferred.members(types.get(read, ""))) > 1,
|
|
349
|
+
)
|
|
350
|
+
for read in (*fix.reads, *held)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def _constant(self, name: str, fix: Inference) -> Inference | None:
|
|
354
|
+
"""Declare an ALL_CAPS constant passed to a call `Final`, which keeps its literal's `Literal` type.
|
|
355
|
+
|
|
356
|
+
`str` would widen it, where a parameter may take only some values (see `doubts`). A guess
|
|
357
|
+
still (a name bound again is left alone, see `rebinds`), and only where the module can name
|
|
358
|
+
`Final`. What's read of `name` has its literal's type, `fix`'s.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
The `Final` inference, or `None`.
|
|
362
|
+
|
|
363
|
+
"""
|
|
364
|
+
plan: ImportPlan | None = self.settings.known.names.plan
|
|
365
|
+
spelled: str | None = None if plan is None else plan.spell(_TYPING_FINAL)
|
|
366
|
+
self.inferred.learn(name, fix.annotation, _LITERAL_DOUBT)
|
|
367
|
+
return (
|
|
368
|
+
None
|
|
369
|
+
if spelled is None
|
|
370
|
+
else Inference(spelled, f"{fix.reason}, a constant passed to a call", fix.kinds | {FINAL_KIND})
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def _owner(self) -> Owner | None:
|
|
374
|
+
"""Find the class this scope is a method of, if its instance (or class) is a `Self` in it.
|
|
375
|
+
|
|
376
|
+
That's a method whose signature says `Self`: in any other, mypy takes `self` for its class.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
It, or `None` outside such a method, or in one whose first parameter isn't `self` or a
|
|
380
|
+
classmethod's.
|
|
381
|
+
|
|
382
|
+
"""
|
|
383
|
+
function: FunctionDef | None = self.kind.function
|
|
384
|
+
owner: str | None = None if function is None else self.settings.owners.get(id(function))
|
|
385
|
+
if function is None or owner is None:
|
|
386
|
+
return None
|
|
387
|
+
args: list[ast.arg] = [*function.args.posonlyargs, *function.args.args]
|
|
388
|
+
decorators: list[str] = [node_name(d) for d in function.decorator_list]
|
|
389
|
+
if (
|
|
390
|
+
not args
|
|
391
|
+
or _STATICMETHOD in decorators
|
|
392
|
+
or not (args[0].arg == _SELF or decorators == [_CLASSMETHOD])
|
|
393
|
+
or not says_self(function)
|
|
394
|
+
):
|
|
395
|
+
return None
|
|
396
|
+
return Owner(owner, args[0].arg, self.settings.known.class_side.lineage.selfish_of(owner))
|
|
397
|
+
|
|
398
|
+
def hint(self, target: ast.Name, value: ast.expr | None = None) -> Inference | None:
|
|
399
|
+
"""Type `target` by the type checkers' hints for it (`--infer-with`): the first the file can use.
|
|
400
|
+
|
|
401
|
+
`value`: what a plain assignment binds it to. One hinted `TypeAlias` is declared an alias,
|
|
402
|
+
in a module body, where it's written as a type made of others (not a bare class's alias,
|
|
403
|
+
which declared one loses the class's type parameters).
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
The inference (a guess), or `None`.
|
|
407
|
+
|
|
408
|
+
"""
|
|
409
|
+
where: tuple[int, int] = (target.lineno, target.end_col_offset or 0)
|
|
410
|
+
composite: bool = value is not None and self.kind.function is None and is_composite(value)
|
|
411
|
+
found: Hints
|
|
412
|
+
for found in self.settings.hints:
|
|
413
|
+
# Not used at all where not offered: what follows from it would rest on it unseen.
|
|
414
|
+
text: str | None = (
|
|
415
|
+
found.types.get(where) if self.settings.checks.fixes.allows(frozenset({found.kind})) else None
|
|
416
|
+
)
|
|
417
|
+
typed: str | None
|
|
418
|
+
if text == hinted.ALIAS:
|
|
419
|
+
if composite and (typed := hinted.type_alias(self.settings.known, target.lineno)) is not None:
|
|
420
|
+
return hinted.inference(typed, found)
|
|
421
|
+
continue
|
|
422
|
+
if (
|
|
423
|
+
text is not None
|
|
424
|
+
and (
|
|
425
|
+
typed := hinted.hinted(
|
|
426
|
+
text,
|
|
427
|
+
self.settings.known,
|
|
428
|
+
nesting=self.settings.checks.nesting,
|
|
429
|
+
# A module body's annotation is evaluated there: only what's bound before it will do.
|
|
430
|
+
before=target.lineno if self.kind.function is None else None,
|
|
431
|
+
offered=found.offered.get(where),
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
# A generic class a checker prints bare has arguments it doesn't know.
|
|
435
|
+
and not bare(typed, self.settings.facts.generics)
|
|
436
|
+
and not hinted.renames(target.id, typed, local=self.kind.function is not None)
|
|
437
|
+
and not (self._undeclared(typed) or self._misread(target.id, typed))
|
|
438
|
+
):
|
|
439
|
+
return hinted.inference(typed, found)
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
def _misread(self, name: str, annotation: str) -> bool:
|
|
443
|
+
"""Check whether a hint is one this scope's function reads otherwise than declared.
|
|
444
|
+
|
|
445
|
+
A union for a name it never tests: used as it is, it's taken for one of its members
|
|
446
|
+
(`opt.cb`, hinted `Option | None`), which the union declared would make an error; tested
|
|
447
|
+
(`if opt is None`), a checker narrows it. And any hint for a name it returns where its
|
|
448
|
+
signature says `Self`: the checker hints the class, which isn't `Self`. And a hint naming
|
|
449
|
+
`Self` where its signature doesn't: pyrefly takes `self` for `Self` everywhere
|
|
450
|
+
(`type(self)(...)`, `self[mask]`), mypy only there, and for its class elsewhere.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Whether it is.
|
|
454
|
+
|
|
455
|
+
"""
|
|
456
|
+
function: FunctionDef | None
|
|
457
|
+
if (function := self.kind.function) is None:
|
|
458
|
+
return False
|
|
459
|
+
if _SELF_WORD.search(annotation) is not None and not says_self(function):
|
|
460
|
+
return True
|
|
461
|
+
if len(members(annotation) or ()) > 1:
|
|
462
|
+
return name not in narrowing(function)
|
|
463
|
+
return says_self(function) and any(
|
|
464
|
+
isinstance(node.value, ast.Name) and node.value.id == name
|
|
465
|
+
for node in ast.walk(function)
|
|
466
|
+
if isinstance(node, ast.Return)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def _undeclared(self, annotation: str) -> bool:
|
|
470
|
+
"""Check whether a hint names a type variable this scope's function doesn't declare.
|
|
471
|
+
|
|
472
|
+
See `undeclared`.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Whether it does.
|
|
476
|
+
|
|
477
|
+
"""
|
|
478
|
+
facts: Facts = self.settings.facts
|
|
479
|
+
function: FunctionDef | None = self.kind.function
|
|
480
|
+
owner: str | None = None if function is None else self.settings.owners.get(id(function))
|
|
481
|
+
return undeclared(annotation, function, facts.type_vars, facts.bound.get(owner or "", frozenset()))
|
|
482
|
+
|
|
483
|
+
def placed(self, name: str, fix: Inference, origins: frozenset[str], *, unsafe: bool) -> Fix | None:
|
|
484
|
+
"""Offer `name`'s fix where it can go: at its binding, or declared before a chained assignment.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
The fix (see `offer`).
|
|
488
|
+
|
|
489
|
+
"""
|
|
490
|
+
span: tuple[int, int] | None
|
|
491
|
+
if (span := self.assignments.chained.get(name)) is None:
|
|
492
|
+
return self.offer(fix, origins, unsafe=unsafe)
|
|
493
|
+
return self.offer(fix, origins, unsafe=unsafe, edit=Edit.DECLARE, span=span)
|
|
494
|
+
|
|
495
|
+
def offer(
|
|
496
|
+
self,
|
|
497
|
+
fix: Inference,
|
|
498
|
+
origins: frozenset[str],
|
|
499
|
+
*,
|
|
500
|
+
unsafe: bool,
|
|
501
|
+
edit: Edit = Edit.ANNOTATE,
|
|
502
|
+
span: tuple[int, int] = (0, 0),
|
|
503
|
+
) -> Fix | None:
|
|
504
|
+
"""Offer `fix` as the project's fix policy has it: selected, and a guess unless trusted.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
The fix, or `None` if a mechanism that decided it isn't selected, or is ignored, or it's
|
|
508
|
+
vaguer than `vague` allows (it would be LVA005).
|
|
509
|
+
|
|
510
|
+
"""
|
|
511
|
+
policy: FixPolicy = self.settings.checks.fixes
|
|
512
|
+
widening: bool = bool(fix.kinds & WIDEN_KINDS) # written whatever `vague` allows, and marked
|
|
513
|
+
if not policy.allows(fix.kinds) or not (widening or self.writable(fix)):
|
|
514
|
+
return None
|
|
515
|
+
certain: bool = not unsafe or policy.trusts(origins)
|
|
516
|
+
plan: ImportPlan = self.settings.known.names.plan or ImportPlan({}, frozenset(), 0)
|
|
517
|
+
if self.kind.owner is None: # a class's own body names its classes bare
|
|
518
|
+
fix = fix._replace(annotation=through(fix.annotation, plan, self.settings.facts.nested))
|
|
519
|
+
if unwritable(fix.annotation, plan, self.settings.facts.nested):
|
|
520
|
+
return None
|
|
521
|
+
guarded: tuple[str, ...] = guarded_imports(fix.annotation, plan)
|
|
522
|
+
guard: str | None = ""
|
|
523
|
+
if guarded and plan.checking.block == (0, 0) and (guard := plan.type_checking()) is None:
|
|
524
|
+
return None # nothing can be `TYPE_CHECKING` to import them under
|
|
525
|
+
return Fix(
|
|
526
|
+
fix.annotation,
|
|
527
|
+
fix.reason,
|
|
528
|
+
not certain,
|
|
529
|
+
edit,
|
|
530
|
+
span,
|
|
531
|
+
fix.kinds,
|
|
532
|
+
# With the import `TYPE_CHECKING` takes, for a new block.
|
|
533
|
+
imports=imports_of(f"{fix.annotation} | {guard}" if guard else fix.annotation, plan),
|
|
534
|
+
after=plan.after,
|
|
535
|
+
guarded=guarded,
|
|
536
|
+
guard=guard or "",
|
|
537
|
+
block=plan.checking.block,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def _offered(
|
|
541
|
+
self,
|
|
542
|
+
target: ast.Name,
|
|
543
|
+
typed: tuple[Inference | None, bool, frozenset[str]],
|
|
544
|
+
widen: ast.expr | None,
|
|
545
|
+
) -> Fix | None:
|
|
546
|
+
"""Offer `target`'s fix as `typed` has it (see `valued`), else a wider one for its value `widen`.
|
|
547
|
+
|
|
548
|
+
A wider type (see `widened.wider`) only where `fix-widen` asks for it.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
The fix, or `None`.
|
|
552
|
+
|
|
553
|
+
"""
|
|
554
|
+
found: Inference | None
|
|
555
|
+
fix: Fix | None
|
|
556
|
+
if typed[0] is not None and (fix := self.placed(target.id, typed[0], typed[2], unsafe=typed[1])):
|
|
557
|
+
return fix
|
|
558
|
+
if widen is None:
|
|
559
|
+
return None
|
|
560
|
+
self.assignments.plain[target.id] = (target, widen, typed[0])
|
|
561
|
+
if UNTYPED_PARAMETERS not in self.settings.checks.fixes.widen:
|
|
562
|
+
return None
|
|
563
|
+
found = widened.wider(
|
|
564
|
+
self.kind.function,
|
|
565
|
+
(target, widen),
|
|
566
|
+
self.inferred.types,
|
|
567
|
+
self.settings.known.names.plan,
|
|
568
|
+
)
|
|
569
|
+
return None if found is None else self.offer(found, frozenset(), unsafe=False)
|
|
570
|
+
|
|
571
|
+
def writable(self, fix: Inference) -> bool:
|
|
572
|
+
"""Check that a type is no vaguer than `vague` allows: one that isn't is known, and never written.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Whether it is.
|
|
576
|
+
|
|
577
|
+
"""
|
|
578
|
+
return fits(fix.annotation, self.settings.checks.vague)
|
|
579
|
+
|
|
580
|
+
def _first(
|
|
581
|
+
self,
|
|
582
|
+
name: str,
|
|
583
|
+
where: tuple[int, int],
|
|
584
|
+
code: str | None,
|
|
585
|
+
fix: Fix | None,
|
|
586
|
+
) -> None:
|
|
587
|
+
"""Unless `name` is already bound, record its first binding, reporting `code` (`None`: typed).
|
|
588
|
+
|
|
589
|
+
The offence offers `fix`, unless this is a class body (a dataclass's annotation is a field),
|
|
590
|
+
where only a plain class's variable's (`_member`) is.
|
|
591
|
+
"""
|
|
592
|
+
if name not in self.declared:
|
|
593
|
+
self.declared.add(name)
|
|
594
|
+
self.first.append(name)
|
|
595
|
+
if code is not None:
|
|
596
|
+
offered: bool = self.kind.fixable or (fix is not None and MEMBER in fix.kinds)
|
|
597
|
+
self.offences.append(Offence(*where, name, code, fix if offered else None))
|
|
598
|
+
|
|
599
|
+
def declare(self, name: str) -> None:
|
|
600
|
+
"""Bind `name` by an annotation (`name: T`, `name: T = ...`): a typed first binding."""
|
|
601
|
+
# position is unused: `code` is `None`, so nothing is reported
|
|
602
|
+
self._first(name, (0, 0), None, None)
|
|
603
|
+
|
|
604
|
+
def opaque(self, names: Iterable[str]) -> None:
|
|
605
|
+
"""Record bindings whose values value flow can't see: an import, a `def`, `except ... as`."""
|
|
606
|
+
name: str
|
|
607
|
+
for name in names:
|
|
608
|
+
self.lifetime(name).bind((0, 0), None)
|
|
609
|
+
|
|
610
|
+
def mark_escaped(self, escaped: frozenset[str]) -> None:
|
|
611
|
+
"""Mark the names something out of sight writes: `escaped` ones, or any in a module or class body.
|
|
612
|
+
|
|
613
|
+
A module or class body's names are state anything can rebind out of its sight (an instance's
|
|
614
|
+
`self.x = ...`, another module's `mod.X = ...`, `monkeypatch`, `globals().update(...)`).
|
|
615
|
+
"""
|
|
616
|
+
name: str
|
|
617
|
+
body: bool = self.kind.unannotated == UNANNOTATED_MEMBER
|
|
618
|
+
for name in self.flow.keys() if body else self.flow.keys() & escaped:
|
|
619
|
+
self.flow[name].escaped = True
|
|
620
|
+
|
|
621
|
+
def value_flow(self, escaped: frozenset[str], skipped: frozenset[str]) -> list[Finding]:
|
|
622
|
+
"""Compare each name's values with its declared type.
|
|
623
|
+
|
|
624
|
+
`escaped` names are written elsewhere; `skipped` ones aren't compared at all.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
The findings, for names the rules cover.
|
|
628
|
+
|
|
629
|
+
"""
|
|
630
|
+
self.mark_escaped(escaped)
|
|
631
|
+
return [
|
|
632
|
+
found
|
|
633
|
+
for name, lifetime in self.flow.items()
|
|
634
|
+
if self._covered(name) and name not in skipped
|
|
635
|
+
for found in findings(name, lifetime, self.settings.hierarchy)
|
|
636
|
+
]
|
|
637
|
+
|
|
638
|
+
def assigned(self, name: str, where: tuple[int, int]) -> None:
|
|
639
|
+
"""Record a plain assignment to `name` at `where`, for LVA012."""
|
|
640
|
+
self.assignments.found.setdefault(name, []).append((where, self.assignments.looping > 0))
|
|
641
|
+
|
|
642
|
+
def evaluated(self, offence: Offence) -> Offence:
|
|
643
|
+
"""Quote a module body's fix that can't be evaluated when the module runs (unless it postpones them).
|
|
644
|
+
|
|
645
|
+
One whose type names what the module imports for type checking alone (under an `if` on a
|
|
646
|
+
flag), or binds only further down (both unbound then, as a quoted annotation's or a
|
|
647
|
+
`# type:` comment's names may be), or subscripts a
|
|
648
|
+
standard-library class that can't be at run time (`itertools.count[int]`). And any scope's
|
|
649
|
+
written as the project's oldest Python (`min-python`) doesn't read a type: `int | None`
|
|
650
|
+
before 3.10, `list[int]` before 3.9.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
The offence, its fix quoted if it must be.
|
|
654
|
+
|
|
655
|
+
"""
|
|
656
|
+
plan: ImportPlan | None = self.settings.known.names.plan
|
|
657
|
+
fix: Fix | None = offence.edit
|
|
658
|
+
if fix is None or plan is None or plan.postponed:
|
|
659
|
+
return offence
|
|
660
|
+
if newer(fix.annotation, self.settings.checks.min_python):
|
|
661
|
+
return replace(offence, edit=fix._replace(annotation=quote(fix.annotation)))
|
|
662
|
+
if self.kind.function is not None:
|
|
663
|
+
return offence
|
|
664
|
+
line: int = fix.span[0] if fix.edit is Edit.DECLARE else offence.line
|
|
665
|
+
names: frozenset[str] = roots(fix.annotation)
|
|
666
|
+
unbound: bool = bool(plan.guarded.keys() & names) or any(
|
|
667
|
+
name in self.settings.facts.checking or plan.defined.get(name, 0) >= line for name in names
|
|
668
|
+
)
|
|
669
|
+
if not unbound and stdlib.evaluable(fix.annotation, self.settings.known):
|
|
670
|
+
return offence
|
|
671
|
+
return replace(offence, edit=fix._replace(annotation=quote(fix.annotation)))
|
|
672
|
+
|
|
673
|
+
def _covered(self, name: str) -> bool:
|
|
674
|
+
"""Check whether the rules cover `name` here.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
Whether they do; a module or class body's dunder names are exempt.
|
|
678
|
+
|
|
679
|
+
"""
|
|
680
|
+
return self.kind.unannotated != UNANNOTATED_MEMBER or not (
|
|
681
|
+
name.startswith("__") and name.endswith("__")
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
def reported(self) -> list[Offence]:
|
|
685
|
+
"""Filter the offences found.
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
All but those for exempt names.
|
|
689
|
+
|
|
690
|
+
"""
|
|
691
|
+
return [self.evaluated(o) for o in self.offences if self._covered(o.name)]
|
|
692
|
+
|
|
693
|
+
def bound(self) -> list[str]:
|
|
694
|
+
"""List the first bindings the rules cover.
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
Their names, typed or not.
|
|
698
|
+
|
|
699
|
+
"""
|
|
700
|
+
return [name for name in self.first if self._covered(name)]
|
|
701
|
+
|
|
702
|
+
def annotation(self, name: str, annotation: ast.expr, *, marked: bool = False) -> None:
|
|
703
|
+
"""Report a vague annotation (LVA005), too deep a one (LVA006), or too long a tuple (LVA011).
|
|
704
|
+
|
|
705
|
+
Vague as far as `vague` allows (see `vague_fits`) isn't LVA005, nor is a widening's own
|
|
706
|
+
(`marked`: see `widened`).
|
|
707
|
+
"""
|
|
708
|
+
if not marked and not vague_fits(
|
|
709
|
+
annotation,
|
|
710
|
+
self.settings.checks.vague,
|
|
711
|
+
):
|
|
712
|
+
self.offences.append(Offence(*at(annotation), name, VAGUE_TYPE))
|
|
713
|
+
if depth(annotation) >= self.settings.checks.nesting:
|
|
714
|
+
self.offences.append(Offence(*at(annotation), name, NESTED_TYPE))
|
|
715
|
+
longest: int
|
|
716
|
+
if (longest := length(annotation)) > self.settings.known.limits.max_length:
|
|
717
|
+
self.offences.append(Offence(*at(annotation), name, LONG_TUPLE, detail=str(longest)))
|
|
718
|
+
|
|
719
|
+
def unannotated(self, type_comment: str | None) -> str | None:
|
|
720
|
+
"""Decide the code for an `=` or `with` binding.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
The code, or `None` if a counted type comment types it.
|
|
724
|
+
|
|
725
|
+
"""
|
|
726
|
+
return (
|
|
727
|
+
None if type_comment is not None and self.settings.checks.type_comments else self.kind.unannotated
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def guesses_in(scope: Scope, values: Iterable[ast.expr]) -> tuple[bool, frozenset[str]]:
|
|
732
|
+
"""Work out whether any of `values`' types is a guess, and what the guesses rest on (see `guessing`).
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
Whether one is, and the guessing mechanisms (`FIX_KINDS`) theirs rest on.
|
|
736
|
+
|
|
737
|
+
"""
|
|
738
|
+
unsafe: bool = False
|
|
739
|
+
found: set[str] = set()
|
|
740
|
+
value: ast.expr
|
|
741
|
+
for value in values:
|
|
742
|
+
guess: bool
|
|
743
|
+
origins: frozenset[str]
|
|
744
|
+
guess, origins = guessing(
|
|
745
|
+
value,
|
|
746
|
+
scope.settings.known,
|
|
747
|
+
frozenset(scope.inferred.guesses),
|
|
748
|
+
scope.inferred.origins,
|
|
749
|
+
scope.inferred.types,
|
|
750
|
+
)
|
|
751
|
+
unsafe = unsafe or guess
|
|
752
|
+
found.update(origins)
|
|
753
|
+
return unsafe, frozenset(found)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def certain_type(
|
|
757
|
+
scope: Scope,
|
|
758
|
+
value: ast.expr,
|
|
759
|
+
worked_out: tuple[str | None, bool] | None = None,
|
|
760
|
+
) -> str | None:
|
|
761
|
+
"""Infer `value`'s type for value flow: only a certain `--fix` inference, never a guess.
|
|
762
|
+
|
|
763
|
+
`None` itself (which `--fix` never offers: `x: None = None` says nothing) is `"None"` here. A
|
|
764
|
+
copy of a name typed as a union is unknown: an `isinstance` or `is None` check before it may
|
|
765
|
+
have narrowed the name, which value flow (blind to control flow) can't see. A type vaguer than
|
|
766
|
+
`vague` allows is unknown too: its `Any` fits whatever the name is declared. `worked_out`:
|
|
767
|
+
`--fix`'s annotation for `value` and whether it's a guess, if they're known already.
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
The type as text, or `None` if it's unknown or only a guess.
|
|
771
|
+
|
|
772
|
+
"""
|
|
773
|
+
if isinstance(value, ast.Constant) and value.value is None:
|
|
774
|
+
return _NONE
|
|
775
|
+
annotation: str | None
|
|
776
|
+
unsafe: bool
|
|
777
|
+
annotation, unsafe = worked_out or (
|
|
778
|
+
inferred(value, scope.settings.known, scope.inferred.types),
|
|
779
|
+
guessed(value, scope.settings.known, frozenset(scope.inferred.guesses), scope.inferred.types),
|
|
780
|
+
)
|
|
781
|
+
if isinstance(value, _READS) and annotation is not None and len(members(annotation) or ()) > 1:
|
|
782
|
+
return None # a read of a union is narrowed where the code checks it, which value flow can't see
|
|
783
|
+
if unsafe or annotation is None or not vague_fits(parsed(annotation), scope.settings.checks.vague):
|
|
784
|
+
return None
|
|
785
|
+
return annotation
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def guessed_type(scope: Scope, value: ast.expr) -> Late | None:
|
|
789
|
+
"""Infer `value`'s type where `--fix` only guesses it: what `certain_type` leaves unknown.
|
|
790
|
+
|
|
791
|
+
Returns:
|
|
792
|
+
The type and what the guess rests on; `None` if it's unknown, certain, or vaguer than `vague`
|
|
793
|
+
allows.
|
|
794
|
+
|
|
795
|
+
"""
|
|
796
|
+
annotation: str | None = inferred(value, scope.settings.known, scope.inferred.types)
|
|
797
|
+
unsafe: bool
|
|
798
|
+
origins: frozenset[str]
|
|
799
|
+
unsafe, origins = guesses_in(scope, [value])
|
|
800
|
+
if not unsafe or annotation is None or not vague_fits(parsed(annotation), scope.settings.checks.vague):
|
|
801
|
+
return None
|
|
802
|
+
return annotation, origins
|