diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index d88525f33bb2..578019cebec1 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -214,3 +214,45 @@ mypy generates an error if it thinks that an expression is redundant. # Error: If condition in comprehension is always true [redundant-expr] [i for i in range(x) if isinstance(i, int)] + + +Check that expression is not implicitly true in boolean context [truthy-bool] +----------------------------------------------------------------------------- + +Warn when an expression whose type does not implement ``__bool__`` or ``__len__`` is used in boolean context, +since unless implemented by a sub-type, the expression will always evaluate to true. + +.. code-block:: python + + # mypy: enable-error-code truthy-bool + + class Foo: + pass + foo = Foo() + # Error: "foo" has type "Foo" which does not implement __bool__ or __len__ so it could always be true in boolean context + if foo: + ... + + +This check might falsely imply an error. For example, ``typing.Iterable`` does not implement +``__len__`` and so this code will be flagged: + +.. code-block:: python + + # mypy: enable-error-code truthy-bool + + def transform(items: Iterable[int]) -> List[int]: + # Error: "items" has type "typing.Iterable[int]" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] + if not items: + return [42] + return [x + 1 for x in items] + + + +If called as ``transform((int(s) for s in []))``, this function would not return ``[42]]`` unlike what the author +might have intended. Of course it's possible that ``transform`` is only passed ``list`` objects, and so there is +no error in practice. In such case, it might be prudent to annotate ``items: typing.Sequence[int]``. + +This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``typing.Sized``), +except that attempting to invoke an undefined method (e.g. ``__len__``) results in an error, +while attempting to evaluate an object in boolean context without a concrete implementation results in a truthy value. diff --git a/mypy/checker.py b/mypy/checker.py index 81ca24900aa4..9b2a192f09c5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3987,6 +3987,63 @@ def conditional_callable_type_map(self, expr: Expression, return None, {} + def _is_truthy_type(self, t: ProperType) -> bool: + return ( + ( + isinstance(t, Instance) and + bool(t.type) and + not t.type.has_readable_member('__bool__') and + not t.type.has_readable_member('__len__') + ) + or isinstance(t, FunctionLike) + or ( + isinstance(t, UnionType) and + all(self._is_truthy_type(t) for t in get_proper_types(t.items)) + ) + ) + + def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: + if not state.strict_optional: + return # if everything can be None, all bets are off + + t = get_proper_type(t) + if not self._is_truthy_type(t): + return + + def format_expr_type() -> str: + if isinstance(expr, MemberExpr): + return f'Member "{expr.name}" has type "{t}"' + elif isinstance(expr, RefExpr) and expr.fullname: + return f'"{expr.fullname}" has type "{t}"' + elif isinstance(expr, CallExpr): + if isinstance(expr.callee, MemberExpr): + return f'"{expr.callee.name}" returns "{t}"' + elif isinstance(expr.callee, RefExpr) and expr.callee.fullname: + return f'"{expr.callee.fullname}" returns "{t}"' + return f'Call returns "{t}"' + else: + return f'Expression has type "{t}"' + + if isinstance(t, FunctionLike): + self.msg.fail( + f'Function "{t}" could always be true in boolean context', expr, + code=codes.TRUTHY_BOOL, + ) + elif isinstance(t, UnionType): + self.msg.fail( + f"{format_expr_type()} of which no members implement __bool__ or __len__ " + "so it could always be true in boolean context", + expr, + code=codes.TRUTHY_BOOL, + ) + else: + self.msg.fail( + f'{format_expr_type()} which does not implement __bool__ or __len__ ' + 'so it could always be true in boolean context', + expr, + code=codes.TRUTHY_BOOL, + ) + def find_type_equals_check(self, node: ComparisonExpr, expr_indices: List[int] ) -> Tuple[TypeMap, TypeMap]: """Narrow types based on any checks of the type ``type(x) == T`` @@ -4089,6 +4146,7 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM elif is_false_literal(node): return None, {} elif isinstance(node, CallExpr): + self._check_for_truthy_type(type_map[node], node) if len(node.args) == 0: return {}, {} expr = collapse_walrus(node.args[0]) @@ -4271,6 +4329,7 @@ def has_no_custom_eq_checks(t: Type) -> bool: # Restrict the type of the variable to True-ish/False-ish in the if and else branches # respectively vartype = type_map[node] + self._check_for_truthy_type(vartype, node) if_type: Type = true_only(vartype) else_type: Type = false_only(vartype) ref: Expression = node diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 22c592959530..d9e11a044a18 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -122,6 +122,12 @@ def __str__(self) -> str: REDUNDANT_EXPR: Final = ErrorCode( "redundant-expr", "Warn about redundant expressions", "General", default_enabled=False ) +TRUTHY_BOOL: Final = ErrorCode( + 'truthy-bool', + "Warn about expressions that could always evaluate to true in boolean contexts", + 'General', + default_enabled=False +) NAME_MATCH: Final = ErrorCode( "name-match", "Check that type definition has consistent naming", "General" ) diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index 41e4105c20f0..c7d891736a44 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -5,13 +5,14 @@ from typing import List, Optional, Tuple +from mypy.semanal_shared import set_callable_name from mypy.types import ( Type, TypeVarType, AnyType, NoneType, Instance, CallableType, TypeVarDef, TypeType, UninhabitedType, TypeOfAny, TypeAliasType, UnionType, LiteralType ) from mypy.nodes import ( - TypeInfo, ClassDef, Block, ARG_POS, ARG_OPT, ARG_STAR, SymbolTable, - COVARIANT, TypeAlias + TypeInfo, ClassDef, FuncDef, Block, ARG_POS, ARG_OPT, ARG_STAR, SymbolTable, + COVARIANT, TypeAlias, SymbolTableNode, MDEF, ) @@ -62,6 +63,7 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, typevars=['T'], variances=[COVARIANT]) # class tuple self.type_typei = self.make_type_info('builtins.type') # class type + self.bool_type_info = self.make_type_info('builtins.bool') self.functioni = self.make_type_info('builtins.function') # function TODO self.ai = self.make_type_info('A', mro=[self.oi]) # class A self.bi = self.make_type_info('B', mro=[self.ai, self.oi]) # class B(A) @@ -165,6 +167,15 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, self.type_t = TypeType.make_normalized(self.t) self.type_any = TypeType.make_normalized(self.anyt) + self._add_bool_dunder(self.bool_type_info) + self._add_bool_dunder(self.ai) + + def _add_bool_dunder(self, type_info: TypeInfo) -> None: + signature = CallableType([], [], [], Instance(self.bool_type_info, []), self.function) + bool_func = FuncDef('__bool__', [], Block([])) + bool_func.type = set_callable_name(signature, bool_func) + type_info.names[bool_func.name] = SymbolTableNode(MDEF, bool_func) + # Helper methods def callable(self, *a: Type) -> CallableType: diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 7479b15b7bc4..c2502a3533e8 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -807,3 +807,54 @@ from typing_extensions import TypedDict Foo = TypedDict("Bar", {}) # E: First argument "Bar" to TypedDict() does not match variable name "Foo" [name-match] [builtins fixtures/dict.pyi] +[case testTruthyBool] +# flags: --enable-error-code truthy-bool +from typing import List, Union + +class Foo: + pass + +foo = Foo() +if foo: # E: "__main__.foo" has type "__main__.Foo" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] + pass + +zero = 0 +if zero: + pass + +false = False +if false: + pass + +null = None +if null: + pass + +s = '' +if s: + pass + +good_union: Union[str, int] = 5 +if good_union: + pass +if not good_union: + pass + +bad_union: Union[Foo, object] = Foo() +if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins.object]" of which no members implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] + pass +if not bad_union: # E: "__main__.bad_union" has type "builtins.object" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] + pass + +def f(): + pass +if f: # E: Function "def () -> Any" could always be true in boolean context [truthy-bool] + pass +if not f: # E: Function "def () -> Any" could always be true in boolean context [truthy-bool] + pass +conditional_result = 'foo' if f else 'bar' # E: Function "def () -> Any" could always be true in boolean context [truthy-bool] + +lst: List[int] = [] +if lst: + pass +[builtins fixtures/list.pyi] diff --git a/test-data/unit/fixtures/list.pyi b/test-data/unit/fixtures/list.pyi index c4baf89ffc13..31dc333b3d4f 100644 --- a/test-data/unit/fixtures/list.pyi +++ b/test-data/unit/fixtures/list.pyi @@ -16,6 +16,7 @@ class list(Sequence[T]): @overload def __init__(self, x: Iterable[T]) -> None: pass def __iter__(self) -> Iterator[T]: pass + def __len__(self) -> int: pass def __contains__(self, item: object) -> bool: pass def __add__(self, x: list[T]) -> list[T]: pass def __mul__(self, x: int) -> list[T]: pass @@ -26,9 +27,12 @@ class list(Sequence[T]): class tuple(Generic[T]): pass class function: pass -class int: pass -class float: pass -class str: pass +class int: + def __bool__(self) -> bool: pass +class float: + def __bool__(self) -> bool: pass +class str: + def __len__(self) -> bool: pass class bool(int): pass property = object() # Dummy definition.