Skip to content

Commit a72fab3

Browse files
ikonsthauntsaninja
andauthored
Check expr that are implicitly true for lack dunder bool/len (#10666)
Optional check which disallows using implicitly true expressions (whose type has no __bool__ nor __len__) in boolean context. The check is only enabled when using strict optionals. Co-authored-by: Shantanu <[email protected]>
1 parent 5e73cd7 commit a72fab3

File tree

6 files changed

+178
-5
lines changed

6 files changed

+178
-5
lines changed

docs/source/error_code_list2.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,45 @@ mypy generates an error if it thinks that an expression is redundant.
214214
215215
# Error: If condition in comprehension is always true [redundant-expr]
216216
[i for i in range(x) if isinstance(i, int)]
217+
218+
219+
Check that expression is not implicitly true in boolean context [truthy-bool]
220+
-----------------------------------------------------------------------------
221+
222+
Warn when an expression whose type does not implement ``__bool__`` or ``__len__`` is used in boolean context,
223+
since unless implemented by a sub-type, the expression will always evaluate to true.
224+
225+
.. code-block:: python
226+
227+
# mypy: enable-error-code truthy-bool
228+
229+
class Foo:
230+
pass
231+
foo = Foo()
232+
# Error: "foo" has type "Foo" which does not implement __bool__ or __len__ so it could always be true in boolean context
233+
if foo:
234+
...
235+
236+
237+
This check might falsely imply an error. For example, ``typing.Iterable`` does not implement
238+
``__len__`` and so this code will be flagged:
239+
240+
.. code-block:: python
241+
242+
# mypy: enable-error-code truthy-bool
243+
244+
def transform(items: Iterable[int]) -> List[int]:
245+
# 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]
246+
if not items:
247+
return [42]
248+
return [x + 1 for x in items]
249+
250+
251+
252+
If called as ``transform((int(s) for s in []))``, this function would not return ``[42]]`` unlike what the author
253+
might have intended. Of course it's possible that ``transform`` is only passed ``list`` objects, and so there is
254+
no error in practice. In such case, it might be prudent to annotate ``items: typing.Sequence[int]``.
255+
256+
This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``typing.Sized``),
257+
except that attempting to invoke an undefined method (e.g. ``__len__``) results in an error,
258+
while attempting to evaluate an object in boolean context without a concrete implementation results in a truthy value.

