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 bad-dunder-name checker #7642

Merged
merged 10 commits into from
Oct 31, 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
6 changes: 6 additions & 0 deletions doc/data/messages/b/bad-dunder-name/bad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Apples:
def _init_(self): # [bad-dunder-name]
pass

def __hello__(self): # [bad-dunder-name]
print("hello")
6 changes: 6 additions & 0 deletions doc/data/messages/b/bad-dunder-name/good.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Apples:
def __init__(self):
pass

def hello(self):
print("hello")
2 changes: 2 additions & 0 deletions doc/data/messages/b/bad-dunder-name/pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[MAIN]
load-plugins=pylint.extensions.dunder
4 changes: 4 additions & 0 deletions doc/whatsnew/fragments/3038.new_check
Original file line number Diff line number Diff line change
@@ -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
109 changes: 3 additions & 106 deletions pylint/checkers/dunder_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
130 changes: 130 additions & 0 deletions pylint/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__",
]
77 changes: 77 additions & 0 deletions pylint/extensions/dunder.py
Original file line number Diff line number Diff line change
@@ -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": "<comma-separated names>",
"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 (
Copy link
Collaborator

Choose a reason for hiding this comment

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

I missed the discussion here, but is there a reason why we don't check for __? Could _test_ be something private?

Copy link
Member

Choose a reason for hiding this comment

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

A function starting with _ is protected, a function starting with __ is private those should not raise. But _init__ is a bad dunder name that we should detect so a single underscore is enough.

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))
Loading