Skip to content

Commit

Permalink
Add broken NoReturn check (#5304)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Mar 10, 2022
1 parent 94f8099 commit 1faf365
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 1 deletion.
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,6 @@
[master]
py-version=3.7
load-plugins=pylint.extensions.typing

[testoptions]
min_pyver=3.7
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

0 comments on commit 1faf365

Please sign in to comment.