diff --git a/ChangeLog b/ChangeLog index ae0c1d6e21..8e99896db6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -53,6 +53,10 @@ Release date: TBA Closes #5323 +* Add checker ``unnecessary-ellipsis``: Emitted when the ellipsis constant is used unnecessarily. + + Closes #5460 + * Fixed incorrect classification of Numpy-style docstring as Google-style docstring for docstrings with property setter documentation. Docstring classification is now based on the highest amount of matched sections instead diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 8555108c11..3f4584fde0 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -10,6 +10,9 @@ Summary -- Release highlights New checkers ============ +* ``unnecessary-ellipsis``: Emmitted when the ellipsis constant is used unnecessarily. + + Closes #5460 Removed checkers ================ diff --git a/pylint/checkers/ellipsis_checker.py b/pylint/checkers/ellipsis_checker.py new file mode 100644 index 0000000000..ea7a260ec4 --- /dev/null +++ b/pylint/checkers/ellipsis_checker.py @@ -0,0 +1,50 @@ +"""Ellipsis checker for Python code +""" +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import check_messages +from pylint.interfaces import IAstroidChecker +from pylint.lint import PyLinter + + +class EllipsisChecker(BaseChecker): + __implements__ = (IAstroidChecker,) + name = "unnecessary_ellipsis" + msgs = { + "W2301": ( + "Unnecessary ellipsis constant", + "unnecessary-ellipsis", + "Used when the ellipsis constant is encountered and can be avoided. " + "A line of code consisting of an ellipsis is unnecessary if " + "there is a docstring on the preceding line or if there is a " + "statement in the same scope.", + ) + } + + @check_messages("unnecessary-ellipsis") + def visit_const(self, node: nodes.Const) -> None: + """Check if the ellipsis constant is used unnecessarily. + Emit a warning when: + - A line consisting of an ellipsis is preceded by a docstring. + - A statement exists in the same scope as the ellipsis. + For example: A function consisting of an ellipsis followed by a + return statement on the next line. + """ + if ( + node.pytype() == "builtins.Ellipsis" + and not isinstance(node.parent, (nodes.Assign, nodes.AnnAssign, nodes.Call)) + and ( + len(node.parent.parent.child_sequence(node.parent)) > 1 + or ( + isinstance(node.parent.parent, (nodes.ClassDef, nodes.FunctionDef)) + and (node.parent.parent.doc is not None) + ) + ) + ): + self.add_message("unnecessary-ellipsis", node=node) + + +def register(linter: PyLinter) -> None: + """required method to auto register this checker""" + linter.register_checker(EllipsisChecker(linter)) diff --git a/tests/functional/c/class_members.py b/tests/functional/c/class_members.py index e43ab57baa..e741c760b3 100644 --- a/tests/functional/c/class_members.py +++ b/tests/functional/c/class_members.py @@ -3,7 +3,6 @@ class Class: attr: int - ... # `bar` definitely does not exist here, but in a complex scenario, diff --git a/tests/functional/s/statement_without_effect.py b/tests/functional/s/statement_without_effect.py index 53da873d88..31fc7250f0 100644 --- a/tests/functional/s/statement_without_effect.py +++ b/tests/functional/s/statement_without_effect.py @@ -1,5 +1,5 @@ """Test for statements without effects.""" -# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-comprehension, use-list-literal +# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-comprehension, unnecessary-ellipsis, use-list-literal # +1:[pointless-string-statement] """inline doc string should use a separated message""" diff --git a/tests/functional/t/too/too_few_public_methods_excluded.py b/tests/functional/t/too/too_few_public_methods_excluded.py index 35ba873ee5..fecf497ad1 100644 --- a/tests/functional/t/too/too_few_public_methods_excluded.py +++ b/tests/functional/t/too/too_few_public_methods_excluded.py @@ -11,4 +11,3 @@ class MyJsonEncoder(JSONEncoder): class InheritedInModule(Control): """This class inherits from a class that doesn't have enough mehods, and its parent is excluded via config, so it doesn't raise.""" - ... diff --git a/tests/functional/u/unnecessary/unnecessary_ellipsis.py b/tests/functional/u/unnecessary/unnecessary_ellipsis.py new file mode 100644 index 0000000000..b5a61e3493 --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_ellipsis.py @@ -0,0 +1,99 @@ +"""Emit a warning when the ellipsis constant is used and can be avoided""" + +# pylint: disable=missing-docstring, too-few-public-methods + +from typing import List, overload, Union + +# Ellipsis and preceding statement +try: + A = 2 +except ValueError: + A = 24 + ... # [unnecessary-ellipsis] + +def ellipsis_and_subsequent_statement(): + ... # [unnecessary-ellipsis] + return 0 + +# The parent of ellipsis is an assignment +B = ... +C = [..., 1, 2, 3] + +# The parent of ellipsis is a call +if "X" is type(...): + ... + +def docstring_only(): + '''In Python, stubbed functions often have a body that contains just a + single `...` constant, indicating that the function doesn't do + anything. However, a stubbed function can also have just a + docstring, and function with a docstring and no body also does + nothing. + ''' + + +# This function has no docstring, so it needs a `...` constant. +def ellipsis_only(): + ... + + +def docstring_and_ellipsis(): + '''This function doesn't do anything, but it has a docstring, so its + `...` constant is useless clutter. + + NEW CHECK: unnecessary-ellipsis + + This would check for stubs with both docstrings and `...` + constants, suggesting the removal of the useless `...` + constants + ''' + ... # [unnecessary-ellipsis] + + +class DocstringOnly: + '''The same goes for class stubs: docstring, or `...`, but not both. + ''' + + +# No problem +class EllipsisOnly: + ... + + +class DocstringAndEllipsis: + '''Whoops! Mark this one as bad too. + ''' + ... # [unnecessary-ellipsis] + + +# Function overloading +@overload +def summarize(data: int) -> float: ... + + +@overload +def summarize(data: str) -> str: ... + + +def summarize(data): + if isinstance(data, str): + ... + return float(data) + + + +# Method overloading +class MyIntegerList(List[int]): + @overload + def __getitem__(self, index: int) -> int: ... + + @overload + def __getitem__(self, index: slice) -> List[int]: ... + + def __getitem__(self, index: Union[int, slice]) -> Union[int, List[int]]: + if isinstance(index, int): + ... + elif isinstance(index, slice): + ... + else: + raise TypeError(...) diff --git a/tests/functional/u/unnecessary/unnecessary_ellipsis.txt b/tests/functional/u/unnecessary/unnecessary_ellipsis.txt new file mode 100644 index 0000000000..7b7b905890 --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_ellipsis.txt @@ -0,0 +1,4 @@ +unnecessary-ellipsis:12:4:12:7::Unnecessary ellipsis constant:UNDEFINED +unnecessary-ellipsis:15:4:15:7:ellipsis_and_subsequent_statement:Unnecessary ellipsis constant:UNDEFINED +unnecessary-ellipsis:50:4:50:7:docstring_and_ellipsis:Unnecessary ellipsis constant:UNDEFINED +unnecessary-ellipsis:66:4:66:7:DocstringAndEllipsis:Unnecessary ellipsis constant:UNDEFINED