Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error-code for truthy-iterable #13762

Merged
merged 3 commits into from
Nov 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions docs/source/error_code_list2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,31 +231,29 @@ since unless implemented by a sub-type, the expression will always evaluate to t
if foo:
...

The check is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``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.

This check might falsely imply an error. For example, ``Iterable`` does not implement
``__len__`` and so this code will be flagged:

.. code-block:: python
Check that iterable is not implicitly true in boolean context [truthy-iterable]
-------------------------------------------------------------------------------

# Use "mypy -enable-error-code truthy-bool ..."
``Iterable`` does not implement ``__len__`` and so this code will be flagged:

.. code-block:: python

from typing import Iterable

def transform(items: Iterable[int]) -> Iterable[int]:
# Error: "items" has type "Iterable[int]" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool]
def transform(items: Iterable[int]) -> list[int]:
# Error: "items" has type "Iterable[int]" which can always be true in boolean context. Consider using "Collection[int]" instead. [truthy-iterable]
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: Sequence[int]``.

This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``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.
If called with a ``Generator`` like ``int(x) for x 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 is recommended to annotate ``items: Collection[int]``.


Check that function isn't used in boolean context [truthy-function]
Expand Down
8 changes: 8 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5088,6 +5088,14 @@ def format_expr_type() -> str:
self.fail(message_registry.FUNCTION_ALWAYS_TRUE.format(format_type(t)), expr)
elif isinstance(t, UnionType):
self.fail(message_registry.TYPE_ALWAYS_TRUE_UNIONTYPE.format(format_expr_type()), expr)
elif isinstance(t, Instance) and t.type.fullname == "typing.Iterable":
_, info = self.make_fake_typeinfo("typing", "Collection", "Collection", [])
self.fail(
message_registry.ITERABLE_ALWAYS_TRUE.format(
format_expr_type(), format_type(Instance(info, t.args))
),
expr,
)
else:
self.fail(message_registry.TYPE_ALWAYS_TRUE.format(format_expr_type()), expr)

Expand Down
6 changes: 6 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ def __str__(self) -> str:
"Warn about function that always evaluate to true in boolean contexts",
"General",
)
TRUTHY_ITERABLE: Final[ErrorCode] = ErrorCode(
"truthy-iterable",
"Warn about Iterable 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"
)
Expand Down
4 changes: 4 additions & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
FUNCTION_ALWAYS_TRUE: Final = ErrorMessage(
"Function {} could always be true in boolean context", code=codes.TRUTHY_FUNCTION
)
ITERABLE_ALWAYS_TRUE: Final = ErrorMessage(
"{} which can always be true in boolean context. Consider using {} instead.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message reads a bit weird to me but I guess it's consistent with the other ALWAYS_TRUE messages.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On its own, I agree. It gets better once the variables are inserted. E.g.

"var" has type "Iterable[str]" which can always be true in boolean context.
Consider using "Collection[str]" instead.  [truthy-iterable]

code=codes.TRUTHY_ITERABLE,
)
NOT_CALLABLE: Final = "{} not callable"
TYPE_MUST_BE_USED: Final = "Value of type {} must be used"

Expand Down
6 changes: 4 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
from __future__ import annotations

from contextlib import contextmanager
from typing import Any, Callable, Iterable, Iterator, List, TypeVar, cast
from typing import Any, Callable, Collection, Iterable, Iterator, List, TypeVar, cast
from typing_extensions import Final, TypeAlias as _TypeAlias

from mypy import errorcodes as codes, message_registry
Expand Down Expand Up @@ -6202,7 +6202,9 @@ def add_plugin_dependency(self, trigger: str, target: str | None = None) -> None
target = self.scope.current_target()
self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target)

def add_type_alias_deps(self, aliases_used: Iterable[str], target: str | None = None) -> None:
def add_type_alias_deps(
self, aliases_used: Collection[str], target: str | None = None
) -> None:
"""Add full names of type aliases on which the current node depends.

This is used by fine-grained incremental mode to re-check the corresponding nodes.
Expand Down
4 changes: 2 additions & 2 deletions mypyc/test-data/irbuild-statements.test
Original file line number Diff line number Diff line change
Expand Up @@ -1006,9 +1006,9 @@ L5:
return 1

[case testForZip]
from typing import List, Iterable
from typing import List, Iterable, Sequence

def f(a: List[int], b: Iterable[bool]) -> None:
def f(a: List[int], b: Sequence[bool]) -> None:
for x, y in zip(a, b):
if b:
x = 1
Expand Down
7 changes: 7 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,13 @@ if not f: # E: Function "Callable[[], Any]" could always be true in boolean con
pass
conditional_result = 'foo' if f else 'bar' # E: Function "Callable[[], Any]" could always be true in boolean context [truthy-function]

[case testTruthyIterable]
# flags: --strict-optional --enable-error-code truthy-iterable
from typing import Iterable
def func(var: Iterable[str]) -> None:
if var: # E: "var" has type "Iterable[str]" which can always be true in boolean context. Consider using "Collection[str]" instead. [truthy-iterable]
...

[case testNoOverloadImplementation]
from typing import overload

Expand Down