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
|
Binary file
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a scope (`constricter.rules.scope.Scope`) is checked with, and what it holds as it's checked.
|
|
3
|
+
|
|
4
|
+
Its module's `Settings` (and the parameters' types given from outside, `Seeded`); its `Kind` of
|
|
5
|
+
body; its plain bindings (`Assignments`), which LVA012 and `--fix` read; and what `--fix` has typed
|
|
6
|
+
in it so far (`Inferred`), late fixes included (`Late`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import NamedTuple, TypeAlias
|
|
13
|
+
|
|
14
|
+
from constricter.empty import Empty
|
|
15
|
+
from constricter.fix.core.known import Hints, Inference, Known, Passed, Typed
|
|
16
|
+
from constricter.fix.values import aliased
|
|
17
|
+
from constricter.fix.values.doubts import (
|
|
18
|
+
Facts,
|
|
19
|
+
)
|
|
20
|
+
from constricter.offences import (
|
|
21
|
+
Checks,
|
|
22
|
+
)
|
|
23
|
+
from constricter.rules.flow import Hierarchy, members
|
|
24
|
+
from constricter.rules.quoted import parsed as unquoted
|
|
25
|
+
from constricter.rules.quoted import unqualified
|
|
26
|
+
from constricter.rules.rebinding import REBOUND
|
|
27
|
+
from constricter.rules.syntax import FunctionDef, Start
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Settings:
|
|
32
|
+
"""One module's options, and its source lines (to place a `**rest` capture)."""
|
|
33
|
+
|
|
34
|
+
# The checks asked for (a module written for Python 2 counts its `# type:` comments whatever they say).
|
|
35
|
+
checks: Checks
|
|
36
|
+
lines: Sequence[str]
|
|
37
|
+
known: Known # what the module declares that `--fix` infers types from
|
|
38
|
+
hierarchy: Hierarchy # which types are narrower than which, for value flow
|
|
39
|
+
owners: dict[int, str] # each method's class, by `id()`, to type its `self`, for `--fix`
|
|
40
|
+
# Where each of the module's `:=` starts, in source order: most modules have none, and needn't be
|
|
41
|
+
# looked through for one; a statement's part without one needn't be either.
|
|
42
|
+
walruses: tuple[Start, ...]
|
|
43
|
+
# Type checkers' types, for what `--fix` can't type (`--infer-with`): each checker's, in order.
|
|
44
|
+
hints: tuple[Hints, ...] = ()
|
|
45
|
+
facts: Facts = field(default_factory=Facts) # what a type checker sees otherwise (see `doubts`)
|
|
46
|
+
parameters: "Seeded | None" = None # what its functions' parameters are given from outside them
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Seeded(NamedTuple):
|
|
50
|
+
"""The types a module's functions' unannotated parameters are given from outside them: guesses.
|
|
51
|
+
|
|
52
|
+
`callers`: what every call passes each parameter of its top-level functions, by `id()` (see
|
|
53
|
+
`constricter.fix.index.callers`); `fixtures`: the pytest fixtures its tests can take, each one's
|
|
54
|
+
value's type (see `constricter.fix.index.fixtures`). And `module`: the names the module binds
|
|
55
|
+
once, at its top level, each with its type there, which every function reads it as. `marked`:
|
|
56
|
+
what a class's `parametrize` gives each of its methods, by `id()` (see `calls.marked`).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
callers: Mapping[int, Mapping[str, Passed]] = Empty()
|
|
60
|
+
fixtures: Mapping[str, Passed] = Empty()
|
|
61
|
+
module: Mapping[str, Passed] = Empty()
|
|
62
|
+
marked: Mapping[int, Mapping[str, Passed]] = Empty()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Kind(NamedTuple):
|
|
66
|
+
"""What kind of body a scope is: the code its unannotated names get, and whether `--fix` fixes it."""
|
|
67
|
+
|
|
68
|
+
unannotated: str # LVA001 in a function, LVA004 in a module or class body
|
|
69
|
+
# A class body isn't: annotating a dataclass's variable makes it a field. Only a plain class's
|
|
70
|
+
# variable typed by its value is fixed there (see `constricter.fix.values.classvars`).
|
|
71
|
+
fixable: bool
|
|
72
|
+
function: FunctionDef | None = None # a function's own: its body, its returns
|
|
73
|
+
owner: str | None = None # a class body's: its class
|
|
74
|
+
|
|
75
|
+
def body(self) -> Sequence[ast.stmt]:
|
|
76
|
+
"""Find the function's body.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
It, or nothing for a module or class body's scope.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
return () if self.function is None else self.function.body
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# A name's first plain assignment: its target, its value, and the value's inferred type, if any.
|
|
86
|
+
Plain: TypeAlias = tuple[ast.Name, ast.expr, Inference | None]
|
|
87
|
+
Bare: TypeAlias = tuple[ast.AnnAssign, Inference | None]
|
|
88
|
+
# One plain assignment: where, and whether it's in a loop's body.
|
|
89
|
+
Placed: TypeAlias = tuple[tuple[int, int], bool]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class Assignments:
|
|
94
|
+
"""A scope's plain `name = value` (or `name: T = value`) bindings, for LVA012 and `--fix`.
|
|
95
|
+
|
|
96
|
+
`chained`: where the statement starts that first binds a name it can't annotate (`a = b = 0`,
|
|
97
|
+
a loop's, an unpacking's, a `with`'s): a fix made late (`optional`, `filled`) declares it there.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
found: dict[str, list[Placed]] = field(default_factory=dict[str, list[Placed]]) # each name's
|
|
101
|
+
looping: int = 0 # how many loops deep the statement being visited is
|
|
102
|
+
empty: dict[str, str] = field(default_factory=dict[str, str]) # names first bound empty: their kind
|
|
103
|
+
chained: dict[str, tuple[int, int]] = field(default_factory=dict[str, tuple[int, int]])
|
|
104
|
+
widened: set[str] = field(default_factory=set[str]) # the names a marked line annotates (see `widened`)
|
|
105
|
+
# Each name `late.widens` may offer a wider type: its target, its value (what a loop's, an
|
|
106
|
+
# unpacking's or a `with`'s is taken from: then in `chained` too), and the type `--fix` infers.
|
|
107
|
+
plain: dict[str, "Plain"] = field(default_factory=dict[str, "Plain"])
|
|
108
|
+
# Each marked declaration with no value (`x: Any`, before a loop), and what its name is bound to.
|
|
109
|
+
bare: dict[str, "Bare"] = field(default_factory=dict[str, "Bare"])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# A name typed late: its type, and what that rests on if it's a guess (`FIX_KINDS`).
|
|
113
|
+
Late: TypeAlias = tuple[str, frozenset[str]]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class Inferred:
|
|
118
|
+
"""What `--fix` knows of a scope's names so far."""
|
|
119
|
+
|
|
120
|
+
types: Typed = field(default_factory=Typed) # each known type, for `x = y`'s
|
|
121
|
+
guesses: set[str] = field(default_factory=set[str]) # `types` from an unsafe fix: copies are too
|
|
122
|
+
# Each guess's guessing mechanisms (`FIX_KINDS`), for `unsafe-fix-select` to trust or not.
|
|
123
|
+
origins: dict[str, frozenset[str]] = field(default_factory=dict[str, frozenset[str]])
|
|
124
|
+
returns: list[ast.expr | None] = field(default_factory=list[ast.expr | None]) # its `return`s' values
|
|
125
|
+
# Its `self.x = value` assignments: each attribute, and its value.
|
|
126
|
+
assigned: list[tuple[str, ast.expr]] = field(default_factory=list[tuple[str, ast.expr]])
|
|
127
|
+
# Names typed only once the whole scope was seen (a container filled later, `None` rebound): the
|
|
128
|
+
# type, and what it rests on if a guess; and those it was checked again knowing.
|
|
129
|
+
late: dict[str, Late] = field(default_factory=dict[str, "Late"])
|
|
130
|
+
seeded: dict[str, Late] = field(default_factory=dict[str, "Late"])
|
|
131
|
+
# The names declared type aliases (`X: TypeAlias = ...`): `TypeAlias` isn't their values' type.
|
|
132
|
+
aliases: set[str] = field(default_factory=set[str])
|
|
133
|
+
unions: Mapping[str, str] = field(default_factory=dict[str, str]) # see `Indirect.unions`
|
|
134
|
+
|
|
135
|
+
def members(self, annotation: str) -> frozenset[str]:
|
|
136
|
+
"""Split a type into its union's members, an alias of a union read as the union it names.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Them; none for a type that can't be read.
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
return members(self.unions.get(annotation, annotation)) or frozenset()
|
|
143
|
+
|
|
144
|
+
def declare(self, name: str, annotation: ast.expr, held: str | None = None) -> None:
|
|
145
|
+
"""Record the type `name` is annotated with: its own from here on, or an alias's declaration.
|
|
146
|
+
|
|
147
|
+
A `Final[T]`'s or a `ClassVar[T]`'s is `T`, and a bare one's its value's (`held`, if known).
|
|
148
|
+
"""
|
|
149
|
+
text: str | None = unqualified(unquoted(annotation)) or held
|
|
150
|
+
if aliased.declares(annotation):
|
|
151
|
+
self.aliases.add(name)
|
|
152
|
+
elif text:
|
|
153
|
+
_ = self.types.setdefault(name, text)
|
|
154
|
+
|
|
155
|
+
def learn(
|
|
156
|
+
self,
|
|
157
|
+
name: str,
|
|
158
|
+
annotation: str,
|
|
159
|
+
origins: frozenset[str] | None,
|
|
160
|
+
*,
|
|
161
|
+
again: bool = False,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Record `name`'s type, the first time it's typed; `origins`: what it rests on, if it's a guess.
|
|
164
|
+
|
|
165
|
+
`again`: whether it was bound before, to a value of no known type, which it may still hold
|
|
166
|
+
(another branch's): its type is then a guess, resting on `rebound`.
|
|
167
|
+
"""
|
|
168
|
+
if name in self.types:
|
|
169
|
+
return
|
|
170
|
+
self.types[name] = annotation
|
|
171
|
+
if origins is None and again:
|
|
172
|
+
origins = frozenset({REBOUND})
|
|
173
|
+
if origins is not None:
|
|
174
|
+
self.guess(name, origins)
|
|
175
|
+
|
|
176
|
+
def guess(self, name: str, origins: frozenset[str]) -> None:
|
|
177
|
+
"""Take what's inferred from `name` from here on for a guess, resting on `origins`."""
|
|
178
|
+
self.guesses.add(name)
|
|
179
|
+
self.origins[name] = origins
|
|
180
|
+
|
|
181
|
+
def rebound(self, name: str, typed: str | None, guess: Late | None = None) -> None:
|
|
182
|
+
"""Record `name` bound again, to a value of type `typed` (`None`: unknown).
|
|
183
|
+
|
|
184
|
+
A type checker narrows a name to what it's assigned: from here on it's `typed`, if known. That's
|
|
185
|
+
certain for a member of a declared union (`int | None`, then `1`), which every checker narrows;
|
|
186
|
+
otherwise (mypy narrows nothing else, and an unknown value may be anything) what's inferred
|
|
187
|
+
from it is a guess, resting on `rebound`. `guess`: the value's type where it's only a guess
|
|
188
|
+
(`config = config or Config()`), and what that rests on: the name's from here on, as one. A
|
|
189
|
+
union bound to a value of no known type is narrowed to that: the name has no type until its
|
|
190
|
+
branch ends.
|
|
191
|
+
"""
|
|
192
|
+
current: str | None = self.types.get(name)
|
|
193
|
+
self.types.hold(name, None)
|
|
194
|
+
if current is None or typed == current:
|
|
195
|
+
return
|
|
196
|
+
if typed is None and guess is not None:
|
|
197
|
+
self.types[name] = guess[0]
|
|
198
|
+
self.guess(name, self.origins.get(name, frozenset()) | guess[1])
|
|
199
|
+
return
|
|
200
|
+
union: frozenset[str] = self.members(current)
|
|
201
|
+
if typed is None and len(union) > 1:
|
|
202
|
+
self.types.forget(name) # its type again past a branch (see `rejoined`), as a guess
|
|
203
|
+
if typed is not None:
|
|
204
|
+
self.types[name] = typed
|
|
205
|
+
if typed in union and len(union) > 1:
|
|
206
|
+
return
|
|
207
|
+
if name not in self.guesses:
|
|
208
|
+
self.guess(name, frozenset({REBOUND}))
|
|
209
|
+
|
|
210
|
+
def rejoined(self, before: Mapping[str, str], untyped: frozenset[str]) -> None:
|
|
211
|
+
"""Take each name a branch (`if`, a loop, `try`, `match`) retyped back to its type `before` it.
|
|
212
|
+
|
|
213
|
+
The branch may not have run: past it, a name is what it was, or what the branch made it. That's
|
|
214
|
+
its type before, certainly, if that's a union the branch's type is a member of (`int | None`,
|
|
215
|
+
narrowed to `int` inside `if`); otherwise `rebound` already made it a guess, as it is where
|
|
216
|
+
the branch left it no type. One of `untyped`, bound before the branch or in an arm of it to a
|
|
217
|
+
value of no known type, may still hold it: it has none past it.
|
|
218
|
+
"""
|
|
219
|
+
name: str
|
|
220
|
+
annotation: str
|
|
221
|
+
for name, annotation in before.items():
|
|
222
|
+
if name not in self.types and name not in self.guesses:
|
|
223
|
+
self.guess(name, frozenset({REBOUND}))
|
|
224
|
+
self.types[name] = annotation
|
|
225
|
+
for name in self.types.keys() & untyped:
|
|
226
|
+
self.forget(name)
|
|
227
|
+
|
|
228
|
+
def forget(self, name: str) -> None:
|
|
229
|
+
"""Take `name` for a value of no known type from here on."""
|
|
230
|
+
self.types.forget(name)
|
|
231
|
+
self.guesses.discard(name)
|
|
232
|
+
_ = self.origins.pop(name, None)
|
|
Binary file
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A module's scopes, built for a check: each function's and class body's, their statements visited.
|
|
3
|
+
|
|
4
|
+
What each binds and declares, in order, and which of them a check already finished.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from typing import Final, cast
|
|
10
|
+
|
|
11
|
+
from constricter.fix.core import imports
|
|
12
|
+
from constricter.fix.core.known import (
|
|
13
|
+
Passed,
|
|
14
|
+
)
|
|
15
|
+
from constricter.fix.values import ordered, returned
|
|
16
|
+
from constricter.offences import (
|
|
17
|
+
UNANNOTATED,
|
|
18
|
+
UNANNOTATED_MEMBER,
|
|
19
|
+
)
|
|
20
|
+
from constricter.rules import binding, late, recorded
|
|
21
|
+
from constricter.rules.annotations import node_name
|
|
22
|
+
from constricter.rules.calls import seed_parameters, unshadowed
|
|
23
|
+
from constricter.rules.declarations import (
|
|
24
|
+
imported_from,
|
|
25
|
+
)
|
|
26
|
+
from constricter.rules.narrowing import module_names
|
|
27
|
+
from constricter.rules.quoted import written
|
|
28
|
+
from constricter.rules.scope import Scope
|
|
29
|
+
from constricter.rules.scoped import Kind, Late, Seeded, Settings
|
|
30
|
+
from constricter.rules.syntax import (
|
|
31
|
+
BRANCHING,
|
|
32
|
+
FunctionDef,
|
|
33
|
+
Start,
|
|
34
|
+
arm_ends,
|
|
35
|
+
child_statements,
|
|
36
|
+
collect_functions,
|
|
37
|
+
expressions,
|
|
38
|
+
has_within,
|
|
39
|
+
target_names,
|
|
40
|
+
)
|
|
41
|
+
from constricter.rules.walked import classes
|
|
42
|
+
|
|
43
|
+
# The node class of `type X = ...` statements, by name: Python 3.11's `ast` has no `TypeAlias`.
|
|
44
|
+
_TYPE_ALIAS: Final = "TypeAlias"
|
|
45
|
+
_CLASSMETHOD: Final = "classmethod"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Enum members mustn't be annotated: a base imported from here is one, however it's aliased.
|
|
49
|
+
_ENUM_MODULES: Final = frozenset({"enum"})
|
|
50
|
+
# The conventional name of an instance method's first parameter: typed as its class, for `--fix`.
|
|
51
|
+
_SELF: Final = "self"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def bound_once(tree: ast.Module, settings: Settings) -> dict[str, Passed]:
|
|
55
|
+
"""Type the names the module binds once, anywhere in it, at its top level: what its functions read.
|
|
56
|
+
|
|
57
|
+
`LIMIT = 10` is an `int` in every function, and `NAMES: Final = ["a"]` a `list[str]`: a name bound
|
|
58
|
+
once means the same thing throughout (see `imports.rebound_names`), and a type checker takes its
|
|
59
|
+
one value's type for its own. As the module's body types it, before its functions are checked.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Each such name's type, and what it rests on if it's a guess.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
scope: Scope = _body_scope(tree.body, settings, Kind(UNANNOTATED_MEMBER, fixable=True))
|
|
66
|
+
rebound: Mapping[str, list[Start]] = imports.rebound_names(tree)
|
|
67
|
+
return {
|
|
68
|
+
name: (held, scope.inferred.origins.get(name, frozenset()))
|
|
69
|
+
for name, held in scope.inferred.types.items()
|
|
70
|
+
if name not in rebound
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def module_scopes(tree: ast.Module, settings: Settings, table: returned.Table | None = None) -> list["Scope"]:
|
|
75
|
+
"""Collect the scopes to check.
|
|
76
|
+
|
|
77
|
+
With a `table` (see `returned.Table`), the functions are checked callees first, each one's
|
|
78
|
+
return type recorded as soon as it's checked: a caller checked later knows it the first time.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Every function's scope, and with `all_scopes` every module and class body's.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
functions: list[FunctionDef] = []
|
|
85
|
+
collect_functions(tree.body, functions)
|
|
86
|
+
if table is not None:
|
|
87
|
+
functions = ordered.in_call_order(tree, functions)
|
|
88
|
+
scopes: list[Scope] = _function_scopes(functions, settings, table)
|
|
89
|
+
if settings.checks.all_scopes:
|
|
90
|
+
scopes += body_scopes(tree, settings)
|
|
91
|
+
return scopes
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def body_scopes(tree: ast.Module, settings: Settings) -> list["Scope"]:
|
|
95
|
+
"""Collect the module and class bodies (LVA004).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Their scopes, but for an enum's.
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
imported: frozenset[str] = imported_from(tree, _ENUM_MODULES)
|
|
102
|
+
# A class body isn't fixed: annotating a dataclass's variable makes it a field. A plain class's
|
|
103
|
+
# variable typed by its value is (see `Scope.assign`).
|
|
104
|
+
return [
|
|
105
|
+
_body_scope(tree.body, settings, Kind(UNANNOTATED_MEMBER, fixable=True)),
|
|
106
|
+
*(
|
|
107
|
+
_body_scope(node.body, settings, Kind(UNANNOTATED_MEMBER, fixable=False, owner=node.name))
|
|
108
|
+
for node in classes(tree)
|
|
109
|
+
if not _is_enum(node, imported)
|
|
110
|
+
),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _body_scope(body: list[ast.stmt], settings: Settings, kind: Kind) -> "Scope":
|
|
115
|
+
"""Check one module's or class's body.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Its scope.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
scope: Scope = Scope({"_"}, [], settings, kind)
|
|
122
|
+
stmt: ast.stmt
|
|
123
|
+
for stmt in body:
|
|
124
|
+
_visit(scope, stmt)
|
|
125
|
+
return scope
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _is_enum(node: ast.ClassDef, imported: frozenset[str]) -> bool:
|
|
129
|
+
"""Check whether a base is an enum: enum members mustn't be annotated.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Whether a base is imported from `enum` (`imported`, however it's aliased), or else its name
|
|
133
|
+
ends in `Enum` or `Flag` (for one imported some other way).
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
return any(
|
|
137
|
+
node_name(base) in imported or node_name(base).endswith(("Enum", "Flag")) for base in node.bases
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _function_scopes(
|
|
142
|
+
functions: list[FunctionDef],
|
|
143
|
+
settings: Settings,
|
|
144
|
+
table: returned.Table | None = None,
|
|
145
|
+
) -> list["Scope"]:
|
|
146
|
+
"""Check `functions` and every function defined inside them (recording each in `table`).
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Their scopes.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
scopes: list[Scope] = []
|
|
153
|
+
func: FunctionDef
|
|
154
|
+
for func in functions:
|
|
155
|
+
nested: list[FunctionDef] = []
|
|
156
|
+
scope: Scope = function_scope(func, nested, settings)
|
|
157
|
+
scopes.append(scope)
|
|
158
|
+
if table is not None:
|
|
159
|
+
finish(table.module, [scope])
|
|
160
|
+
# Its returns are typed as it's checked; but one with late types (`None` rebound, ...) is
|
|
161
|
+
# checked again knowing them (`_returned`), and until then nothing's recorded: a caller
|
|
162
|
+
# would take its type too soon (`int`, for `None` rebound to `int`: `int | None`).
|
|
163
|
+
settled: bool = not scope.inferred.late.keys() - scope.inferred.seeded.keys()
|
|
164
|
+
table.checked(
|
|
165
|
+
func,
|
|
166
|
+
recorded.returns(scope, func, table.module) if settled else [],
|
|
167
|
+
(recorded.assigned(scope), recorded.used(scope, func, table.module)) if settled else ([], []),
|
|
168
|
+
)
|
|
169
|
+
scopes += _function_scopes(nested, scope.settings, table)
|
|
170
|
+
return scopes
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def function_scope(
|
|
174
|
+
func: FunctionDef,
|
|
175
|
+
functions: list[FunctionDef],
|
|
176
|
+
settings: Settings,
|
|
177
|
+
seed: Mapping[str, Late] | None = None,
|
|
178
|
+
) -> "Scope":
|
|
179
|
+
"""Check one function; functions defined in it are collected into `functions`.
|
|
180
|
+
|
|
181
|
+
`seed`: names already known to be typed late (see `Inferred.late`), known from the start.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Its scope.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
settings = unshadowed(settings, func)
|
|
188
|
+
args: ast.arguments = func.args
|
|
189
|
+
named: tuple[ast.arg, ...] = (*args.posonlyargs, *args.args, *args.kwonlyargs)
|
|
190
|
+
params: set[str] = {a.arg for a in named}
|
|
191
|
+
params.update(extra.arg for extra in (args.vararg, args.kwarg) if extra is not None)
|
|
192
|
+
# `_` is a discard.
|
|
193
|
+
scope: Scope = Scope(
|
|
194
|
+
params | {"_"},
|
|
195
|
+
functions,
|
|
196
|
+
settings,
|
|
197
|
+
Kind(UNANNOTATED, fixable=True, function=func),
|
|
198
|
+
)
|
|
199
|
+
# A copy of a plain, annotated parameter (`*args`/`**kwargs` aren't the type they're annotated
|
|
200
|
+
# with) can be typed the same way, the moment it's assigned.
|
|
201
|
+
scope.inferred.types.taking(
|
|
202
|
+
{arg.arg: written(arg.annotation) for arg in named if arg.annotation is not None},
|
|
203
|
+
)
|
|
204
|
+
# A method's `self` is its class's instance; so is the `self` a function defined in it reads.
|
|
205
|
+
owner: str | None = settings.owners.get(id(func))
|
|
206
|
+
if owner is not None and (_SELF not in params or [arg.arg for arg in named[:1]] == [_SELF]):
|
|
207
|
+
_ = scope.inferred.types.setdefault(_SELF, owner)
|
|
208
|
+
# A classmethod's first parameter is its class (`type[C]`), whatever it's called.
|
|
209
|
+
if owner is not None and named and [node_name(d) for d in func.decorator_list] == [_CLASSMETHOD]:
|
|
210
|
+
_ = scope.inferred.types.setdefault(named[0].arg, f"type[{owner}]")
|
|
211
|
+
seed_parameters(scope, func, named)
|
|
212
|
+
# What the module's top level binds once, the function reads as it's typed there.
|
|
213
|
+
name: str
|
|
214
|
+
passed: Passed
|
|
215
|
+
for name, passed in (settings.parameters or Seeded()).module.items():
|
|
216
|
+
scope.inferred.learn(name, passed[0], passed[1] or None)
|
|
217
|
+
arg: ast.arg
|
|
218
|
+
for arg in named:
|
|
219
|
+
# A parameter holds whatever its callers pass: its declared type, as far as value flow knows.
|
|
220
|
+
if arg.annotation is not None:
|
|
221
|
+
scope.lifetime(arg.arg).declare(ast.unparse(arg.annotation), (arg.lineno, arg.col_offset))
|
|
222
|
+
scope.lifetime(arg.arg).bind(
|
|
223
|
+
(arg.lineno, arg.col_offset),
|
|
224
|
+
None if arg.annotation is None else ast.unparse(arg.annotation),
|
|
225
|
+
)
|
|
226
|
+
scope.opaque(extra.arg for extra in (args.vararg, args.kwarg) if extra is not None)
|
|
227
|
+
typed: Late
|
|
228
|
+
for name, typed in (seed or {}).items():
|
|
229
|
+
scope.inferred.seeded[name] = typed
|
|
230
|
+
scope.inferred.learn(name, typed[0], typed[1] or None)
|
|
231
|
+
stmt: ast.stmt
|
|
232
|
+
for stmt in func.body:
|
|
233
|
+
_visit(scope, stmt)
|
|
234
|
+
return scope
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _visit(scope: Scope, stmt: ast.stmt, head: ast.stmt | None = None) -> None:
|
|
238
|
+
"""Bind the names `stmt` binds, as Python would, then visit its nested statements.
|
|
239
|
+
|
|
240
|
+
`head`: for an `elif`, the `if` it belongs to, which a declaration of its `:=`'s goes before.
|
|
241
|
+
"""
|
|
242
|
+
part: ast.AST
|
|
243
|
+
walruses: tuple[Start, ...]
|
|
244
|
+
if walruses := scope.settings.walruses: # most modules have no `:=`: none of their parts need a look
|
|
245
|
+
for part in expressions(stmt):
|
|
246
|
+
if has_within(walruses, part): # a `:=` in it
|
|
247
|
+
binding.walruses(scope, part, head or stmt)
|
|
248
|
+
_declare(scope, stmt)
|
|
249
|
+
binding.bind(scope, stmt)
|
|
250
|
+
if isinstance(stmt, ast.Return):
|
|
251
|
+
scope.inferred.returns.append(stmt.value)
|
|
252
|
+
elif isinstance(stmt, ast.Assign):
|
|
253
|
+
scope.inferred.assigned.extend(
|
|
254
|
+
(target.attr, stmt.value)
|
|
255
|
+
for target in stmt.targets
|
|
256
|
+
if isinstance(target, ast.Attribute)
|
|
257
|
+
and isinstance(target.value, ast.Name)
|
|
258
|
+
and target.value.id == _SELF
|
|
259
|
+
)
|
|
260
|
+
# A loop's body runs again and again (not its `else`), for LVA012.
|
|
261
|
+
body: set[int] = (
|
|
262
|
+
{id(s) for s in stmt.body} if isinstance(stmt, ast.For | ast.AsyncFor | ast.While) else set()
|
|
263
|
+
)
|
|
264
|
+
before: dict[str, str] | None = dict(scope.inferred.types) if isinstance(stmt, BRANCHING) else None
|
|
265
|
+
# What has no known type before it, or where one of its arms ends: it may still, past it.
|
|
266
|
+
untyped: set[str] = set() if before is None else scope.declared - scope.inferred.types.keys()
|
|
267
|
+
ends: frozenset[int] = frozenset() if before is None else arm_ends(stmt)
|
|
268
|
+
child: ast.stmt
|
|
269
|
+
for child in child_statements(stmt):
|
|
270
|
+
scope.assignments.looping += id(child) in body
|
|
271
|
+
_visit(scope, child, (head or stmt) if _is_elif(stmt, child) else None)
|
|
272
|
+
scope.assignments.looping -= id(child) in body
|
|
273
|
+
if before is not None and id(child) in ends:
|
|
274
|
+
untyped |= scope.declared - scope.inferred.types.keys() - before.keys()
|
|
275
|
+
if before is not None:
|
|
276
|
+
scope.inferred.rejoined(before, frozenset(untyped))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _is_elif(stmt: ast.stmt, child: ast.stmt) -> bool:
|
|
280
|
+
"""Check whether `child` is `stmt`'s `elif`: an `if`'s only `else` statement, an `if` at its own column.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Whether it is.
|
|
284
|
+
|
|
285
|
+
"""
|
|
286
|
+
return (
|
|
287
|
+
isinstance(stmt, ast.If)
|
|
288
|
+
and isinstance(child, ast.If)
|
|
289
|
+
and stmt.orelse == [child]
|
|
290
|
+
and child.col_offset == stmt.col_offset
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _declare(scope: Scope, stmt: ast.stmt) -> None:
|
|
295
|
+
"""Bind the names `stmt` binds that need no annotation, or carry their own."""
|
|
296
|
+
aliases: list[ast.alias]
|
|
297
|
+
names: list[str]
|
|
298
|
+
handlers: list[ast.ExceptHandler]
|
|
299
|
+
target: ast.Name
|
|
300
|
+
match stmt:
|
|
301
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
302
|
+
scope.declared.add(stmt.name)
|
|
303
|
+
scope.opaque([stmt.name])
|
|
304
|
+
scope.nested.append(stmt) # its body is its own scope
|
|
305
|
+
case ast.ClassDef():
|
|
306
|
+
scope.declared.add(stmt.name)
|
|
307
|
+
scope.opaque([stmt.name])
|
|
308
|
+
collect_functions(stmt.body, scope.nested) # methods of a class defined in a function
|
|
309
|
+
case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
|
|
310
|
+
names = [(alias.asname or alias.name).split(".")[0] for alias in aliases]
|
|
311
|
+
scope.declared.update(names)
|
|
312
|
+
scope.opaque(names)
|
|
313
|
+
case ast.Global(names=names) | ast.Nonlocal(names=names):
|
|
314
|
+
scope.declared.update(names)
|
|
315
|
+
case ast.AnnAssign(target=ast.Name() as target):
|
|
316
|
+
binding.annotated(scope, stmt, target)
|
|
317
|
+
case _ if type(stmt).__name__ == _TYPE_ALIAS:
|
|
318
|
+
alias: ast.expr = cast("ast.expr", next(ast.iter_child_nodes(stmt))) # its first field, the name
|
|
319
|
+
scope.declared.update(name.id for name in target_names(alias))
|
|
320
|
+
case ast.Try(handlers=handlers) | ast.TryStar(handlers=handlers):
|
|
321
|
+
names = [handler.name for handler in handlers if handler.name]
|
|
322
|
+
scope.declared.update(names)
|
|
323
|
+
scope.opaque(names)
|
|
324
|
+
case _:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def finish(tree: ast.Module, scopes: Sequence[Scope]) -> None:
|
|
329
|
+
"""Finish `scopes` for their late fixes (`None` rebound, filled containers, shadowed types).
|
|
330
|
+
|
|
331
|
+
Those need the names written out of sight marked first. Safe to run again: marking is
|
|
332
|
+
idempotent, and no fix redoes one it made.
|
|
333
|
+
"""
|
|
334
|
+
scope: Scope
|
|
335
|
+
for scope in scopes:
|
|
336
|
+
scope.mark_escaped(module_names(tree)[0])
|
|
337
|
+
late.optionals(scope)
|
|
338
|
+
late.rebinds(scope)
|
|
339
|
+
late.extended(scope)
|
|
340
|
+
late.fills(scope)
|
|
341
|
+
late.lambdas(scope)
|
|
342
|
+
late.widens(scope, tree)
|
|
343
|
+
binding.redeclared(scope)
|
|
344
|
+
late.shadowed(scope)
|
|
345
|
+
late.excused(scope)
|
|
346
|
+
late.unchecked(scope, tree)
|
|
Binary file
|