diff --git a/mypy/checker.py b/mypy/checker.py index 3b0fd4e6bed8..d925fd665eb1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2159,7 +2159,8 @@ def find_isinstance_check(node: Node, weak: bool=False ) -> Tuple[Optional[Dict[Node, Type]], Optional[Dict[Node, Type]]]: """Find any isinstance checks (within a chain of ands). Includes - implicit and explicit checks for None. + implicit and explicit checks for None. `node` must already have + been type checked so that its subexpressions have types in type_map. Return value is a map of variables to their types if the condition is true and a map of variables to their types if the condition is false. diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 7782843b98f9..f771e3b5b86a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1077,25 +1077,47 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type: # inference of the right operand so that expressions such as # '[1] or []' are inferred correctly. ctx = self.chk.type_context[-1] - left_type = self.accept(e.left, ctx) + # The left operand will always be evaluated, so make sure it + # type checks unconditionally. (We have to type check it before + # calling find_isinstance_check anyways.) + ctx = left_type = self.accept(e.left, ctx) + + # If this is an 'and' or 'or' operation, then left_map and right_map + # are the typing conditions that must hold for the expression to + # evaluate to its left or right operand, respectively. if e.op == 'and': - right_map, _ = \ + right_map, left_map = \ mypy.checker.find_isinstance_check(e.left, self.chk.type_map, self.chk.typing_mode_weak()) elif e.op == 'or': - _, right_map = \ + left_map, right_map = \ mypy.checker.find_isinstance_check(e.left, self.chk.type_map, self.chk.typing_mode_weak()) else: - right_map = None + assert False, "check_boolean_op can only process 'and' and 'or' expressions" with self.chk.binder.frame_context(): - if right_map: - for var, type in right_map.items(): + if left_map == {}: + # optimization: we learned nothing from + # find_isinstance_check, so no need to re-check. + # (The old parser generates left-nested 'and' trees + # for 'e1 and ... and eN'; fixed in fast parser.) + pass + elif left_map is not None: + for var, type in left_map.items(): self.chk.binder.push(var, type) + left_type = self.accept(e.left, ctx) + else: + left_type = UninhabitedType() - right_type = self.accept(e.right, left_type) + with self.chk.binder.frame_context(): + if right_map is not None: + for var, type in right_map.items(): + self.chk.binder.push(var, type) + right_type = self.accept(e.right, ctx) + else: + right_type = UninhabitedType() self.check_not_void(left_type, context) self.check_not_void(right_type, context) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index f8ffd61efaad..c44f86fdb113 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1099,3 +1099,33 @@ def f(x: Union[List[int], str]) -> None: [out] main: note: In function "f": main:4: error: "int" not callable + +[case testIsinstanceInWrongOrderInBooleanOp] +class A: + m = 1 +def f(x: object) -> None: + if x.m and isinstance(x, A) or False: # E: "object" has no attribute "m" + pass +[builtins fixtures/isinstance.py] +[out] +main: note: In function "f": + +[case testIsinstanceAndOr] +class A: + a = None # type: A + +def f(x: object) -> None: + b = isinstance(x, A) and x.a or A() + reveal_type(b) # E: Revealed type is '__main__.A' +[builtins fixtures/isinstance.py] +[out] +main: note: In function "f": + +[case testLeftNestedAnds] +one = 1 +assert ((((((((((((((((((((((((((((((one + and one) and one) and one) and one) and one) and one) and one) + and one) and one) and one) and one) and one) and one) and one) + and one) and one) and one) and one) and one) and one) and one) + and one) and one) and one) and one) and one) and one) and one) + and one) and one) diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 7dad0933cf24..0779756f6d97 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -77,6 +77,30 @@ else: reveal_type(x) # E: Revealed type is 'builtins.int' [builtins fixtures/bool.py] +[case testOrCases] +from typing import Optional +x = None # type: Optional[str] +y1 = x or 'a' +reveal_type(y1) # E: Revealed type is 'builtins.str' +y2 = x or 1 +reveal_type(y2) # E: Revealed type is 'Union[builtins.str, builtins.int]' +z1 = 'a' or x +reveal_type(z1) # E: Revealed type is 'Union[builtins.str, builtins.None]' +z2 = 1 or x +reveal_type(z2) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.None]' + +[case testAndCases] +from typing import Optional +x = None # type: Optional[str] +y1 = x and 'b' +reveal_type(y1) # E: Revealed type is 'Union[builtins.None, builtins.str]' +y2 = x and 1 # x could be '', so... +reveal_type(y2) # E: Revealed type is 'Union[builtins.str, builtins.None, builtins.int]' +z1 = 'b' and x +reveal_type(z1) # E: Revealed type is 'Union[builtins.str, builtins.None]' +z2 = 1 and x +reveal_type(z2) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.None]' + [case testLambdaReturningNone] f = lambda: None x = f()