Skip to content

Commit e96ce45

Browse files
committed
Fail when conditioning on implicit bool
1 parent 61c3462 commit e96ce45

File tree

10 files changed

+155
-6
lines changed

10 files changed

+155
-6
lines changed

docs/source/error_code_list.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,24 @@ consistently when using the call-based syntax. Example:
665665
# Error: First argument to namedtuple() should be "Point2D", not "Point"
666666
Point2D = NamedTuple("Point", [("x", int), ("y", int)])
667667
668+
669+
670+
Implicitly converting to bool always evaluates as True [implicit-bool]
671+
----------------------------------------------------------------------
672+
673+
In contexts where an expression has to be converted to a bool, an object
674+
which does not implement __bool__ nor __len__ will always evaluate to True.
675+
676+
.. code-block:: python
677+
678+
class Foo:
679+
pass
680+
681+
foo = Foo()
682+
# Error: "{}" does not implement __bool__ or __len__ so the expression is always truthy
683+
if foo:
684+
...
685+
668686
Report syntax errors [syntax]
669687
-----------------------------
670688

mypy/checker.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
try_getting_str_literals_from_type, try_getting_int_literals_from_type,
5252
tuple_fallback, is_singleton_type, try_expanding_enum_to_union,
5353
true_only, false_only, function_type, get_type_vars, custom_special_method,
54-
is_literal_type_like,
54+
is_literal_type_like, type_has_explicit_conversion_to_bool,
5555
)
5656
from mypy import message_registry
5757
from mypy.subtypes import (
@@ -3240,6 +3240,14 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
32403240
if self.in_checked_function():
32413241
self.fail(message_registry.RETURN_VALUE_EXPECTED, s, code=codes.RETURN_VALUE)
32423242

3243+
def _type_has_explicit_conversion_to_bool(self, t: Type) -> bool:
3244+
if isinstance(t, Instance):
3245+
return t.type.has_readable_member('__bool__') or t.type.has_readable_member('__len__')
3246+
elif isinstance(t, FunctionLike):
3247+
return False
3248+
else:
3249+
return True
3250+
32433251
def visit_if_stmt(self, s: IfStmt) -> None:
32443252
"""Type check an if statement."""
32453253
# This frame records the knowledge from previous if/elif clauses not being taken.
@@ -3251,6 +3259,14 @@ def visit_if_stmt(self, s: IfStmt) -> None:
32513259
if isinstance(t, DeletedType):
32523260
self.msg.deleted_as_rvalue(t, s)
32533261

3262+
if codes.IMPLICIT_BOOL in self.options.enabled_error_codes:
3263+
if isinstance(t, (Instance, FunctionLike)):
3264+
if not type_has_explicit_conversion_to_bool(t):
3265+
self.msg.type_converts_to_bool_implicitly(t, e)
3266+
elif isinstance(t, UnionType):
3267+
if not any(type_has_explicit_conversion_to_bool(item) for item in t.items):
3268+
self.msg.type_union_converts_to_bool_implicitly(t, e)
3269+
32543270
if_map, else_map = self.find_isinstance_check(e)
32553271

32563272
# XXX Issue a warning if condition is always False?

mypy/checkexpr.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
from mypy.typeops import (
6565
tuple_fallback, make_simplified_union, true_only, false_only, erase_to_union_or_bound,
6666
function_type, callable_type, try_getting_str_literals, custom_special_method,
67-
is_literal_type_like,
67+
is_literal_type_like, type_has_explicit_conversion_to_bool,
6868
)
6969
import mypy.errorcodes as codes
7070

@@ -3819,7 +3819,7 @@ def check_for_comp(self, e: Union[GeneratorExpr, DictionaryComprehension]) -> No
38193819
self.msg.redundant_condition_in_comprehension(True, condition)
38203820

38213821
def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = False) -> Type:
3822-
self.accept(e.cond)
3822+
cond_type = self.accept(e.cond)
38233823
ctx = self.type_context[-1]
38243824

38253825
# Gain type information from isinstance if it is there
@@ -3831,6 +3831,17 @@ def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = F
38313831
elif else_map is None:
38323832
self.msg.redundant_condition_in_if(True, e.cond)
38333833

