Skip to content

Commit 37044dd

Browse files
authored
Support match statement in partially defined check (#13860)
This adds support for match statements for the partially-defined variables check. This should completely cover all match features. In addition, during testing, I found a bug in the generic branch tracking logic, which is also fixed here. Because match is only supported in python 3.10, I had to put the tests in a separate file.
1 parent 3108669 commit 37044dd

File tree

3 files changed

+108
-2
lines changed

3 files changed

+108
-2
lines changed

mypy/partially_defined.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
IfStmt,
1919
ListExpr,
2020
Lvalue,
21+
MatchStmt,
2122
NameExpr,
2223
RaiseStmt,
2324
ReturnStmt,
2425
TupleExpr,
2526
WhileStmt,
2627
WithStmt,
2728
)
29+
from mypy.patterns import AsPattern, StarredPattern
30+
from mypy.reachability import ALWAYS_TRUE, infer_pattern_value
2831
from mypy.traverser import ExtendedTraverserVisitor
2932
from mypy.types import Type, UninhabitedType
3033

@@ -57,11 +60,19 @@ class BranchStatement:
5760
def __init__(self, initial_state: BranchState) -> None:
5861
self.initial_state = initial_state
5962
self.branches: list[BranchState] = [
60-
BranchState(must_be_defined=self.initial_state.must_be_defined)
63+
BranchState(
64+
must_be_defined=self.initial_state.must_be_defined,
65+
may_be_defined=self.initial_state.may_be_defined,
66+
)
6167
]
6268

6369
def next_branch(self) -> None:
64-
self.branches.append(BranchState(must_be_defined=self.initial_state.must_be_defined))
70+
self.branches.append(
71+
BranchState(
72+
must_be_defined=self.initial_state.must_be_defined,
73+
may_be_defined=self.initial_state.may_be_defined,
74+
)
75+
)
6576

6677
def record_definition(self, name: str) -> None:
6778
assert len(self.branches) > 0
@@ -198,6 +209,21 @@ def visit_if_stmt(self, o: IfStmt) -> None:
198209
o.else_body.accept(self)
199210
self.tracker.end_branch_statement()
200211

212+
def visit_match_stmt(self, o: MatchStmt) -> None:
213+
self.tracker.start_branch_statement()
214+
o.subject.accept(self)
215+
for i in range(len(o.patterns)):
216+
pattern = o.patterns[i]
217+
pattern.accept(self)
218+
guard = o.guards[i]
219+
if guard is not None:
220+
guard.accept(self)
221+
o.bodies[i].accept(self)
222+
is_catchall = infer_pattern_value(pattern) == ALWAYS_TRUE
223+
if not is_catchall:
224+
self.tracker.next_branch()
225+
self.tracker.end_branch_statement()
226+
201227
def visit_func_def(self, o: FuncDef) -> None:
202228
self.tracker.enter_scope()
203229
super().visit_func_def(o)
@@ -270,6 +296,16 @@ def visit_while_stmt(self, o: WhileStmt) -> None:
270296
o.else_body.accept(self)
271297
self.tracker.end_branch_statement()
272298

299+
def visit_as_pattern(self, o: AsPattern) -> None:
300+
if o.name is not None:
301+
self.process_lvalue(o.name)
302+
super().visit_as_pattern(o)
303+
304+
def visit_starred_pattern(self, o: StarredPattern) -> None:
305+
if o.capture is not None:
306+
self.process_lvalue(o.capture)
307+
super().visit_starred_pattern(o)
308+
273309
def visit_name_expr(self, o: NameExpr) -> None:
274310
if self.tracker.is_possibly_undefined(o.name):
275311
self.msg.variable_may_be_undefined(o.name, o)

test-data/unit/check-partially-defined.test

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ else:
1818

1919
z = a + 1 # E: Name "a" may be undefined
2020

21+
[case testUsedInIf]
22+
# flags: --enable-error-code partially-defined
23+
if int():
24+
y = 1
25+
if int():
26+
x = y # E: Name "y" may be undefined
27+
2128
[case testDefinedInAllBranches]
2229
# flags: --enable-error-code partially-defined
2330
if int():

test-data/unit/check-python310.test

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,3 +1725,66 @@ def my_func(pairs: Iterable[tuple[S, S]]) -> None:
17251725
reveal_type(pair) # N: Revealed type is "Tuple[builtins.int, builtins.int]" \
17261726
# N: Revealed type is "Tuple[builtins.str, builtins.str]"
17271727
[builtins fixtures/tuple.pyi]
1728+
1729+
[case testPartiallyDefinedMatch]
1730+
# flags: --enable-error-code partially-defined
1731+
def f0(x: int | str) -> int:
1732+
match x:
1733+
case int():
1734+
y = 1
1735+
return y # E: Name "y" may be undefined
1736+
1737+
def f1(a: object) -> None:
1738+
match a:
1739+
case [y]: pass
1740+
case _:
1741+
y = 1
1742+
x = 2
1743+
z = y
1744+
z = x # E: Name "x" may be undefined
1745+
1746+
def f2(a: object) -> None:
1747+
match a:
1748+
case [[y] as x]: pass
1749+
case {"k1": 1, "k2": x, "k3": y}: pass
1750+
case [0, *x]:
1751+
y = 2
1752+
case _:
1753+
y = 1
1754+
x = [2]
1755+
z = x
1756+
z = y
1757+
1758+
def f3(a: object) -> None:
1759+
y = 1
1760+
match a:
1761+
case [x]:
1762+
y = 2
1763+
# Note the missing `case _:`
1764+
z = x # E: Name "x" may be undefined
1765+
z = y
1766+
1767+
def f4(a: object) -> None:
1768+
y = 1
1769+
match a:
1770+
case [x]:
1771+
y = 2
1772+
case _:
1773+
assert False, "unsupported"
1774+
z = x
1775+
z = y
1776+
1777+
def f5(a: object) -> None:
1778+
match a:
1779+
case tuple(x): pass
1780+
case _:
1781+
return
1782+
y = x
1783+
1784+
def f6(a: object) -> None:
1785+
if int():
1786+
y = 1
1787+
match a:
1788+
case _ if y is not None: # E: Name "y" may be undefined
1789+
pass
1790+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)