diff --git a/ChangeLog b/ChangeLog index 464a621213..30de6af729 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,10 @@ Release date: TBA .. Put new features here and also in 'doc/whatsnew/2.12.rst' +* Add checkers ``overridden-final-method`` & ``subclassed-final-class`` + + Closes #3197 + * Added support for ``ModuleNotFoundError`` (``import-error`` and ``no-name-in-module``). ``ModuleNotFoundError`` inherits from ``ImportError`` and was added in Python ``3.6`` diff --git a/doc/whatsnew/2.12.rst b/doc/whatsnew/2.12.rst index 494393adb1..7cdd12003e 100644 --- a/doc/whatsnew/2.12.rst +++ b/doc/whatsnew/2.12.rst @@ -12,6 +12,14 @@ Summary -- Release highlights New checkers ============ +* Checkers for ``typing.final`` + + * Added ``overridden-final-method``: Emitted when a method which is annotated with ``typing.final`` is overridden + + * Added ``subclassed-final-class``: Emitted when a class which is annotated with ``typing.final`` is subclassed + + Closes #3197 + Removed checkers ================ diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index 14e0ef6350..489bb5063c 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -79,6 +79,7 @@ safe_infer, unimplemented_abstract_methods, ) +from pylint.constants import PY38_PLUS from pylint.interfaces import IAstroidChecker from pylint.utils import get_global_option @@ -649,6 +650,16 @@ def _has_same_layout_slots(slots, assigned_value): "unused-private-member", "Emitted when a private member of a class is defined but not used.", ), + "W0239": ( + "Method %r overrides a method decorated with typing.final which is defined in class %r", + "overridden-final-method", + "Used when a method decorated with typing.final has been overridden.", + ), + "W0240": ( + "Class %r is a subclass of a class decorated with typing.final: %r", + "subclassed-final-class", + "Used when a class decorated with typing.final has been subclassed.", + ), "E0236": ( "Invalid object %r in __slots__, must contain only non empty strings", "invalid-slots-object", @@ -860,6 +871,7 @@ def visit_classdef(self, node: nodes.ClassDef) -> None: self.add_message("no-init", args=node, node=node) self._check_slots(node) self._check_proper_bases(node) + self._check_typing_final(node) self._check_consistent_mro(node) def _check_consistent_mro(self, node): @@ -898,6 +910,23 @@ def _check_proper_bases(self, node): "useless-object-inheritance", args=node.name, node=node ) + def _check_typing_final(self, node: nodes.ClassDef) -> None: + """Detect that a class does not subclass a class decorated with `typing.final`""" + if not PY38_PLUS: + return + for base in node.bases: + ancestor = safe_infer(base) + if not ancestor: + continue + if isinstance(ancestor, nodes.ClassDef) and decorated_with( + ancestor, ["typing.final"] + ): + self.add_message( + "subclassed-final-class", + args=(node.name, ancestor.name), + node=node, + ) + @check_messages("unused-private-member", "attribute-defined-outside-init") def leave_classdef(self, node: nodes.ClassDef) -> None: """close a class node: @@ -1347,6 +1376,12 @@ def _check_invalid_overridden_method(self, function_node, parent_function_node): args=(function_node.name, "non-async", "async"), node=function_node, ) + if decorated_with(parent_function_node, ["typing.final"]) and PY38_PLUS: + self.add_message( + "overridden-final-method", + args=(function_node.name, parent_function_node.parent.name), + node=function_node, + ) def _check_slots(self, node): if "__slots__" not in node.locals: diff --git a/tests/functional/o/overridden_final_method_py38.py b/tests/functional/o/overridden_final_method_py38.py new file mode 100644 index 0000000000..d951c26da3 --- /dev/null +++ b/tests/functional/o/overridden_final_method_py38.py @@ -0,0 +1,17 @@ +"""Since Python version 3.8, a method decorated with typing.final cannot be +overridden""" + +# pylint: disable=no-init, import-error, invalid-name, using-constant-test, useless-object-inheritance +# pylint: disable=missing-docstring, too-few-public-methods + +from typing import final + +class Base: + @final + def my_method(self): + pass + + +class Subclass(Base): + def my_method(self): # [overridden-final-method] + pass diff --git a/tests/functional/o/overridden_final_method_py38.rc b/tests/functional/o/overridden_final_method_py38.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/o/overridden_final_method_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/o/overridden_final_method_py38.txt b/tests/functional/o/overridden_final_method_py38.txt new file mode 100644 index 0000000000..2c8bca4424 --- /dev/null +++ b/tests/functional/o/overridden_final_method_py38.txt @@ -0,0 +1 @@ +overridden-final-method:16:4:Subclass.my_method:Method 'my_method' overrides a method decorated with typing.final which is defined in class 'Base':HIGH diff --git a/tests/functional/s/subclassed_final_class_py38.py b/tests/functional/s/subclassed_final_class_py38.py new file mode 100644 index 0000000000..816ef537e7 --- /dev/null +++ b/tests/functional/s/subclassed_final_class_py38.py @@ -0,0 +1,16 @@ +"""Since Python version 3.8, a class decorated with typing.final cannot be +subclassed """ + +# pylint: disable=no-init, import-error, invalid-name, using-constant-test, useless-object-inheritance +# pylint: disable=missing-docstring, too-few-public-methods + +from typing import final + + +@final +class Base: + pass + + +class Subclass(Base): # [subclassed-final-class] + pass diff --git a/tests/functional/s/subclassed_final_class_py38.rc b/tests/functional/s/subclassed_final_class_py38.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/s/subclassed_final_class_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/s/subclassed_final_class_py38.txt b/tests/functional/s/subclassed_final_class_py38.txt new file mode 100644 index 0000000000..46fb5200e5 --- /dev/null +++ b/tests/functional/s/subclassed_final_class_py38.txt @@ -0,0 +1 @@ +subclassed-final-class:15:0:Subclass:"Class 'Subclass' is a subclass of a class decorated with typing.final: 'Base'":HIGH