summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Young <80432516+jpy-git@users.noreply.github.com>2022-03-30 11:47:30 +0100
committerGitHub <noreply@github.com>2022-03-30 12:47:30 +0200
commit1f0b406d3e7b7e670ff7f887c9ed25228da220d2 (patch)
treec3ef11d8056bd4245bd37d93aab8180792cb91b8
parent5324fecb32b0c86a70c88fc196360d721085fdfc (diff)
downloadpylint-git-1f0b406d3e7b7e670ff7f887c9ed25228da220d2.tar.gz
C2801: New check for manual __dunder__ methods (#5938)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com> Co-authored-by: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>
-rw-r--r--ChangeLog4
-rw-r--r--doc/data/messages/u/unnecessary-dunder-call/bad.py6
-rw-r--r--doc/data/messages/u/unnecessary-dunder-call/good.py6
-rw-r--r--doc/whatsnew/2.14.rst4
-rw-r--r--pylint/checkers/base_checker.py2
-rw-r--r--pylint/checkers/dunder_methods.py151
-rw-r--r--tests/functional/a/assigning/assigning_non_slot.py2
-rw-r--r--tests/functional/c/class_members_py30.py2
-rw-r--r--tests/functional/g/generic_alias/generic_alias_typing.py2
-rw-r--r--tests/functional/i/inner_classes.py2
-rw-r--r--tests/functional/n/non/non_init_parent_called.py2
-rw-r--r--tests/functional/t/too/too_many_arguments.py2
-rw-r--r--tests/functional/u/unnecessary/unnecessary_dunder_call.py46
-rw-r--r--tests/functional/u/unnecessary/unnecessary_dunder_call.txt3
14 files changed, 227 insertions, 7 deletions
diff --git a/ChangeLog b/ChangeLog
index 9214b58b1..a634e6b7c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -10,6 +10,10 @@ Release date: TBA
Put new features here and also in 'doc/whatsnew/2.14.rst'
+* Add new check ``unnecessary-dunder-call`` for unnecessary dunder method calls.
+
+ Closes #5936
+
* ``potential-index-error``: Emitted when the index of a list or tuple exceeds its length.
This checker is currently quite conservative to avoid false positives. We welcome
suggestions for improvements.
diff --git a/doc/data/messages/u/unnecessary-dunder-call/bad.py b/doc/data/messages/u/unnecessary-dunder-call/bad.py
new file mode 100644
index 000000000..66149d772
--- /dev/null
+++ b/doc/data/messages/u/unnecessary-dunder-call/bad.py
@@ -0,0 +1,6 @@
+three = 3.0.__str__() # [unnecessary-dunder-call]
+twelve = "1".__add__("2") # [unnecessary-dunder-call]
+
+
+def is_bigger_than_two(x):
+ return x.__gt__(2) # [unnecessary-dunder-call]
diff --git a/doc/data/messages/u/unnecessary-dunder-call/good.py b/doc/data/messages/u/unnecessary-dunder-call/good.py
new file mode 100644
index 000000000..fe41db776
--- /dev/null
+++ b/doc/data/messages/u/unnecessary-dunder-call/good.py
@@ -0,0 +1,6 @@
+three = str(3.0)
+twelve = "1" + "2"
+
+
+def is_bigger_than_two(x):
+ return x > 2
diff --git a/doc/whatsnew/2.14.rst b/doc/whatsnew/2.14.rst
index ad38200e7..ecb7388de 100644
--- a/doc/whatsnew/2.14.rst
+++ b/doc/whatsnew/2.14.rst
@@ -12,6 +12,10 @@ Summary -- Release highlights
New checkers
============
+* Add new check ``unnecessary-dunder-call`` for unnecessary dunder method calls.
+
+ Closes #5936
+
* ``potential-index-error``: Emitted when the index of a list or tuple exceeds its length.
This checker is currently quite conservative to avoid false positives. We welcome
suggestions for improvements.
diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py
index 21c63364a..31e178b32 100644
--- a/pylint/checkers/base_checker.py
+++ b/pylint/checkers/base_checker.py
@@ -61,7 +61,7 @@ class BaseChecker(OptionsProviderMixIn):
def __gt__(self, other):
"""Permit to sort a list of Checker by name."""
- return f"{self.name}{self.msgs}".__gt__(f"{other.name}{other.msgs}")
+ return f"{self.name}{self.msgs}" > (f"{other.name}{other.msgs}")
def __repr__(self):
status = "Checker" if self.enabled else "Disabled checker"
diff --git a/pylint/checkers/dunder_methods.py b/pylint/checkers/dunder_methods.py
new file mode 100644
index 000000000..4c260c34a
--- /dev/null
+++ b/pylint/checkers/dunder_methods.py
@@ -0,0 +1,151 @@
+# 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 typing import TYPE_CHECKING
+
+from astroid import nodes
+
+from pylint.checkers import BaseChecker
+from pylint.interfaces import HIGH, IAstroidChecker
+
+if TYPE_CHECKING:
+ from pylint.lint import PyLinter
+
+
+class DunderCallChecker(BaseChecker):
+ """Check for unnecessary dunder method calls.
+
+ Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization
+ We exclude __init__, __new__, __subclasses__, __init_subclass__,
+ __set_name__, __class_getitem__, __missing__, __exit__, __await__,
+ __del__, __aexit__, __getnewargs_ex__, __getnewargs__, __getstate__,
+ __setstate__, __reduce__, __reduce_ex__
+ since these either have no alternative method of being called or
+ have a genuine use case for being called manually.
+ """
+
+ __implements__ = IAstroidChecker
+
+ includedict = {
+ "__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 funciton",
+ "__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",
+ "__index__": "Use index method",
+ "__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",
+ "__aiter__": "Use iter built-in function",
+ "__anext__": "Use next built-in function",
+ "__aenter__": "Invoke context manager directly",
+ "__copy__": "Use copy.copy function",
+ "__deepcopy__": "Use copy.deepcopy function",
+ "__fspath__": "Use os.fspath function instead",
+ }
+ name = "unnecessary-dunder-call"
+ priority = -1
+ msgs = {
+ "C2801": (
+ "Unnecessarily calls dunder method %s. %s.",
+ "unnecessary-dunder-call",
+ "Used when a dunder method is manually called instead "
+ "of using the corresponding function/method/operator.",
+ ),
+ }
+ options = ()
+
+ def visit_call(self, node: nodes.Call) -> None:
+ """Check if method being called is an unnecessary dunder method."""
+ if (
+ isinstance(node.func, nodes.Attribute)
+ and node.func.attrname in self.includedict
+ ):
+ self.add_message(
+ "unnecessary-dunder-call",
+ node=node,
+ args=(node.func.attrname, self.includedict[node.func.attrname]),
+ confidence=HIGH,
+ )
+
+
+def register(linter: "PyLinter") -> None:
+ linter.register_checker(DunderCallChecker(linter))
diff --git a/tests/functional/a/assigning/assigning_non_slot.py b/tests/functional/a/assigning/assigning_non_slot.py
index 2cd1483e0..0a0c0427c 100644
--- a/tests/functional/a/assigning/assigning_non_slot.py
+++ b/tests/functional/a/assigning/assigning_non_slot.py
@@ -1,7 +1,7 @@
""" Checks assigning attributes not found in class slots
will trigger assigning-non-slot warning.
"""
-# pylint: disable=too-few-public-methods, no-init, missing-docstring, import-error, useless-object-inheritance, redundant-u-string-prefix
+# pylint: disable=too-few-public-methods, no-init, missing-docstring, import-error, useless-object-inheritance, redundant-u-string-prefix, unnecessary-dunder-call
from collections import deque
from missing import Unknown
diff --git a/tests/functional/c/class_members_py30.py b/tests/functional/c/class_members_py30.py
index cb7267ce5..2e34127e3 100644
--- a/tests/functional/c/class_members_py30.py
+++ b/tests/functional/c/class_members_py30.py
@@ -1,5 +1,5 @@
""" Various tests for class members access. """
-# pylint: disable=too-few-public-methods,import-error,no-init,missing-docstring, wrong-import-position,wrong-import-order, useless-object-inheritance
+# pylint: disable=too-few-public-methods,import-error,no-init,missing-docstring, wrong-import-position,wrong-import-order, useless-object-inheritance, unnecessary-dunder-call
from missing import Missing
class MyClass(object):
"""class docstring"""
diff --git a/tests/functional/g/generic_alias/generic_alias_typing.py b/tests/functional/g/generic_alias/generic_alias_typing.py
index 2ce05deaf..6d114739a 100644
--- a/tests/functional/g/generic_alias/generic_alias_typing.py
+++ b/tests/functional/g/generic_alias/generic_alias_typing.py
@@ -1,7 +1,7 @@
"""Test generic alias support for typing.py types."""
# flake8: noqa
# pylint: disable=missing-docstring,pointless-statement
-# pylint: disable=too-few-public-methods,multiple-statements,line-too-long
+# pylint: disable=too-few-public-methods,multiple-statements,line-too-long, unnecessary-dunder-call
import abc
import typing
diff --git a/tests/functional/i/inner_classes.py b/tests/functional/i/inner_classes.py
index 7ce8101f0..cabae5734 100644
--- a/tests/functional/i/inner_classes.py
+++ b/tests/functional/i/inner_classes.py
@@ -1,4 +1,4 @@
-# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-pass
+# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-pass, unnecessary-dunder-call
"""Backend Base Classes for the schwelm user DB"""
__revision__ = "alpha"
diff --git a/tests/functional/n/non/non_init_parent_called.py b/tests/functional/n/non/non_init_parent_called.py
index 97489accf..7ad3f1932 100644
--- a/tests/functional/n/non/non_init_parent_called.py
+++ b/tests/functional/n/non/non_init_parent_called.py
@@ -1,5 +1,5 @@
# pylint: disable=protected-access,import-self,too-few-public-methods,line-too-long
-# pylint: disable=wrong-import-order, useless-object-inheritance,
+# pylint: disable=wrong-import-order, useless-object-inheritance, unnecessary-dunder-call
"""test for call to __init__ from a non ancestor class
"""
from __future__ import print_function
diff --git a/tests/functional/t/too/too_many_arguments.py b/tests/functional/t/too/too_many_arguments.py
index 689745e27..8d26902af 100644
--- a/tests/functional/t/too/too_many_arguments.py
+++ b/tests/functional/t/too/too_many_arguments.py
@@ -1,4 +1,4 @@
-# pylint: disable=missing-docstring,wrong-import-position
+# pylint: disable=missing-docstring,wrong-import-position,unnecessary-dunder-call
def stupid_function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9): # [too-many-arguments]
return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9
diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call.py b/tests/functional/u/unnecessary/unnecessary_dunder_call.py
new file mode 100644
index 000000000..341353a82
--- /dev/null
+++ b/tests/functional/u/unnecessary/unnecessary_dunder_call.py
@@ -0,0 +1,46 @@
+"""Checks for unnecessary-dunder-call."""
+# pylint: disable=too-few-public-methods, undefined-variable, useless-object-inheritance
+# pylint: disable=missing-class-docstring, missing-function-docstring
+
+# Test includelisted dunder methods raise lint when manually called.
+num_str = some_num.__str__() # [unnecessary-dunder-call]
+num_repr = some_num.__add__(2) # [unnecessary-dunder-call]
+my_repr = my_module.my_object.__repr__() # [unnecessary-dunder-call]
+
+# Test unknown/user-defined dunder methods don't raise lint.
+my_woohoo = my_object.__woohoo__()
+
+# Test allowed dunder methods don't raise lint.
+class Foo1(object):
+ def __init__(self):
+ object.__init__(self)
+
+class Foo2(object):
+ def __init__(self):
+ super().__init__(self)
+
+class Bar1(object):
+ def __new__(cls):
+ object.__new__(cls)
+
+class Bar2(object):
+ def __new__(cls):
+ super().__new__(cls)
+
+class Base:
+ @classmethod
+ def get_first_subclass(cls):
+ for subklass in cls.__subclasses__():
+ return subklass
+ return object
+
+class PluginBase(object):
+ subclasses = []
+
+ def __init_subclass__(cls, **kwargs):
+ super().__init_subclass__(**kwargs)
+ cls.subclasses.append(cls)
+
+# Test no lint raised for attributes.
+my_instance_name = x.__class__.__name__
+my_pkg_version = pkg.__version__
diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call.txt b/tests/functional/u/unnecessary/unnecessary_dunder_call.txt
new file mode 100644
index 000000000..208ad96bb
--- /dev/null
+++ b/tests/functional/u/unnecessary/unnecessary_dunder_call.txt
@@ -0,0 +1,3 @@
+unnecessary-dunder-call:6:10:6:28::Unnecessarily calls dunder method __str__. Use str built-in function.:HIGH
+unnecessary-dunder-call:7:11:7:30::Unnecessarily calls dunder method __add__. Use + operator.:HIGH
+unnecessary-dunder-call:8:10:8:40::Unnecessarily calls dunder method __repr__. Use repr built-in function.:HIGH