3834+
if codes.IMPLICIT_BOOL in self.chk.options.enabled_error_codes:
3835+
cond_proper_type = get_proper_type(cond_type)
3836+
3837+
if isinstance(cond_proper_type, (Instance, FunctionLike)):
3838+
if not type_has_explicit_conversion_to_bool(cond_proper_type):
3839+
self.msg.type_converts_to_bool_implicitly(cond_proper_type, e.cond)
3840+
elif isinstance(cond_proper_type, UnionType):
3841+
if not any(type_has_explicit_conversion_to_bool(item)
3842+
for item in cond_proper_type.items):
3843+
self.msg.type_union_converts_to_bool_implicitly(cond_proper_type, e.cond)
3844+
38343845
if_type = self.analyze_cond_branch(if_map, e.if_expr, context=ctx,
38353846
allow_none_return=allow_none_return)
38363847

mypy/errorcodes.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ def __str__(self) -> str:
9292
'General') # type: Final
9393
EXIT_RETURN = ErrorCode(
9494
'exit-return', "Warn about too general return type for '__exit__'", 'General') # type: Final
95+
NAME_MATCH = ErrorCode(
96+
'name-match', "Check that type definition has consistent naming", 'General') # type: Final
97+
IMPLICIT_BOOL = ErrorCode(
98+
'implicit-bool', "Implicitly converting to bool always evaluates as True",
99+
'General') # type: Final
95100

96101
# These error codes aren't enabled by default.
97102
NO_UNTYPED_DEF = ErrorCode(
@@ -117,8 +122,6 @@ def __str__(self) -> str:
117122
"Warn about redundant expressions",
118123
'General',
119124
default_enabled=False) # type: Final
120-
NAME_MATCH = ErrorCode(
121-
'name-match', "Check that type definition has consistent naming", 'General') # type: Final
122125

123126

124127
# Syntax errors are often blocking.

mypy/message_registry.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@
8484
DESCRIPTOR_SET_NOT_CALLABLE = "{}.__set__ is not callable" # type: Final
8585
DESCRIPTOR_GET_NOT_CALLABLE = "{}.__get__ is not callable" # type: Final
8686
MODULE_LEVEL_GETATTRIBUTE = '__getattribute__ is not valid at the module level' # type: Final
87+
TYPE_CONVERTS_TO_BOOL_IMPLICITLY = \
88+
'"{}" does not implement __bool__ or __len__ so the expression is always truthy' # type: Final
89+
ALL_TYPES_IN_UNION_CONVERT_TO_BOOL_IMPLICITLY = \
90+
'Neither of {} implements __bool__ or __len__ '\
91+
'so the expression is always truthy' # type: Final
8792

8893
# Generic
8994
GENERIC_INSTANCE_VAR_CLASS_ACCESS = \

mypy/messages.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,14 @@ def deleted_as_lvalue(self, typ: DeletedType, context: Context) -> None:
718718
s = ' "{}"'.format(typ.source)
719719
self.fail('Assignment to variable{} outside except: block'.format(s), context)
720720

