diff --git a/mypy/reachability.py b/mypy/reachability.py index 44a21b993cfc..135f610647d9 100644 --- a/mypy/reachability.py +++ b/mypy/reachability.py @@ -101,7 +101,7 @@ def infer_condition_value(expr: Expression, options: Options) -> int: else: result = consider_sys_version_info(expr, pyversion) if result == TRUTH_VALUE_UNKNOWN: - result = consider_sys_platform(expr, options.platform) + result = consider_platform(expr, options.platform) if result == TRUTH_VALUE_UNKNOWN: if name == 'PY2': result = ALWAYS_TRUE if pyversion[0] == 2 else ALWAYS_FALSE @@ -162,38 +162,59 @@ def consider_sys_version_info(expr: Expression, pyversion: Tuple[int, ...]) -> i return TRUTH_VALUE_UNKNOWN -def consider_sys_platform(expr: Expression, platform: str) -> int: - """Consider whether expr is a comparison involving sys.platform. +def consider_platform(expr: Expression, sys_platform: str) -> int: + """Consider whether expr is a comparison involving sys.platform or platform.system(). Return ALWAYS_TRUE, ALWAYS_FALSE, or TRUTH_VALUE_UNKNOWN. + + Cases supported: + - sys.platform == 'posix' + - sys.platform != 'win32' + - sys.platform.startswith('win') + - platform.system() == 'Linux' + - platform.system() != 'Windows' + - ... """ - # Cases supported: - # - sys.platform == 'posix' - # - sys.platform != 'win32' - # - sys.platform.startswith('win') + MAPPING = { + 'darwin': 'Darwin', + 'linux': 'Linux', + 'win32': 'Windows', + # 'aix': N/A, + # 'cygwin': N/A, + # 'freebsd': N/A, + # ... : N/A + } + if isinstance(expr, ComparisonExpr): # Let's not yet support chained comparisons. if len(expr.operators) > 1: return TRUTH_VALUE_UNKNOWN + op = expr.operators[0] if op not in ('==', '!='): return TRUTH_VALUE_UNKNOWN - if not is_sys_attr(expr.operands[0], 'platform'): - return TRUTH_VALUE_UNKNOWN + right = expr.operands[1] if not isinstance(right, (StrExpr, UnicodeExpr)): return TRUTH_VALUE_UNKNOWN - return fixed_comparison(platform, op, right.value) + + if expr_matches(expr.operands[0], 'sys', 'platform'): + return fixed_comparison(sys_platform, op, right.value) + if isinstance(expr.operands[0], CallExpr) \ + and expr_matches(expr.operands[0].callee, 'platform', 'system') \ + and sys_platform in MAPPING: + return fixed_comparison(MAPPING[sys_platform], op, right.value) + return TRUTH_VALUE_UNKNOWN elif isinstance(expr, CallExpr): if not isinstance(expr.callee, MemberExpr): return TRUTH_VALUE_UNKNOWN if len(expr.args) != 1 or not isinstance(expr.args[0], (StrExpr, UnicodeExpr)): return TRUTH_VALUE_UNKNOWN - if not is_sys_attr(expr.callee.expr, 'platform'): + if not expr_matches(expr.callee.expr, 'sys', 'platform'): return TRUTH_VALUE_UNKNOWN if expr.callee.name != 'startswith': return TRUTH_VALUE_UNKNOWN - if platform.startswith(expr.args[0].value): + if sys_platform.startswith(expr.args[0].value): return ALWAYS_TRUE else: return ALWAYS_FALSE @@ -238,9 +259,9 @@ def contains_int_or_tuple_of_ints(expr: Expression def contains_sys_version_info(expr: Expression ) -> Union[None, int, Tuple[Optional[int], Optional[int]]]: - if is_sys_attr(expr, 'version_info'): + if expr_matches(expr, 'sys', 'version_info'): return (None, None) # Same as sys.version_info[:] - if isinstance(expr, IndexExpr) and is_sys_attr(expr.base, 'version_info'): + if isinstance(expr, IndexExpr) and expr_matches(expr.base, 'sys', 'version_info'): index = expr.index if isinstance(index, IntExpr): return index.value @@ -261,15 +282,15 @@ def contains_sys_version_info(expr: Expression return None -def is_sys_attr(expr: Expression, name: str) -> bool: +def expr_matches(expr: Expression, package: str, attr: str) -> bool: # TODO: This currently doesn't work with code like this: - # - import sys as _sys - # - from sys import version_info - if isinstance(expr, MemberExpr) and expr.name == name: - if isinstance(expr.expr, NameExpr) and expr.expr.name == 'sys': - # TODO: Guard against a local named sys, etc. - # (Though later passes will still do most checking.) - return True + # - import package as _package + # - from package import version_info + if isinstance(expr, MemberExpr) and expr.name == attr \ + and isinstance(expr.expr, NameExpr) and expr.expr.name == package: + # TODO: Guard against a local variable hiding package, etc. + # (Though later passes will still do most checking) + return True return False diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 832bb5737a37..f90efe0d0d1d 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -338,6 +338,36 @@ foo() + 0 [builtins fixtures/ops.pyi] [out] +[case testPlatformSystem1] +import platform +if platform.system() == 'fictional': + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + '' +[builtins fixtures/ops.pyi] +[out] + +[case testPlatformSystem2] +import platform +if platform.system() != 'fictional': + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.pyi] +[out] + +[case testPlatformSystemNegated] +import platform +if not (platform.system() == 'fictional'): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.pyi] +[out] + [case testSysVersionInfoClass] import sys if sys.version_info < (3, 5): @@ -464,6 +494,76 @@ x = 1 [builtins fixtures/ops.pyi] [out] +[case testPlatformSystemInMethod1] +import platform +class C: + def foo(self) -> None: + if platform.system() != 'fictional': + x = '' + else: + x = 0 + reveal_type(x) # N: Revealed type is "builtins.str" +[builtins fixtures/ops.pyi] +[out] + +[case testPlatformSystemInFunctionImport1] +import platform +def foo() -> None: + if platform.system() != 'fictional': + import a + else: + import b as a + a.x +[file a.py] +x = 1 +[builtins fixtures/ops.pyi] +[out] + +[case testPlatformSystemInFunctionImport2] +import platform +def foo() -> None: + if platform.system() == 'fictional': + import b as a + else: + import a + a.x +[file a.py] +x = 1 +[builtins fixtures/ops.pyi] +[out] + +[case testPlatformSystemInFunctionImport3] +from typing import Callable +import platform + +def idf(x: Callable[[], None]) -> Callable[[], None]: return x + +@idf +def foo() -> None: + if platform.system() == 'fictional': + import b as a + else: + import a + a.x +[file a.py] +x = 1 +[builtins fixtures/ops.pyi] +[out] + +[case testPlatformSystemInMethodImport2] +import platform +class A: + def foo(self) -> None: + if platform.system() == 'fictional': + import b as a + else: + import a + a.x +[file a.py] +x = 1 +[builtins fixtures/ops.pyi] +[out] + [case testCustomSysVersionInfo] # flags: --python-version 3.5 import sys @@ -519,6 +619,28 @@ reveal_type(x) # N: Revealed type is "builtins.str" [builtins fixtures/ops.pyi] [out] +[case testCustomPlatformSystem1] +# flags: --platform linux +import platform +if platform.system() == 'Linux': + x = "foo" +else: + x = 3 +reveal_type(x) # N: Revealed type is "builtins.str" +[builtins fixtures/ops.pyi] +[out] + +[case testCustomPlatformSystem2] +# flags: --platform win32 +import platform +if platform.system() == 'Linux': + x = "foo" +else: + x = 3 +reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/ops.pyi] +[out] + [case testShortCircuitInExpression] import typing def make() -> bool: pass @@ -543,7 +665,7 @@ reveal_type(h) # N: Revealed type is "builtins.bool" [builtins fixtures/ops.pyi] [out] -[case testShortCircuitAndWithConditionalAssignment] +[case testShortCircuitAndWithConditionalAssignment1] # flags: --platform linux import sys @@ -561,7 +683,25 @@ else: reveal_type(y) # N: Revealed type is "builtins.int" [builtins fixtures/ops.pyi] -[case testShortCircuitOrWithConditionalAssignment] +[case testShortCircuitAndWithConditionalAssignment2] +# flags: --platform linux +import platform + +def f(): pass +PY2 = f() +if PY2 and platform.system() == 'Linux': + x = 'foo' +else: + x = 3 +reveal_type(x) # N: Revealed type is "builtins.int" +if platform.system() == 'Linux' and PY2: + y = 'foo' +else: + y = 3 +reveal_type(y) # N: Revealed type is "builtins.int" +[builtins fixtures/ops.pyi] + +[case testShortCircuitOrWithConditionalAssignment1] # flags: --platform linux import sys @@ -579,7 +719,25 @@ else: reveal_type(y) # N: Revealed type is "builtins.str" [builtins fixtures/ops.pyi] -[case testShortCircuitNoEvaluation] +[case testShortCircuitOrWithConditionalAssignment2] +# flags: --platform linux +import platform + +def f(): pass +PY2 = f() +if PY2 or platform.system() == 'Linux': + x = 'foo' +else: + x = 3 +reveal_type(x) # N: Revealed type is "builtins.str" +if platform.system() == 'Linux' or PY2: + y = 'foo' +else: + y = 3 +reveal_type(y) # N: Revealed type is "builtins.str" +[builtins fixtures/ops.pyi] + +[case testShortCircuitNoEvaluation1] # flags: --platform linux --always-false COMPILE_TIME_FALSE import sys @@ -615,6 +773,20 @@ if MYPY or mypy_only: pass [builtins fixtures/ops.pyi] +[case testShortCircuitNoEvaluation2] +# flags: --platform linux --always-false COMPILE_TIME_FALSE +import platform + +if platform.system() == 'Darwin': + mac_only = 'junk' + +# `mac_only` should not be evaluated +if platform.system() == 'Darwin' and mac_only: + pass +if platform.system() == 'Linux' or mac_only: + pass +[builtins fixtures/ops.pyi] + [case testSemanticAnalysisFalseButTypeNarrowingTrue] # flags: --always-false COMPILE_TIME_FALSE from typing import Literal @@ -727,6 +899,13 @@ assert NOPE reveal_type('') # No error here :-) [builtins fixtures/ops.pyi] +[case testUnreachableAfterToplevelAssert5] +import platform +reveal_type(0) # N: Revealed type is "Literal[0]?" +assert platform.system() == 'lol' +reveal_type('') # No error here :-) +[builtins fixtures/ops.pyi] + [case testUnreachableAfterToplevelAssertImport] import foo foo.bar() # E: "object" has no attribute "bar" @@ -746,7 +925,30 @@ assert sys.platform == 'lol' def bar() -> None: pass [builtins fixtures/ops.pyi] -[case testUnreachableAfterToplevelAssertNotInsideIf] +[case testUnreachableAfterToplevelAssertImport3] +import foo +foo.bar() # E: Module has no attribute "bar" +[file foo.py] +import platform +assert platform.system() == 'lol' +def bar() -> None: pass +[builtins fixtures/ops.pyi] + +[case testUnreachableAfterToplevelAssertImport4] +-- The truth value of the assert statement is unknown since +-- the custom sys.platform cannot be mapped to platform.system(). +-- Only sys.platform (darwin, linux, win32) can be mapped to +-- platform.system() (Darwin, Linux, Windows). +# flags: --platform lol +import foo +foo.bar() # No error +[file foo.py] +import platform +assert platform.system() == 'lol' +def bar() -> None: pass +[builtins fixtures/ops.pyi] + +[case testUnreachableAfterToplevelAssertNotInsideIf1] import sys if sys.version_info[0] >= 2: assert sys.platform == 'lol' @@ -754,6 +956,15 @@ if sys.version_info[0] >= 2: reveal_type('') # N: Revealed type is "Literal['']?" [builtins fixtures/ops.pyi] +[case testUnreachableAfterToplevelAssertNotInsideIf2] +import sys +import platform +if sys.version_info[0] >= 2: + assert platform.system() == 'lol' + reveal_type('') # N: Revealed type is "Literal['']?" +reveal_type('') # N: Revealed type is "Literal['']?" +[builtins fixtures/ops.pyi] + [case testUnreachableFlagWithBadControlFlow1] # flags: --warn-unreachable a: int @@ -844,6 +1055,7 @@ def baz(x: int) -> int: [case testUnreachableFlagIgnoresSemanticAnalysisUnreachable] # flags: --warn-unreachable --python-version 3.7 --platform win32 --always-false FOOBAR import sys +import platform from typing import TYPE_CHECKING x: int @@ -867,6 +1079,16 @@ if sys.platform == 'win32': else: reveal_type(x) +if platform.system() == 'Darwin': + reveal_type(x) +else: + reveal_type(x) # N: Revealed type is "builtins.int" + +if platform.system() == 'Windows': + reveal_type(x) # N: Revealed type is "builtins.int" +else: + reveal_type(x) + if sys.version_info == (2, 7): reveal_type(x) else: @@ -1375,7 +1597,7 @@ def test_untyped_fn(obj): assert obj.prop is False reveal_type(obj.prop) # E: Statement is unreachable -[case testConditionalTypeVarException] +[case testConditionalTypeVarException1] # every part of this test case was necessary to trigger the crash import sys from typing import TypeVar @@ -1390,6 +1612,49 @@ def f(t: T) -> None: pass [builtins fixtures/dict.pyi] +[case testConditionalTypeVarException2] +import platform +from typing import TypeVar + +T = TypeVar("T", int, str) + +def f(t: T) -> None: + if platform.system() == "lol": + try: + pass + except BaseException as e: + pass +[builtins fixtures/dict.pyi] + +[case testSysPlatformToPlatformSystem1] +# flags: --platform darwin +import platform +a = 0 +if platform.system() == 'Darwin': + a = 1 +else: + a = "error" +[builtins fixtures/ops.pyi] + +[case testSysPlatformToPlatformSystem2] +# flags: --platform linux +import platform +a = 0 +if platform.system() == 'Linux': + a = 1 +else: + a = "error" +[builtins fixtures/ops.pyi] + +[case testSysPlatformToPlatformSystem3] +# flags: --platform win32 +import platform +a = 0 +if platform.system() == 'Windows': + a = 1 +else: + a = "error" +[builtins fixtures/ops.pyi] [case testUnreachableLiteral] # flags: --warn-unreachable