diff --git a/doc/data/messages/b/bad-dunder-name/bad.py b/doc/data/messages/b/bad-dunder-name/bad.py new file mode 100644 index 0000000000..f01f65010e --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/bad.py @@ -0,0 +1,6 @@ +class Apples: + def _init_(self): # [bad-dunder-name] + pass + + def __hello__(self): # [bad-dunder-name] + print("hello") diff --git a/doc/data/messages/b/bad-dunder-name/good.py b/doc/data/messages/b/bad-dunder-name/good.py new file mode 100644 index 0000000000..4f0adb9b62 --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/good.py @@ -0,0 +1,6 @@ +class Apples: + def __init__(self): + pass + + def hello(self): + print("hello") diff --git a/doc/data/messages/b/bad-dunder-name/pylintrc b/doc/data/messages/b/bad-dunder-name/pylintrc new file mode 100644 index 0000000000..c70980544f --- /dev/null +++ b/doc/data/messages/b/bad-dunder-name/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.dunder diff --git a/doc/whatsnew/fragments/3038.new_check b/doc/whatsnew/fragments/3038.new_check new file mode 100644 index 0000000000..8e61147fc3 --- /dev/null +++ b/doc/whatsnew/fragments/3038.new_check @@ -0,0 +1,4 @@ +Added ``bad-dunder-name`` extension check, which flags bad or misspelled dunder methods. +You can use the ``good-dunder-names`` option to allow specific dunder names. + +Closes #3038 diff --git a/pylint/checkers/dunder_methods.py b/pylint/checkers/dunder_methods.py index 2e5e54a57c..987e539aab 100644 --- a/pylint/checkers/dunder_methods.py +++ b/pylint/checkers/dunder_methods.py @@ -10,122 +10,19 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import safe_infer +from pylint.constants import DUNDER_METHODS from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint import PyLinter -DUNDER_METHODS: dict[tuple[int, int], dict[str, str]] = { - (0, 0): { - "__init__": "Instantiate class directly", - "__del__": "Use del keyword", - "__repr__": "Use repr built-in function", - "__str__": "Use str built-in function", - "__bytes__": "Use bytes built-in function", - "__format__": "Use format built-in function, format string method, or f-string", - "__lt__": "Use < operator", - "__le__": "Use <= operator", - "__eq__": "Use == operator", - "__ne__": "Use != operator", - "__gt__": "Use > operator", - "__ge__": "Use >= operator", - "__hash__": "Use hash built-in function", - "__bool__": "Use bool built-in function", - "__getattr__": "Access attribute directly or use getattr built-in function", - "__getattribute__": "Access attribute directly or use getattr built-in function", - "__setattr__": "Set attribute directly or use setattr built-in function", - "__delattr__": "Use del keyword", - "__dir__": "Use dir built-in function", - "__get__": "Use get method", - "__set__": "Use set method", - "__delete__": "Use del keyword", - "__instancecheck__": "Use isinstance built-in function", - "__subclasscheck__": "Use issubclass built-in function", - "__call__": "Invoke instance directly", - "__len__": "Use len built-in function", - "__length_hint__": "Use length_hint method", - "__getitem__": "Access item via subscript", - "__setitem__": "Set item via subscript", - "__delitem__": "Use del keyword", - "__iter__": "Use iter built-in function", - "__next__": "Use next built-in function", - "__reversed__": "Use reversed built-in function", - "__contains__": "Use in keyword", - "__add__": "Use + operator", - "__sub__": "Use - operator", - "__mul__": "Use * operator", - "__matmul__": "Use @ operator", - "__truediv__": "Use / operator", - "__floordiv__": "Use // operator", - "__mod__": "Use % operator", - "__divmod__": "Use divmod built-in function", - "__pow__": "Use ** operator or pow built-in function", - "__lshift__": "Use << operator", - "__rshift__": "Use >> operator", - "__and__": "Use & operator", - "__xor__": "Use ^ operator", - "__or__": "Use | operator", - "__radd__": "Use + operator", - "__rsub__": "Use - operator", - "__rmul__": "Use * operator", - "__rmatmul__": "Use @ operator", - "__rtruediv__": "Use / operator", - "__rfloordiv__": "Use // operator", - "__rmod__": "Use % operator", - "__rdivmod__": "Use divmod built-in function", - "__rpow__": "Use ** operator or pow built-in function", - "__rlshift__": "Use << operator", - "__rrshift__": "Use >> operator", - "__rand__": "Use & operator", - "__rxor__": "Use ^ operator", - "__ror__": "Use | operator", - "__iadd__": "Use += operator", - "__isub__": "Use -= operator", - "__imul__": "Use *= operator", - "__imatmul__": "Use @= operator", - "__itruediv__": "Use /= operator", - "__ifloordiv__": "Use //= operator", - "__imod__": "Use %= operator", - "__ipow__": "Use **= operator", - "__ilshift__": "Use <<= operator", - "__irshift__": "Use >>= operator", - "__iand__": "Use &= operator", - "__ixor__": "Use ^= operator", - "__ior__": "Use |= operator", - "__neg__": "Multiply by -1 instead", - "__pos__": "Multiply by +1 instead", - "__abs__": "Use abs built-in function", - "__invert__": "Use ~ operator", - "__complex__": "Use complex built-in function", - "__int__": "Use int built-in function", - "__float__": "Use float built-in function", - "__round__": "Use round built-in function", - "__trunc__": "Use math.trunc function", - "__floor__": "Use math.floor function", - "__ceil__": "Use math.ceil function", - "__enter__": "Invoke context manager directly", - "__aenter__": "Invoke context manager directly", - "__copy__": "Use copy.copy function", - "__deepcopy__": "Use copy.deepcopy function", - "__fspath__": "Use os.fspath function instead", - }, - (3, 10): { - "__aiter__": "Use aiter built-in function", - "__anext__": "Use anext built-in function", - }, -} - - class DunderCallChecker(BaseChecker): """Check for unnecessary dunder method calls. Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization - We exclude __new__, __subclasses__, __init_subclass__, __set_name__, - __class_getitem__, __missing__, __exit__, __await__, - __aexit__, __getnewargs_ex__, __getnewargs__, __getstate__, - __setstate__, __reduce__, __reduce_ex__, - and __index__ (see https://github.com/PyCQA/pylint/issues/6795) + We exclude names in list pylint.constants.EXTRA_DUNDER_METHODS such as + __index__ (see https://github.com/PyCQA/pylint/issues/6795) since these either have no alternative method of being called or have a genuine use case for being called manually. diff --git a/pylint/constants.py b/pylint/constants.py index 6ad5b82d35..d9ff20c466 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -168,3 +168,133 @@ def _get_pylint_home() -> str: "typing_extensions.Never", ) ) + +DUNDER_METHODS: dict[tuple[int, int], dict[str, str]] = { + (0, 0): { + "__init__": "Instantiate class directly", + "__del__": "Use del keyword", + "__repr__": "Use repr built-in function", + "__str__": "Use str built-in function", + "__bytes__": "Use bytes built-in function", + "__format__": "Use format built-in function, format string method, or f-string", + "__lt__": "Use < operator", + "__le__": "Use <= operator", + "__eq__": "Use == operator", + "__ne__": "Use != operator", + "__gt__": "Use > operator", + "__ge__": "Use >= operator", + "__hash__": "Use hash built-in function", + "__bool__": "Use bool built-in function", + "__getattr__": "Access attribute directly or use getattr built-in function", + "__getattribute__": "Access attribute directly or use getattr built-in function", + "__setattr__": "Set attribute directly or use setattr built-in function", + "__delattr__": "Use del keyword", + "__dir__": "Use dir built-in function", + "__get__": "Use get method", + "__set__": "Use set method", + "__delete__": "Use del keyword", + "__instancecheck__": "Use isinstance built-in function", + "__subclasscheck__": "Use issubclass built-in function", + "__call__": "Invoke instance directly", + "__len__": "Use len built-in function", + "__length_hint__": "Use length_hint method", + "__getitem__": "Access item via subscript", + "__setitem__": "Set item via subscript", + "__delitem__": "Use del keyword", + "__iter__": "Use iter built-in function", + "__next__": "Use next built-in function", + "__reversed__": "Use reversed built-in function", + "__contains__": "Use in keyword", + "__add__": "Use + operator", + "__sub__": "Use - operator", + "__mul__": "Use * operator", + "__matmul__": "Use @ operator", + "__truediv__": "Use / operator", + "__floordiv__": "Use // operator", + "__mod__": "Use % operator", + "__divmod__": "Use divmod built-in function", + "__pow__": "Use ** operator or pow built-in function", + "__lshift__": "Use << operator", + "__rshift__": "Use >> operator", + "__and__": "Use & operator", + "__xor__": "Use ^ operator", + "__or__": "Use | operator", + "__radd__": "Use + operator", + "__rsub__": "Use - operator", + "__rmul__": "Use * operator", + "__rmatmul__": "Use @ operator", + "__rtruediv__": "Use / operator", + "__rfloordiv__": "Use // operator", + "__rmod__": "Use % operator", + "__rdivmod__": "Use divmod built-in function", + "__rpow__": "Use ** operator or pow built-in function", + "__rlshift__": "Use << operator", + "__rrshift__": "Use >> operator", + "__rand__": "Use & operator", + "__rxor__": "Use ^ operator", + "__ror__": "Use | operator", + "__iadd__": "Use += operator", + "__isub__": "Use -= operator", + "__imul__": "Use *= operator", + "__imatmul__": "Use @= operator", + "__itruediv__": "Use /= operator", + "__ifloordiv__": "Use //= operator", + "__imod__": "Use %= operator", + "__ipow__": "Use **= operator", + "__ilshift__": "Use <<= operator", + "__irshift__": "Use >>= operator", + "__iand__": "Use &= operator", + "__ixor__": "Use ^= operator", + "__ior__": "Use |= operator", + "__neg__": "Multiply by -1 instead", + "__pos__": "Multiply by +1 instead", + "__abs__": "Use abs built-in function", + "__invert__": "Use ~ operator", + "__complex__": "Use complex built-in function", + "__int__": "Use int built-in function", + "__float__": "Use float built-in function", + "__round__": "Use round built-in function", + "__trunc__": "Use math.trunc function", + "__floor__": "Use math.floor function", + "__ceil__": "Use math.ceil function", + "__enter__": "Invoke context manager directly", + "__aenter__": "Invoke context manager directly", + "__copy__": "Use copy.copy function", + "__deepcopy__": "Use copy.deepcopy function", + "__fspath__": "Use os.fspath function instead", + }, + (3, 10): { + "__aiter__": "Use aiter built-in function", + "__anext__": "Use anext built-in function", + }, +} + +EXTRA_DUNDER_METHODS = [ + "__new__", + "__subclasses__", + "__init_subclass__", + "__set_name__", + "__class_getitem__", + "__missing__", + "__exit__", + "__await__", + "__aexit__", + "__getnewargs_ex__", + "__getnewargs__", + "__getstate__", + "__setstate__", + "__reduce__", + "__reduce_ex__", + "__post_init__", # part of `dataclasses` module +] + +DUNDER_PROPERTIES = [ + "__class__", + "__dict__", + "__doc__", + "__format__", + "__module__", + "__sizeof__", + "__subclasshook__", + "__weakref__", +] diff --git a/pylint/extensions/dunder.py b/pylint/extensions/dunder.py new file mode 100644 index 0000000000..e0e9af316a --- /dev/null +++ b/pylint/extensions/dunder.py @@ -0,0 +1,77 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.constants import DUNDER_METHODS, DUNDER_PROPERTIES, EXTRA_DUNDER_METHODS +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class DunderChecker(BaseChecker): + """Checks related to dunder methods.""" + + name = "dunder" + priority = -1 + msgs = { + "W3201": ( + "Bad or misspelled dunder method name %s.", + "bad-dunder-name", + "Used when a dunder method is misspelled or defined with a name " + "not within the predefined list of dunder names.", + ), + } + options = ( + ( + "good-dunder-names", + { + "default": [], + "type": "csv", + "metavar": "", + "help": "Good dunder names which should always be accepted.", + }, + ), + ) + + def open(self) -> None: + self._dunder_methods = ( + EXTRA_DUNDER_METHODS + + DUNDER_PROPERTIES + + self.linter.config.good_dunder_names + ) + for since_vers, dunder_methods in DUNDER_METHODS.items(): + if since_vers <= self.linter.config.py_version: + self._dunder_methods.extend(list(dunder_methods.keys())) + + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + """Check if known dunder method is misspelled or dunder name is not one + of the pre-defined names. + """ + # ignore module-level functions + if not node.is_method(): + return + + # Detect something that could be a bad dunder method + if ( + node.name.startswith("_") + and node.name.endswith("_") + and node.name not in self._dunder_methods + ): + self.add_message( + "bad-dunder-name", + node=node, + args=(node.name), + confidence=HIGH, + ) + + +def register(linter: PyLinter) -> None: + linter.register_checker(DunderChecker(linter)) diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.py b/tests/functional/ext/bad_dunder/bad_dunder_name.py new file mode 100644 index 0000000000..48247aba03 --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.py @@ -0,0 +1,54 @@ +# pylint: disable=missing-module-docstring, missing-class-docstring, +# pylint: disable=missing-function-docstring, unused-private-member + + +class Apples: + __slots__ = ("a", "b") + + def __hello__(self): # [bad-dunder-name] + # not one of the explicitly defined dunder name methods + print("hello") + + def hello(self): + print("hello") + + def __init__(self): + pass + + def init(self): + # valid name even though someone could accidentally mean __init__ + pass + + def __init_(self): # [bad-dunder-name] + # author likely unintentionally misspelled the correct init dunder. + pass + + def _init_(self): # [bad-dunder-name] + # author likely unintentionally misspelled the correct init dunder. + pass + + def ___neg__(self): # [bad-dunder-name] + # author likely accidentally added an additional `_` + pass + + def __inv__(self): # [bad-dunder-name] + # author likely meant to call the invert dunder method + pass + + def __allowed__(self): + # user-configured allowed dunder name + pass + + def _protected_method(self): + print("Protected") + + def __private_method(self): + print("Private") + + @property + def __doc__(self): + return "Docstring" + + +def __increase_me__(val): + return val + 1 diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.rc b/tests/functional/ext/bad_dunder/bad_dunder_name.rc new file mode 100644 index 0000000000..0b449f3a30 --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.rc @@ -0,0 +1,4 @@ +[MAIN] +load-plugins=pylint.extensions.dunder + +good-dunder-names = __allowed__, diff --git a/tests/functional/ext/bad_dunder/bad_dunder_name.txt b/tests/functional/ext/bad_dunder/bad_dunder_name.txt new file mode 100644 index 0000000000..bb1d1e692c --- /dev/null +++ b/tests/functional/ext/bad_dunder/bad_dunder_name.txt @@ -0,0 +1,5 @@ +bad-dunder-name:8:4:8:17:Apples.__hello__:Bad or misspelled dunder method name __hello__.:HIGH +bad-dunder-name:22:4:22:15:Apples.__init_:Bad or misspelled dunder method name __init_.:HIGH +bad-dunder-name:26:4:26:14:Apples._init_:Bad or misspelled dunder method name _init_.:HIGH +bad-dunder-name:30:4:30:16:Apples.___neg__:Bad or misspelled dunder method name ___neg__.:HIGH +bad-dunder-name:34:4:34:15:Apples.__inv__:Bad or misspelled dunder method name __inv__.:HIGH