From cae4615720e56d0076ed54c184b6792e1ee9607d Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 20 Apr 2025 22:17:24 +0200 Subject: [PATCH 1/3] Make infer_condition_value recognize the whole truth table --- mypy/reachability.py | 40 +++++++++++++--------- test-data/unit/check-unreachable-code.test | 40 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/mypy/reachability.py b/mypy/reachability.py index 5d170b5071db..611d022b1209 100644 --- a/mypy/reachability.py +++ b/mypy/reachability.py @@ -115,31 +115,41 @@ def infer_condition_value(expr: Expression, options: Options) -> int: MYPY_TRUE if true under mypy and false at runtime, MYPY_FALSE if false under mypy and true at runtime, else TRUTH_VALUE_UNKNOWN. """ + if isinstance(expr, UnaryExpr) and expr.op == "not": + positive = infer_condition_value(expr.expr, options) + return inverted_truth_mapping[positive] + pyversion = options.python_version name = "" - negated = False - alias = expr - if isinstance(alias, UnaryExpr): - if alias.op == "not": - expr = alias.expr - negated = True + result = TRUTH_VALUE_UNKNOWN if isinstance(expr, NameExpr): name = expr.name elif isinstance(expr, MemberExpr): name = expr.name elif isinstance(expr, OpExpr) and expr.op in ("and", "or"): + # This is a bit frivolous with MYPY_* vs ALWAYS_* returns: for example, here + # `MYPY_TRUE or ALWAYS_TRUE` will be `MYPY_TRUE`, while + # `ALWAYS_TRUE or MYPY_TRUE` will be `ALWAYS_TRUE`. This literally never + # makes any difference in consuming code, so short-circuiting here is probably + # good enough as it allows referencing platform-dependent variables in + # statement parts that will not be executed. left = infer_condition_value(expr.left, options) - if (left in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "and") or ( - left in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "or" + if (left in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "or") or ( + left in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "and" ): - # Either `True and ` or `False or `: the result will - # always be the right-hand-side. - return infer_condition_value(expr.right, options) - else: - # The result will always be the left-hand-side (e.g. ALWAYS_* or - # TRUTH_VALUE_UNKNOWN). + # Either `True or ` or `False and `: `` doesn't matter return left + right = infer_condition_value(expr.right, options) + if (right in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "or") or ( + right in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "and" + ): + # Either ` or True` or ` and False`: `` doesn't matter + return right + # Now we have `True and True`, `False or False` or smth indeterminate. + if TRUTH_VALUE_UNKNOWN in (left, right) or expr.op not in ("or", "and"): + return TRUTH_VALUE_UNKNOWN + return left else: result = consider_sys_version_info(expr, pyversion) if result == TRUTH_VALUE_UNKNOWN: @@ -155,8 +165,6 @@ def infer_condition_value(expr: Expression, options: Options) -> int: result = ALWAYS_TRUE elif name in options.always_false: result = ALWAYS_FALSE - if negated: - result = inverted_truth_mapping[result] return result diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index a40aa21ff26a..bbcec27aaa0b 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -500,6 +500,46 @@ reveal_type(h) # N: Revealed type is "builtins.bool" [builtins fixtures/ops.pyi] [out] +[case testConditionalValuesBinaryOps] +# flags: --platform linux +import sys + +t_and_t = (sys.platform == 'linux' and sys.platform == 'linux') and 's' +t_or_t = (sys.platform == 'linux' or sys.platform == 'linux') and 's' +t_and_f = (sys.platform == 'linux' and sys.platform == 'windows') and 's' +t_or_f = (sys.platform == 'linux' or sys.platform == 'windows') and 's' +f_and_t = (sys.platform == 'windows' and sys.platform == 'linux') and 's' +f_or_t = (sys.platform == 'windows' or sys.platform == 'linux') and 's' +f_and_f = (sys.platform == 'windows' and sys.platform == 'windows') and 's' +f_or_f = (sys.platform == 'windows' or sys.platform == 'windows') and 's' +reveal_type(t_and_t) # N: Revealed type is "Literal['s']" +reveal_type(t_or_t) # N: Revealed type is "Literal['s']" +reveal_type(f_and_t) # N: Revealed type is "builtins.bool" +reveal_type(f_or_t) # N: Revealed type is "Literal['s']" +reveal_type(t_and_f) # N: Revealed type is "builtins.bool" +reveal_type(t_or_f) # N: Revealed type is "Literal['s']" +reveal_type(f_and_f) # N: Revealed type is "builtins.bool" +reveal_type(f_or_f) # N: Revealed type is "builtins.bool" +[builtins fixtures/ops.pyi] + +[case testConditionalValuesNegation] +# flags: --platform linux +import sys + +not_t = not sys.platform == 'linux' and 's' +not_f = not sys.platform == 'windows' and 's' +not_and_t = not (sys.platform == 'linux' and sys.platform == 'linux') and 's' +not_and_f = not (sys.platform == 'linux' and sys.platform == 'windows') and 's' +not_or_t = not (sys.platform == 'linux' or sys.platform == 'linux') and 's' +not_or_f = not (sys.platform == 'windows' or sys.platform == 'windows') and 's' +reveal_type(not_t) # N: Revealed type is "builtins.bool" +reveal_type(not_f) # N: Revealed type is "Literal['s']" +reveal_type(not_and_t) # N: Revealed type is "builtins.bool" +reveal_type(not_and_f) # N: Revealed type is "Literal['s']" +reveal_type(not_or_t) # N: Revealed type is "builtins.bool" +reveal_type(not_or_f) # N: Revealed type is "Literal['s']" +[builtins fixtures/ops.pyi] + [case testShortCircuitAndWithConditionalAssignment] # flags: --platform linux import sys From a50082672f473792ed4135e7bc9108f1684c2535 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 21 Apr 2025 23:16:02 +0200 Subject: [PATCH 2/3] Test that we do not try to handle other operators --- test-data/unit/check-unreachable-code.test | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index bbcec27aaa0b..5b75f5fa1ea2 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -540,6 +540,16 @@ reveal_type(not_or_t) # N: Revealed type is "builtins.bool" reveal_type(not_or_f) # N: Revealed type is "Literal['s']" [builtins fixtures/ops.pyi] +[case testConditionalValuesUnsupportedOps] +# flags: --platform linux +import sys + +unary_minus = -(sys.platform == 'linux') and 's' +binary_minus = ((sys.platform == 'linux') - (sys.platform == 'linux')) and 's' +reveal_type(unary_minus) # N: Revealed type is "Union[Literal[0], builtins.str]" +reveal_type(binary_minus) # N: Revealed type is "Union[Literal[0], builtins.str]" +[builtins fixtures/ops.pyi] + [case testShortCircuitAndWithConditionalAssignment] # flags: --platform linux import sys From 648bd84bbce00248795f6d2c555d866342462ccb Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 22 Apr 2025 00:09:36 +0200 Subject: [PATCH 3/3] Remove short-circuiting logic here, it is safe to check truthiness of any stmt - there are no error reports. Make MYPY_* vs ALWAYS_* consistent. --- mypy/reachability.py | 43 ++++++++++++---------- test-data/unit/check-unreachable-code.test | 26 +++++++++++++ 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/mypy/reachability.py b/mypy/reachability.py index 611d022b1209..dd54f927773a 100644 --- a/mypy/reachability.py +++ b/mypy/reachability.py @@ -128,28 +128,31 @@ def infer_condition_value(expr: Expression, options: Options) -> int: elif isinstance(expr, MemberExpr): name = expr.name elif isinstance(expr, OpExpr) and expr.op in ("and", "or"): - # This is a bit frivolous with MYPY_* vs ALWAYS_* returns: for example, here - # `MYPY_TRUE or ALWAYS_TRUE` will be `MYPY_TRUE`, while - # `ALWAYS_TRUE or MYPY_TRUE` will be `ALWAYS_TRUE`. This literally never - # makes any difference in consuming code, so short-circuiting here is probably - # good enough as it allows referencing platform-dependent variables in - # statement parts that will not be executed. + if expr.op not in ("or", "and"): + return TRUTH_VALUE_UNKNOWN + left = infer_condition_value(expr.left, options) - if (left in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "or") or ( - left in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "and" - ): - # Either `True or ` or `False and `: `` doesn't matter - return left right = infer_condition_value(expr.right, options) - if (right in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "or") or ( - right in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "and" - ): - # Either ` or True` or ` and False`: `` doesn't matter - return right - # Now we have `True and True`, `False or False` or smth indeterminate. - if TRUTH_VALUE_UNKNOWN in (left, right) or expr.op not in ("or", "and"): - return TRUTH_VALUE_UNKNOWN - return left + results = {left, right} + if expr.op == "or": + if ALWAYS_TRUE in results: + return ALWAYS_TRUE + elif MYPY_TRUE in results: + return MYPY_TRUE + elif left == right == MYPY_FALSE: + return MYPY_FALSE + elif results <= {ALWAYS_FALSE, MYPY_FALSE}: + return ALWAYS_FALSE + elif expr.op == "and": + if ALWAYS_FALSE in results: + return ALWAYS_FALSE + elif MYPY_FALSE in results: + return MYPY_FALSE + elif left == right == ALWAYS_TRUE: + return ALWAYS_TRUE + elif results <= {ALWAYS_TRUE, MYPY_TRUE}: + return MYPY_TRUE + return TRUTH_VALUE_UNKNOWN else: result = consider_sys_version_info(expr, pyversion) if result == TRUTH_VALUE_UNKNOWN: diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 5b75f5fa1ea2..5f3ceda3f27d 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -550,6 +550,32 @@ reveal_type(unary_minus) # N: Revealed type is "Union[Literal[0], builtins.str]" reveal_type(binary_minus) # N: Revealed type is "Union[Literal[0], builtins.str]" [builtins fixtures/ops.pyi] +[case testMypyFalseValuesInBinaryOps_no_empty] +# flags: --platform linux +import sys +from typing import TYPE_CHECKING + +MYPY = 0 + +if TYPE_CHECKING and sys.platform == 'linux': + def foo1() -> int: ... +if sys.platform == 'linux' and TYPE_CHECKING: + def foo2() -> int: ... +if MYPY and sys.platform == 'linux': + def foo3() -> int: ... +if sys.platform == 'linux' and MYPY: + def foo4() -> int: ... + +if TYPE_CHECKING or sys.platform == 'linux': + def bar1() -> int: ... # E: Missing return statement +if sys.platform == 'linux' or TYPE_CHECKING: + def bar2() -> int: ... # E: Missing return statement +if MYPY or sys.platform == 'linux': + def bar3() -> int: ... # E: Missing return statement +if sys.platform == 'linux' or MYPY: + def bar4() -> int: ... # E: Missing return statement +[builtins fixtures/ops.pyi] + [case testShortCircuitAndWithConditionalAssignment] # flags: --platform linux import sys