mypy/checker.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4072,6 +4072,63 @@ def conditional_callable_type_map(self, expr: Expression,
40724072

40734073
return None, {}
40744074

4075+
def _is_truthy_type(self, t: ProperType) -> bool:
4076+
return (
4077+
(
4078+
isinstance(t, Instance) and
4079+
bool(t.type) and
4080+
not t.type.has_readable_member('__bool__') and
4081+
not t.type.has_readable_member('__len__')
4082+
)
4083+
or isinstance(t, FunctionLike)
4084+
or (
4085+
isinstance(t, UnionType) and
4086+
all(self._is_truthy_type(t) for t in get_proper_types(t.items))
4087+
)
4088+
)
4089+
4090+
def _check_for_truthy_type(self, t: Type, expr: Expression) -> None:
4091+
if not state.strict_optional:
4092+
return # if everything can be None, all bets are off
4093+
4094+
t = get_proper_type(t)
4095+
if not self._is_truthy_type(t):
4096+
return
4097+
4098+
def format_expr_type() -> str:
4099+
if isinstance(expr, MemberExpr):
4100+
return f'Member "{expr.name}" has type "{t}"'
4101+
elif isinstance(expr, RefExpr) and expr.fullname:
4102+
return f'"{expr.fullname}" has type "{t}"'
4103+
elif isinstance(expr, CallExpr):
4104+
if isinstance(expr.callee, MemberExpr):
4105+
return f'"{expr.callee.name}" returns "{t}"'
4106+
elif isinstance(expr.callee, RefExpr) and expr.callee.fullname:
4107+
return f'"{expr.callee.fullname}" returns "{t}"'
4108+
return f'Call returns "{t}"'
4109+
else:
4110+
return f'Expression has type "{t}"'
4111+
4112+
if isinstance(t, FunctionLike):
4113+
self.msg.fail(
4114+
f'Function "{t}" could always be true in boolean context', expr,
4115+
code=codes.TRUTHY_BOOL,
4116+
)
4117+
elif isinstance(t, UnionType):
4118+
self.msg.fail(
4119+
f"{format_expr_type()} of which no members implement __bool__ or __len__ "
4120+
"so it could always be true in boolean context",
4121+
expr,
4122+
code=codes.TRUTHY_BOOL,
4123+
)
4124+
else:
4125+
self.msg.fail(
4126+
f'{format_expr_type()} which does not implement __bool__ or __len__ '
4127+
'so it could always be true in boolean context',
4128+
expr,
4129+
code=codes.TRUTHY_BOOL,
4130+
)
4131+
40754132
def find_type_equals_check(self, node: ComparisonExpr, expr_indices: List[int]
40764133
) -> Tuple[TypeMap, TypeMap]:
40774134
"""Narrow types based on any checks of the type ``type(x) == T``
@@ -4174,6 +4231,7 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM
41744231
elif is_false_literal(node):
41754232
return None, {}
41764233
elif isinstance(node, CallExpr):
4234+
self._check_for_truthy_type(type_map[node], node)
41774235
if len(node.args) == 0:
41784236
return {}, {}
41794237
expr = collapse_walrus(node.args[0])
@@ -4361,6 +4419,7 @@ def has_no_custom_eq_checks(t: Type) -> bool:
43614419
# Restrict the type of the variable to True-ish/False-ish in the if and else branches
43624420
# respectively
43634421
vartype = type_map[node]
4422+
self._check_for_truthy_type(vartype, node)
43644423
if_type: Type = true_only(vartype)
43654424
else_type: Type = false_only(vartype)
43664425
ref: Expression = node

mypy/errorcodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ def __str__(self) -> str:
122122
REDUNDANT_EXPR: Final = ErrorCode(
123123
"redundant-expr", "Warn about redundant expressions", "General", default_enabled=False
124124
)
125+
TRUTHY_BOOL: Final = ErrorCode(
126+
'truthy-bool',
127+
"Warn about expressions that could always evaluate to true in boolean contexts",
128+
'General',
129+
default_enabled=False
130+
)
125131
NAME_MATCH: Final = ErrorCode(
126132
"name-match", "Check that type definition has consistent naming", "General"
127133
)

mypy/test/typefixture.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55

66
from typing import List, Optional, Tuple
77

8+
from mypy.semanal_shared import set_callable_name
89
from mypy.types import (
910
Type, TypeVarType, AnyType, NoneType, Instance, CallableType, TypeVarType, TypeType,
1011
UninhabitedType, TypeOfAny, TypeAliasType, UnionType, LiteralType
1112
)
1213
from mypy.nodes import (
13-
TypeInfo, ClassDef, Block, ARG_POS, ARG_OPT, ARG_STAR, SymbolTable,
14-
COVARIANT, TypeAlias
14+
TypeInfo, ClassDef, FuncDef, Block, ARG_POS, ARG_OPT, ARG_STAR, SymbolTable,
15+
COVARIANT, TypeAlias, SymbolTableNode, MDEF,
1516
)
1617

1718

@@ -62,6 +63,7 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type,
6263
typevars=['T'],
6364
variances=[COVARIANT]) # class tuple
6465
self.type_typei = self.make_type_info('builtins.type') # class type
66+
self.bool_type_info = self.make_type_info('builtins.bool')
6567
self.functioni = self.make_type_info('builtins.function') # function TODO
6668
self.ai = self.make_type_info('A', mro=[self.oi]) # class A
6769
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,
165167
self.type_t = TypeType.make_normalized(self.t)
166168
self.type_any = TypeType.make_normalized(self.anyt)
167169

170+
self._add_bool_dunder(self.bool_type_info)
171+
self._add_bool_dunder(self.ai)
172+
173+
def _add_bool_dunder(self, type_info: TypeInfo) -> None:
174+
signature = CallableType([], [], [], Instance(self.bool_type_info, []), self.function)
175+
bool_func = FuncDef('__bool__', [], Block([]))
176+
bool_func.type = set_callable_name(signature, bool_func)
177+
type_info.names[bool_func.name] = SymbolTableNode(MDEF, bool_func)
178+
168179
# Helper methods
169180

170181
def callable(self, *a: Type) -> CallableType:

test-data/unit/check-errorcodes.test

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,3 +822,54 @@ from typing_extensions import TypedDict
822822

823823
Foo = TypedDict("Bar", {}) # E: First argument "Bar" to TypedDict() does not match variable name "Foo" [name-match]
824824
[builtins fixtures/dict.pyi]
825+
[case testTruthyBool]
826+
# flags: --enable-error-code truthy-bool
827+
from typing import List, Union
828+
829+
class Foo:
830+
pass
831+
832+
foo = Foo()
833+
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]
834+
pass
835+
836+
zero = 0
837+
if zero:
838+
pass
839+
840+
false = False
841+
if false:
842+
pass
843+
844+
null = None
845+
if null:
846+
pass
847+
848+
s = ''
849+
if s:
850+
pass
851+
852+
good_union: Union[str, int] = 5
853+
if good_union:
854+
pass
855+
if not good_union:
856+
pass
857+
858+
bad_union: Union[Foo, object] = Foo()
859+
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]
860+
pass
861+
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]
862+
pass
863+
864+
def f():
865+
pass
866+
if f: # E: Function "def () -> Any" could always be true in boolean context [truthy-bool]
867+
pass
868+
if not f: # E: Function "def () -> Any" could always be true in boolean context [truthy-bool]
869+
pass
870+
conditional_result = 'foo' if f else 'bar' # E: Function "def () -> Any" could always be true in boolean context [truthy-bool]
871+
872+
lst: List[int] = []
873+
if lst:
874+
pass
875+
[builtins fixtures/list.pyi]

test-data/unit/fixtures/list.pyi

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class list(Sequence[T]):
1616
@overload
1717
def __init__(self, x: Iterable[T]) -> None: pass
1818
def __iter__(self) -> Iterator[T]: pass
19+
def __len__(self) -> int: pass
1920
def __contains__(self, item: object) -> bool: pass
2021
def __add__(self, x: list[T]) -> list[T]: pass
2122
def __mul__(self, x: int) -> list[T]: pass
@@ -26,9 +27,12 @@ class list(Sequence[T]):
2627

2728
class tuple(Generic[T]): pass
2829
class function: pass
29-
class int: pass
30-
class float: pass
31-
class str: pass
30+
class int:
31+
def __bool__(self) -> bool: pass
32+
class float:
33+
def __bool__(self) -> bool: pass
34+
class str:
35+
def __len__(self) -> bool: pass
3236
class bool(int): pass
3337

3438
property = object() # Dummy definition.

0 commit comments

Comments
 (0)