Skip to content

Commit 85d0e14

Browse files
authored
Make infer_condition_value recognize the whole truth table (#18944)
Fixes #18901.
1 parent b6f2ea3 commit 85d0e14

File tree

3 files changed

+119
-32
lines changed

3 files changed

+119
-32
lines changed

mypy/reachability.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -115,31 +115,44 @@ def infer_condition_value(expr: Expression, options: Options) -> int:
115115
MYPY_TRUE if true under mypy and false at runtime, MYPY_FALSE if
116116
false under mypy and true at runtime, else TRUTH_VALUE_UNKNOWN.
117117
"""
118+
if isinstance(expr, UnaryExpr) and expr.op == "not":
119+
positive = infer_condition_value(expr.expr, options)
120+
return inverted_truth_mapping[positive]
121+
118122
pyversion = options.python_version
119123
name = ""
120-
negated = False
121-
alias = expr
122-
if isinstance(alias, UnaryExpr):
123-
if alias.op == "not":
124-
expr = alias.expr
125-
negated = True
124+
126125
result = TRUTH_VALUE_UNKNOWN
127126
if isinstance(expr, NameExpr):
128127
name = expr.name
129128
elif isinstance(expr, MemberExpr):
130129
name = expr.name
131-
elif isinstance(expr, OpExpr) and expr.op in ("and", "or"):
130+
elif isinstance(expr, OpExpr):
131+
if expr.op not in ("or", "and"):
132+
return TRUTH_VALUE_UNKNOWN
133+
132134
left = infer_condition_value(expr.left, options)
133-
if (left in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "and") or (
134-
left in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "or"
135-
):
136-
# Either `True and <other>` or `False or <other>`: the result will
137-
# always be the right-hand-side.
138-
return infer_condition_value(expr.right, options)
139-
else:
140-
# The result will always be the left-hand-side (e.g. ALWAYS_* or
141-
# TRUTH_VALUE_UNKNOWN).
142-
return left
135+
right = infer_condition_value(expr.right, options)
136+
results = {left, right}
137+
if expr.op == "or":
138+
if ALWAYS_TRUE in results:
139+
return ALWAYS_TRUE
140+
elif MYPY_TRUE in results:
141+
return MYPY_TRUE
142+
elif left == right == MYPY_FALSE:
143+
return MYPY_FALSE
144+
elif results <= {ALWAYS_FALSE, MYPY_FALSE}:
145+
return ALWAYS_FALSE
146+
elif expr.op == "and":
147+
if ALWAYS_FALSE in results:
148+
return ALWAYS_FALSE
149+
elif MYPY_FALSE in results:
150+
return MYPY_FALSE
151+
elif left == right == ALWAYS_TRUE:
152+
return ALWAYS_TRUE
153+
elif results <= {ALWAYS_TRUE, MYPY_TRUE}:
154+
return MYPY_TRUE
155+
return TRUTH_VALUE_UNKNOWN
143156
else:
144157
result = consider_sys_version_info(expr, pyversion)
145158
if result == TRUTH_VALUE_UNKNOWN:
@@ -155,8 +168,6 @@ def infer_condition_value(expr: Expression, options: Options) -> int:
155168
result = ALWAYS_TRUE
156169
elif name in options.always_false:
157170
result = ALWAYS_FALSE
158-
if negated:
159-
result = inverted_truth_mapping[result]
160171
return result
161172

162173

test-data/unit/check-unreachable-code.test

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -481,25 +481,101 @@ import typing
481481
def make() -> bool: pass
482482
PY2 = PY3 = make()
483483

484-
a = PY2 and 's'
485-
b = PY3 and 's'
486-
c = PY2 or 's'
487-
d = PY3 or 's'
488-
e = (PY2 or PY3) and 's'
489-
f = (PY3 or PY2) and 's'
490-
g = (PY2 or PY3) or 's'
491-
h = (PY3 or PY2) or 's'
484+
a = PY2 and str()
485+
b = PY3 and str()
486+
c = PY2 or str()
487+
d = PY3 or str()
488+
e = (PY2 or PY3) and str()
489+
f = (PY3 or PY2) and str()
490+
g = (PY2 or PY3) or str()
491+
h = (PY3 or PY2) or str()
492492
reveal_type(a) # N: Revealed type is "builtins.bool"
493-
reveal_type(b) # N: Revealed type is "Literal['s']"
494-
reveal_type(c) # N: Revealed type is "Literal['s']"
493+
reveal_type(b) # N: Revealed type is "builtins.str"
494+
reveal_type(c) # N: Revealed type is "builtins.str"
495495
reveal_type(d) # N: Revealed type is "builtins.bool"
496-
reveal_type(e) # N: Revealed type is "Literal['s']"
497-
reveal_type(f) # N: Revealed type is "Literal['s']"
496+
reveal_type(e) # N: Revealed type is "builtins.str"
497+
reveal_type(f) # N: Revealed type is "builtins.str"
498498
reveal_type(g) # N: Revealed type is "builtins.bool"
499499
reveal_type(h) # N: Revealed type is "builtins.bool"
500500
[builtins fixtures/ops.pyi]
501501
[out]
502502

503+
[case testConditionalValuesBinaryOps]
504+
# flags: --platform linux
505+
import sys
506+
507+
t_and_t = (sys.platform == 'linux' and sys.platform == 'linux') and str()
508+
t_or_t = (sys.platform == 'linux' or sys.platform == 'linux') and str()
509+
t_and_f = (sys.platform == 'linux' and sys.platform == 'windows') and str()
510+
t_or_f = (sys.platform == 'linux' or sys.platform == 'windows') and str()
511+
f_and_t = (sys.platform == 'windows' and sys.platform == 'linux') and str()
512+
f_or_t = (sys.platform == 'windows' or sys.platform == 'linux') and str()
513+
f_and_f = (sys.platform == 'windows' and sys.platform == 'windows') and str()
514+
f_or_f = (sys.platform == 'windows' or sys.platform == 'windows') and str()
515+
reveal_type(t_and_t) # N: Revealed type is "builtins.str"
516+
reveal_type(t_or_t) # N: Revealed type is "builtins.str"
517+
reveal_type(f_and_t) # N: Revealed type is "builtins.bool"
518+
reveal_type(f_or_t) # N: Revealed type is "builtins.str"
519+
reveal_type(t_and_f) # N: Revealed type is "builtins.bool"
520+
reveal_type(t_or_f) # N: Revealed type is "builtins.str"
521+
reveal_type(f_and_f) # N: Revealed type is "builtins.bool"
522+
reveal_type(f_or_f) # N: Revealed type is "builtins.bool"
523+
[builtins fixtures/ops.pyi]
524+
525+
[case testConditionalValuesNegation]
526+
# flags: --platform linux
527+
import sys
528+
529+
not_t = not sys.platform == 'linux' and str()
530+
not_f = not sys.platform == 'windows' and str()
531+
not_and_t = not (sys.platform == 'linux' and sys.platform == 'linux') and str()
532+
not_and_f = not (sys.platform == 'linux' and sys.platform == 'windows') and str()
533+
not_or_t = not (sys.platform == 'linux' or sys.platform == 'linux') and str()
534+
not_or_f = not (sys.platform == 'windows' or sys.platform == 'windows') and str()
535+
reveal_type(not_t) # N: Revealed type is "builtins.bool"
536+
reveal_type(not_f) # N: Revealed type is "builtins.str"
537+
reveal_type(not_and_t) # N: Revealed type is "builtins.bool"
538+
reveal_type(not_and_f) # N: Revealed type is "builtins.str"
539+
reveal_type(not_or_t) # N: Revealed type is "builtins.bool"
540+
reveal_type(not_or_f) # N: Revealed type is "builtins.str"
541+
[builtins fixtures/ops.pyi]
542+
543+
[case testConditionalValuesUnsupportedOps]
544+
# flags: --platform linux
545+
import sys
546+
547+
unary_minus = -(sys.platform == 'linux') and str()
548+
binary_minus = ((sys.platform == 'linux') - (sys.platform == 'linux')) and str()
549+
reveal_type(unary_minus) # N: Revealed type is "Union[Literal[0], builtins.str]"
550+
reveal_type(binary_minus) # N: Revealed type is "Union[Literal[0], builtins.str]"
551+
[builtins fixtures/ops.pyi]
552+
553+
[case testMypyFalseValuesInBinaryOps_no_empty]
554+
# flags: --platform linux
555+
import sys
556+
from typing import TYPE_CHECKING
557+
558+
MYPY = 0
559+
560+
if TYPE_CHECKING and sys.platform == 'linux':
561+
def foo1() -> int: ...
562+
if sys.platform == 'linux' and TYPE_CHECKING:
563+
def foo2() -> int: ...
564+
if MYPY and sys.platform == 'linux':
565+
def foo3() -> int: ...
566+
if sys.platform == 'linux' and MYPY:
567+
def foo4() -> int: ...
568+
569+
if TYPE_CHECKING or sys.platform == 'linux':
570+
def bar1() -> int: ... # E: Missing return statement
571+
if sys.platform == 'linux' or TYPE_CHECKING:
572+
def bar2() -> int: ... # E: Missing return statement
573+
if MYPY or sys.platform == 'linux':
574+
def bar3() -> int: ... # E: Missing return statement
575+
if sys.platform == 'linux' or MYPY:
576+
def bar4() -> int: ... # E: Missing return statement
577+
[builtins fixtures/ops.pyi]
578+
503579
[case testShortCircuitAndWithConditionalAssignment]
504580
# flags: --platform linux
505581
import sys

test-data/unit/fixtures/ops.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class tuple(Sequence[Tco]):
2525
class function: pass
2626

2727
class str:
28-
def __init__(self, x: 'int') -> None: pass
28+
def __init__(self, x: 'int' = ...) -> None: pass
2929
def __add__(self, x: 'str') -> 'str': pass
3030
def __eq__(self, x: object) -> bool: pass
3131
def startswith(self, x: 'str') -> bool: pass

0 commit comments

Comments
 (0)