diff --git a/ChangeLog b/ChangeLog index 37523bbe33..68d3b29cb4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -35,6 +35,11 @@ Release date: TBA Closes #4798 Closes #5081 +* Fix a crash in ``unused-private-member`` checker when analyzing code using + ``type(self)`` in bound methods. + + Closes #5569 + * Pyreverse - add output in mermaidjs format * ``used-before-assignment`` now considers that assignments in a try block diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 0d38432568..959ded73b8 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -74,6 +74,11 @@ Other Changes Closes #4716 +* Fix a crash in ``unused-private-member`` checker when analyzing code using + ``type(self)`` in bound methods. + + Closes #5569 + * Fix crash in ``unnecessary-dict-index-lookup`` checker if the output of ``items()`` is assigned to a 1-tuple. diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index aaa5de0cb2..f64e7addc5 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -970,6 +970,9 @@ def _check_unused_private_attributes(self, node: nodes.ClassDef) -> None: if attribute.attrname != assign_attr.attrname: continue + if self._is_type_self_call(attribute.expr): + continue + if ( assign_attr.expr.name in { @@ -1666,7 +1669,7 @@ def _is_called_inside_special_method(node: nodes.NodeNG) -> bool: return False return frame_name and frame_name in PYMETHODS - def _is_type_self_call(self, expr): + def _is_type_self_call(self, expr: nodes.NodeNG) -> bool: return ( isinstance(expr, nodes.Call) and isinstance(expr.func, nodes.Name) @@ -2029,16 +2032,24 @@ def _uses_mandatory_method_param(self, node): """ return self._is_mandatory_method_param(node.expr) - def _is_mandatory_method_param(self, node): + def _is_mandatory_method_param(self, node: nodes.NodeNG) -> bool: """Check if nodes.Name corresponds to first attribute variable name Name is `self` for method, `cls` for classmethod and `mcs` for metaclass. """ - return ( - self._first_attrs - and isinstance(node, nodes.Name) - and node.name == self._first_attrs[-1] - ) + if self._first_attrs: + first_attr = self._first_attrs[-1] + else: + # It's possible the function was already unregistered. + closest_func = utils.get_node_first_ancestor_of_type( + node, nodes.FunctionDef + ) + if closest_func is None: + return False + if not closest_func.args.args: + return False + first_attr = closest_func.args.args[0].name + return isinstance(node, nodes.Name) and node.name == first_attr def _ancestors_to_call(klass_node, method="__init__"): diff --git a/tests/functional/u/unused/unused_private_member.py b/tests/functional/u/unused/unused_private_member.py index fc42ea8fb8..1b81d738b8 100644 --- a/tests/functional/u/unused/unused_private_member.py +++ b/tests/functional/u/unused/unused_private_member.py @@ -309,3 +309,13 @@ class Foo: def method(self): print(self.__class__.__ham) + + +class TypeSelfCallInMethod: + """Regression test for issue 5569""" + @classmethod + def b(cls) -> None: + cls.__a = '' # [unused-private-member] + + def a(self): + return type(self).__a diff --git a/tests/functional/u/unused/unused_private_member.txt b/tests/functional/u/unused/unused_private_member.txt index 8d69dd02c3..99f34b7f53 100644 --- a/tests/functional/u/unused/unused_private_member.txt +++ b/tests/functional/u/unused/unused_private_member.txt @@ -17,3 +17,4 @@ unused-private-member:243:12:243:50:FalsePositive4681.__init__:Unused private me unused-private-member:274:4:275:26:FalsePositive4849.__unused_private_method:Unused private member `FalsePositive4849.__unused_private_method()`:UNDEFINED unused-private-member:291:4:294:44:Pony.__init_defaults:Unused private member `Pony.__init_defaults(self)`:UNDEFINED unused-private-member:296:4:298:20:Pony.__get_fur_color:Unused private member `Pony.__get_fur_color(self)`:UNDEFINED +unused-private-member:318:8:318:15:TypeSelfCallInMethod.b:Unused private member `TypeSelfCallInMethod.__a`:UNDEFINED