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 broken NoReturn check #5304

Merged
merged 16 commits into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,12 @@ Release date: TBA

Closes #5371

* ``TypingChecker``

* Added new check ``broken-noreturn`` to detect broken uses of ``typing.NoReturn``
if ``py-version`` is set to Python ``3.7.1`` or below.
https://bugs.python.org/issue34921

* The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests
without these will trigger a ``DeprecationWarning``.

Expand Down Expand Up @@ -555,6 +561,7 @@ Release date: 2021-11-24
* Properly identify parameters with no documentation and add new message called ``missing-any-param-doc``

Closes #3799

* Add checkers ``overridden-final-method`` & ``subclassed-final-class``

Closes #3197
Expand Down
6 changes: 6 additions & 0 deletions doc/whatsnew/2.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ Extensions

* Pyreverse - add output in mermaid-js format and html which is an mermaid js diagram with html boilerplate

* ``TypingChecker``

* Added new check ``broken-noreturn`` to detect broken uses of ``typing.NoReturn``
if ``py-version`` is set to Python ``3.7.1`` or below.
https://bugs.python.org/issue34921

* ``DocstringParameterChecker``

* Fixed incorrect classification of Numpy-style docstring as Google-style docstring for
Expand Down
51 changes: 50 additions & 1 deletion pylint/extensions/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from pylint.checkers.utils import (
check_messages,
is_node_in_type_annotation_context,
is_postponed_evaluation_enabled,
safe_infer,
)
from pylint.interfaces import IAstroidChecker
from pylint.interfaces import INFERENCE, IAstroidChecker
from pylint.utils.utils import get_global_option

if TYPE_CHECKING:
Expand Down Expand Up @@ -68,6 +69,12 @@ class TypingAlias(NamedTuple):

ALIAS_NAMES = frozenset(key.split(".")[1] for key in DEPRECATED_TYPING_ALIASES)
UNION_NAMES = ("Optional", "Union")
TYPING_NORETURN = frozenset(
(
"typing.NoReturn",
"typing_extensions.NoReturn",
)
)


class DeprecatedTypingAliasMsg(NamedTuple):
Expand Down Expand Up @@ -103,6 +110,14 @@ class TypingChecker(BaseChecker):
"Emitted when 'typing.Union' or 'typing.Optional' is used "
"instead of the alternative Union syntax 'int | None'.",
),
"E6004": (
"'NoReturn' inside compound types is broken in 3.7.0 / 3.7.1",
"broken-noreturn",
"``typing.NoReturn`` inside compound types is broken in "
"Python 3.7.0 and 3.7.1. If not dependent on runtime introspection, "
"use string annotation instead. E.g. "
"``Callable[..., 'NoReturn']``. https://bugs.python.org/issue34921",
),
}
options = (
(
Expand Down Expand Up @@ -153,6 +168,8 @@ def open(self) -> None:
self._py37_plus and self.config.runtime_typing is False
)

self._should_check_noreturn = py_version < (3, 7, 2)

def _msg_postponed_eval_hint(self, node) -> str:
"""Message hint if postponed evaluation isn't enabled."""
if self._py310_plus or "annotations" in node.root().future_imports:
Expand All @@ -163,23 +180,29 @@ def _msg_postponed_eval_hint(self, node) -> str:
"deprecated-typing-alias",
"consider-using-alias",
"consider-alternative-union-syntax",
"broken-noreturn",
)
def visit_name(self, node: nodes.Name) -> None:
if self._should_check_typing_alias and node.name in ALIAS_NAMES:
self._check_for_typing_alias(node)
if self._should_check_alternative_union_syntax and node.name in UNION_NAMES:
self._check_for_alternative_union_syntax(node, node.name)
if self._should_check_noreturn and node.name == "NoReturn":
self._check_broken_noreturn(node)

@check_messages(
"deprecated-typing-alias",
"consider-using-alias",
"consider-alternative-union-syntax",
"broken-noreturn",
)
def visit_attribute(self, node: nodes.Attribute) -> None:
if self._should_check_typing_alias and node.attrname in ALIAS_NAMES:
self._check_for_typing_alias(node)
if self._should_check_alternative_union_syntax and node.attrname in UNION_NAMES:
self._check_for_alternative_union_syntax(node, node.attrname)
if self._should_check_noreturn and node.attrname == "NoReturn":
self._check_broken_noreturn(node)

