Skip to content

Commit cae4615

Browse files
committed
Make infer_condition_value recognize the whole truth table
1 parent 99e2688 commit cae4615

File tree

2 files changed

+64
-16
lines changed

2 files changed

+64
-16
lines changed

mypy/reachability.py

+24-16
Original file line numberDiff line numberDiff line change
@@ -115,31 +115,41 @@ 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
131130
elif isinstance(expr, OpExpr) and expr.op in ("and", "or"):
131+
# This is a bit frivolous with MYPY_* vs ALWAYS_* returns: for example, here
132+
# `MYPY_TRUE or ALWAYS_TRUE` will be `MYPY_TRUE`, while
133+
# `ALWAYS_TRUE or MYPY_TRUE` will be `ALWAYS_TRUE`. This literally never
134+
# makes any difference in consuming code, so short-circuiting here is probably
135+
# good enough as it allows referencing platform-dependent variables in
136+
# statement parts that will not be executed.
132137
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"
138+
if (left in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "or") or (
139+
left in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "and"
135140
):
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).
141+
# Either `True or <other>` or `False and <other>`: `<other>` doesn't matter
142142
return left
143+
right = infer_condition_value(expr.right, options)
144+
if (right in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "or") or (
145+
right in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "and"
146+
):
147+
# Either `<other> or True` or `<other> and False`: `<other>` doesn't matter
148+
return right
149+
# Now we have `True and True`, `False or False` or smth indeterminate.
150+
if TRUTH_VALUE_UNKNOWN in (left, right) or expr.op not in ("or", "and"):
151+
return TRUTH_VALUE_UNKNOWN
152+
return left
143153
else:
144154
result = consider_sys_version_info(expr, pyversion)
145155
if result == TRUTH_VALUE_UNKNOWN:
@@ -155,8 +165,6 @@ def infer_condition_value(expr: Expression, options: Options) -> int:
155165
result = ALWAYS_TRUE
156166
elif name in options.always_false:
157167
result = ALWAYS_FALSE
158-
if negated:
159-
result = inverted_truth_mapping[result]
160168
return result
161169

162170

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

+40
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,46 @@ 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 's'
508+
t_or_t = (sys.platform == 'linux' or sys.platform == 'linux') and 's'
509+
t_and_f = (sys.platform == 'linux' and sys.platform == 'windows') and 's'
510+
t_or_f = (sys.platform == 'linux' or sys.platform == 'windows') and 's'
511+
f_and_t = (sys.platform == 'windows' and sys.platform == 'linux') and 's'
512+
f_or_t = (sys.platform == 'windows' or sys.platform == 'linux') and 's'
513+
f_and_f = (sys.platform == 'windows' and sys.platform == 'windows') and 's'
514+
f_or_f = (sys.platform == 'windows' or sys.platform == 'windows') and 's'
515+
reveal_type(t_and_t) # N: Revealed type is "Literal['s']"
516+
reveal_type(t_or_t) # N: Revealed type is "Literal['s']"
517+
reveal_type(f_and_t) # N: Revealed type is "builtins.bool"
518+
reveal_type(f_or_t) # N: Revealed type is "Literal['s']"
519+
reveal_type(t_and_f) # N: Revealed type is "builtins.bool"
520+
reveal_type(t_or_f) # N: Revealed type is "Literal['s']"
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 's'
530+
not_f = not sys.platform == 'windows' and 's'
531+
not_and_t = not (sys.platform == 'linux' and sys.platform == 'linux') and 's'
532+
not_and_f = not (sys.platform == 'linux' and sys.platform == 'windows') and 's'
533+
not_or_t = not (sys.platform == 'linux' or sys.platform == 'linux') and 's'
534+
not_or_f = not (sys.platform == 'windows' or sys.platform == 'windows') and 's'
535+
reveal_type(not_t) # N: Revealed type is "builtins.bool"
536+
reveal_type(not_f) # N: Revealed type is "Literal['s']"
537+
reveal_type(not_and_t) # N: Revealed type is "builtins.bool"
538+
reveal_type(not_and_f) # N: Revealed type is "Literal['s']"
539+
reveal_type(not_or_t) # N: Revealed type is "builtins.bool"
540+
reveal_type(not_or_f) # N: Revealed type is "Literal['s']"
541+
[builtins fixtures/ops.pyi]
542+
503543
[case testShortCircuitAndWithConditionalAssignment]
504544
# flags: --platform linux
505545
import sys

0 commit comments

Comments
 (0)