721+
def type_converts_to_bool_implicitly(self, typ: Type, context: Context):
722+
return self.fail(message_registry.TYPE_CONVERTS_TO_BOOL_IMPLICITLY
723+
.format(typ), context, code=codes.IMPLICIT_BOOL)
724+
725+
def type_union_converts_to_bool_implicitly(self, typ: UnionType, context: Context):
726+
return self.fail(message_registry.ALL_TYPES_IN_UNION_CONVERT_TO_BOOL_IMPLICITLY
727+
.format(typ), context, code=codes.IMPLICIT_BOOL)
728+
721729
def no_variant_matches_arguments(self,
722730
plausible_targets: List[CallableType],
723731
overload: Overloaded,

mypy/test/helpers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import List, Iterable, Dict, Tuple, Callable, Any, Optional, Iterator
99

1010
from mypy import defaults
11+
from mypy import errorcodes as codes
1112
import mypy.api as api
1213

1314
import pytest
@@ -372,16 +373,19 @@ def assert_type(typ: type, value: object) -> None:
372373
def parse_options(program_text: str, testcase: DataDrivenTestCase,
373374
incremental_step: int) -> Options:
374375
"""Parse comments like '# flags: --foo' in a test case."""
375-
options = Options()
376376
flags = re.search('# flags: (.*)$', program_text, flags=re.MULTILINE)
377377
if incremental_step > 1:
378378
flags2 = re.search('# flags{}: (.*)$'.format(incremental_step), program_text,
379379
flags=re.MULTILINE)
380380
if flags2:
381381
flags = flags2
382382

383+
disabled_error_codes = {codes.IMPLICIT_BOOL}
384+
383385
if flags:
384386
flag_list = flags.group(1).split()
387+
for code in disabled_error_codes:
388+
flag_list.extend(['--disable-error-code', code.code])
385389
flag_list.append('--no-site-packages') # the tests shouldn't need an installed Python
386390
targets, options = process_options(flag_list, require_targets=False)
387391
if targets:
@@ -393,6 +397,7 @@ def parse_options(program_text: str, testcase: DataDrivenTestCase,
393397
# TODO: Enable strict optional in test cases by default (requires *many* test case changes)
394398
options.strict_optional = False
395399
options.error_summary = False
400+
options.disabled_error_codes = disabled_error_codes
396401

397402
# Allow custom python version to override testcase_pyversion.
398403
if all(flag.split('=')[0] not in ['--python-version', '-2', '--py2'] for flag in flag_list):

mypy/typeops.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,15 @@ def get_enum_values(typ: Instance) -> List[str]:
642642
return [name for name, sym in typ.type.names.items() if isinstance(sym.node, Var)]
643643

644644

645+
def type_has_explicit_conversion_to_bool(typ: Type) -> bool:
646+
if isinstance(typ, Instance):
647+
return typ.type.has_readable_member('__bool__') or typ.type.has_readable_member('__len__')
648+
elif isinstance(typ, FunctionLike):
649+
return False
650+
else:
651+
return True
652+
653+
645654
def is_singleton_type(typ: Type) -> bool:
646655
"""Returns 'true' if this type is a "singleton type" -- if there exists
647656
exactly only one runtime value associated with this type.

test-data/unit/check-errorcodes.test

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,3 +807,61 @@ from typing_extensions import TypedDict
807807

808808
Foo = TypedDict("Bar", {}) # E: First argument "Bar" to TypedDict() does not match variable name "Foo" [name-match]
809809
[builtins fixtures/dict.pyi]
810+
[case testImplicitBool]
811+
# flags: --enable-error-code implicit-bool
812+
from typing import List, Union
813+
814+
class Foo:
815+
pass
816+
817+
818+
foo = Foo()
819+
if foo: # E: "__main__.Foo" does not implement __bool__ or __len__ so the expression is always truthy [implicit-bool]
820+
pass
821+
822+
zero = 0
823+
if zero:
824+
pass
825+
826+
827+
false = False
828+
if false:
829+
pass
830+
831+
832+
null = None
833+
if null:
834+
pass
835+
836+
837+
s = ''
838+
if s:
839+
pass
840+
841+
842+
good_union: Union[str, int] = 5
843+
if good_union:
844+
pass
845+
846+
847+
bad_union: Union[Foo, object] = Foo()
848+
if bad_union: # E: Neither of Union[__main__.Foo, builtins.object] implements __bool__ or __len__ so the expression is always truthy [implicit-bool]
849+
pass
850+
851+
852+
def f():
853+
pass
854+
855+
856+
if f: # E: "def () -> Any" does not implement __bool__ or __len__ so the expression is always truthy [implicit-bool]
857+
pass
858+
859+
860+
lst: List[int] = []
861+
if lst:
862+
pass
863+
864+
865+
conditional_result = 'foo' if f else 'bar' # E: "def () -> Any" does not implement __bool__ or __len__ so the expression is always truthy [implicit-bool]
866+
867+
[builtins fixtures/implicit_bool.pyi]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from typing import Generic, Sequence, TypeVar
2+
3+
_T = TypeVar('_T')
4+
5+
6+
class object:
7+
def __init__(self): pass
8+
9+
class type: pass
10+
class int:
11+
def __bool__(self) -> bool: pass
12+
class bool(int): pass
13+
class list(Generic[_T], Sequence[_T]):
14+
def __len__(self) -> int: pass
15+
class str:
16+
def __len__(self) -> int: pass

0 commit comments

Comments
 (0)