def _check_for_alternative_union_syntax(
self,
Expand Down Expand Up @@ -279,6 +302,32 @@ def leave_module(self, node: nodes.Module) -> None:
self._alias_name_collisions.clear()
self._consider_using_alias_msgs.clear()

def _check_broken_noreturn(self, node: Union[nodes.Name, nodes.Attribute]) -> None:
"""Check for 'NoReturn' inside compound types."""
if not isinstance(node.parent, nodes.BaseContainer):
# NoReturn not part of a Union or Callable type
return

if is_postponed_evaluation_enabled(node) and is_node_in_type_annotation_context(
node
):
return

for inferred in node.infer():
# To deal with typing_extensions, don't use safe_infer
if (
isinstance(inferred, (nodes.FunctionDef, nodes.ClassDef))
and inferred.qname() in TYPING_NORETURN
# In Python 3.6, NoReturn is alias of '_NoReturn'
# In Python 3.7 - 3.8, NoReturn is alias of '_SpecialForm'
or isinstance(inferred, astroid.bases.BaseInstance)
and isinstance(inferred._proxied, nodes.ClassDef)
and inferred._proxied.qname()
in {"typing._NoReturn", "typing._SpecialForm"}
):
self.add_message("broken-noreturn", node=node, confidence=INFERENCE)
break


def register(linter: "PyLinter") -> None:
linter.register_checker(TypingChecker(linter))
1 change: 1 addition & 0 deletions requirements_test_min.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-e .[testutil]
# astroid dependency is also defined in setup.cfg
astroid==2.9.3 # Pinned to a specific version for tests
typing-extensions~=4.0
pytest~=7.0
pytest-benchmark~=3.4
32 changes: 32 additions & 0 deletions tests/functional/ext/typing/typing_broken_noreturn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
'typing.NoReturn' is broken inside compond types for Python 3.7.0
https://bugs.python.org/issue34921

If no runtime introspection is required, use string annotations instead.
"""
# pylint: disable=missing-docstring
import typing
from typing import Callable, NoReturn, Union

import typing_extensions


def func1() -> NoReturn:
raise Exception

def func2() -> Union[None, NoReturn]: # [broken-noreturn]
pass

def func3() -> Union[None, "NoReturn"]:
pass

def func4() -> Union[None, typing.NoReturn]: # [broken-noreturn]
pass

def func5() -> Union[None, typing_extensions.NoReturn]: # [broken-noreturn]
pass


Alias1 = NoReturn
Alias2 = Callable[..., NoReturn] # [broken-noreturn]
Alias3 = Callable[..., "NoReturn"]
3 changes: 3 additions & 0 deletions tests/functional/ext/typing/typing_broken_noreturn.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[master]
py-version=3.7
load-plugins=pylint.extensions.typing
4 changes: 4 additions & 0 deletions tests/functional/ext/typing/typing_broken_noreturn.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
broken-noreturn:17:27:17:35:func2:'NoReturn' inside compound types is broken in 3.7.0 / 3.7.1:INFERENCE
broken-noreturn:23:27:23:42:func4:'NoReturn' inside compound types is broken in 3.7.0 / 3.7.1:INFERENCE
broken-noreturn:26:27:26:53:func5:'NoReturn' inside compound types is broken in 3.7.0 / 3.7.1:INFERENCE
broken-noreturn:31:23:31:31::'NoReturn' inside compound types is broken in 3.7.0 / 3.7.1:INFERENCE
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
'typing.NoReturn' is broken inside compond types for Python 3.7.0
https://bugs.python.org/issue34921

If no runtime introspection is required, use string annotations instead.

With 'from __future__ import annotations', only emit errors for nodes
not in a type annotation context.
"""
# pylint: disable=missing-docstring
from __future__ import annotations

import typing
from typing import Callable, NoReturn, Union

import typing_extensions


def func1() -> NoReturn:
raise Exception

def func2() -> Union[None, NoReturn]:
pass

def func3() -> Union[None, "NoReturn"]:
pass

def func4() -> Union[None, typing.NoReturn]:
pass

def func5() -> Union[None, typing_extensions.NoReturn]:
pass


Alias1 = NoReturn
Alias2 = Callable[..., NoReturn] # [broken-noreturn]
Alias3 = Callable[..., "NoReturn"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[master]
py-version=3.7
load-plugins=pylint.extensions.typing
cdce8p marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
broken-noreturn:36:23:36:31::'NoReturn' inside compound types is broken in 3.7.0 / 3.7.1:INFERENCE
34 changes: 34 additions & 0 deletions tests/functional/ext/typing/typing_broken_noreturn_py372.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
'typing.NoReturn' is broken inside compond types for Python 3.7.0
https://bugs.python.org/issue34921
If no runtime introspection is required, use string annotations instead.
Don't emit errors if py-version set to >= 3.7.2.
"""
# pylint: disable=missing-docstring
import typing
from typing import Callable, NoReturn, Union

import typing_extensions


def func1() -> NoReturn:
raise Exception

def func2() -> Union[None, NoReturn]:
pass

def func3() -> Union[None, "NoReturn"]:
pass

def func4() -> Union[None, typing.NoReturn]:
pass

def func5() -> Union[None, typing_extensions.NoReturn]:
pass


Alias1 = NoReturn
Alias2 = Callable[..., NoReturn]
Alias3 = Callable[..., "NoReturn"]
3 changes: 3 additions & 0 deletions tests/functional/ext/typing/typing_broken_noreturn_py372.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[master]
py-version=3.7.2
load-plugins=pylint.extensions.typing