Skip to content

Commit d3427c1

Browse files
authored
[partially defined] fix a false-negative with variable defined in a skipped branch (#14221)
The goal of partially-defined check is to detect variables that could be undefined but semantic analyzer doesn't detect them as undefined. In this case, a variable was defined in a branch that returns, so semantic analyzer considered it defined when it was not. I've discovered this when testing support for try/except statements (#14114).
1 parent 98f1b00 commit d3427c1

File tree

2 files changed

+25
-13
lines changed

2 files changed

+25
-13
lines changed

mypy/partially_defined.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,23 +121,29 @@ def is_defined_in_a_branch(self, name: str) -> bool:
121121
return False
122122

123123
def done(self) -> BranchState:
124-
branches = [b for b in self.branches if not b.skipped]
125-
if len(branches) == 0:
126-
return BranchState(skipped=True)
127-
if len(branches) == 1:
128-
return branches[0]
129-
130-
# must_be_defined is a union of must_be_defined of all branches.
131-
must_be_defined = set(branches[0].must_be_defined)
132-
for b in branches[1:]:
133-
must_be_defined.intersection_update(b.must_be_defined)
134-
# may_be_defined are all variables that are not must be defined.
124+
# First, compute all vars, including skipped branches. We include skipped branches
125+
# because our goal is to capture all variables that semantic analyzer would
126+
# consider defined.
135127
all_vars = set()
136-
for b in branches:
128+
for b in self.branches:
137129
all_vars.update(b.may_be_defined)
138130
all_vars.update(b.must_be_defined)
131+
# For the rest of the things, we only care about branches that weren't skipped.
132+
non_skipped_branches = [b for b in self.branches if not b.skipped]
133+
if len(non_skipped_branches) > 0:
134+
must_be_defined = non_skipped_branches[0].must_be_defined
135+
for b in non_skipped_branches[1:]:
136+
must_be_defined.intersection_update(b.must_be_defined)
137+
else:
138+
must_be_defined = set()
139+
# Everything that wasn't defined in all branches but was defined
140+
# in at least one branch should be in `may_be_defined`!
139141
may_be_defined = all_vars.difference(must_be_defined)
140-
return BranchState(may_be_defined=may_be_defined, must_be_defined=must_be_defined)
142+
return BranchState(
143+
must_be_defined=must_be_defined,
144+
may_be_defined=may_be_defined,
145+
skipped=len(non_skipped_branches) == 0,
146+
)
141147

142148

143149
class Scope:

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ def f5() -> int:
345345
return 3
346346
return 1
347347

348+
def f6() -> int:
349+
if int():
350+
x = 0
351+
return x
352+
return x # E: Name "x" may be undefined
353+
348354
[case testDefinedDifferentBranchUseBeforeDef]
349355
# flags: --enable-error-code partially-defined --enable-error-code use-before-def
350356

0 commit comments

Comments
 (0)