diff --git a/doc/data/messages/m/missing-param-type-annotation/bad.py b/doc/data/messages/m/missing-param-type-annotation/bad.py new file mode 100644 index 0000000000..344cb05772 --- /dev/null +++ b/doc/data/messages/m/missing-param-type-annotation/bad.py @@ -0,0 +1,12 @@ +def greet(name) -> str: # [missing-param-type-annotation] + return f"Hello, {name}!" + + +def add(x, y) -> int: # [missing-param-type-annotation, missing-param-type-annotation] + return x + y + + +def process( # [missing-param-type-annotation, missing-param-type-annotation] + *args, **kwargs +) -> dict: + return combine(args, kwargs) diff --git a/doc/data/messages/m/missing-param-type-annotation/details.rst b/doc/data/messages/m/missing-param-type-annotation/details.rst new file mode 100644 index 0000000000..baa72cab0a --- /dev/null +++ b/doc/data/messages/m/missing-param-type-annotation/details.rst @@ -0,0 +1,20 @@ +Type annotations improve code readability and enable better static analysis. This check +ensures that all function and method parameters have type annotations, making the expected +types clear and allowing type checkers like mypy to verify correct usage. + +This check is opt-in (disabled by default) to maintain backward compatibility. Enable it +with ``--enable=missing-param-type-annotation``. + +The check automatically skips: + +- ``self`` and ``cls`` parameters in methods +- Parameters in abstract methods (``@abstractmethod``, ``@abstractproperty``) +- Parameters in overload stub definitions (``@typing.overload``) + +All parameter types are checked, including: + +- Regular positional parameters +- Positional-only parameters (before ``/``) +- Keyword-only parameters (after ``*``) +- Variadic positional parameters (``*args``) +- Variadic keyword parameters (``**kwargs``) diff --git a/doc/data/messages/m/missing-param-type-annotation/good.py b/doc/data/messages/m/missing-param-type-annotation/good.py new file mode 100644 index 0000000000..e41e3abbaf --- /dev/null +++ b/doc/data/messages/m/missing-param-type-annotation/good.py @@ -0,0 +1,15 @@ +def greet(name: str) -> str: + return f"Hello, {name}!" + + +def add(x: int, y: int) -> int: + return x + y + + +def process(*args: str, **kwargs: bool) -> dict: + return combine(args, kwargs) + + +class Calculator: + def compute(self, x: int, y: int) -> int: # self doesn't need annotation + return x + y diff --git a/doc/data/messages/m/missing-param-type-annotation/pylintrc b/doc/data/messages/m/missing-param-type-annotation/pylintrc new file mode 100644 index 0000000000..229b3932e3 --- /dev/null +++ b/doc/data/messages/m/missing-param-type-annotation/pylintrc @@ -0,0 +1,6 @@ +[MAIN] +load-plugins = pylint.extensions.type_annotations + +[MESSAGES CONTROL] +disable = missing-return-type-annotation +enable = missing-param-type-annotation diff --git a/doc/data/messages/m/missing-return-type-annotation/bad.py b/doc/data/messages/m/missing-return-type-annotation/bad.py new file mode 100644 index 0000000000..f7fb9e789e --- /dev/null +++ b/doc/data/messages/m/missing-return-type-annotation/bad.py @@ -0,0 +1,6 @@ +def calculate_sum(numbers: list[int]): # [missing-return-type-annotation] + return sum(numbers) + + +async def fetch_data(url: str): # [missing-return-type-annotation] + return await get(url) diff --git a/doc/data/messages/m/missing-return-type-annotation/details.rst b/doc/data/messages/m/missing-return-type-annotation/details.rst new file mode 100644 index 0000000000..a6124b11a0 --- /dev/null +++ b/doc/data/messages/m/missing-return-type-annotation/details.rst @@ -0,0 +1,13 @@ +Type annotations improve code readability and enable better static analysis. This check +ensures that all functions and methods have return type annotations, making the code's +intent clearer and allowing type checkers like mypy to verify correctness. + +This check is opt-in (disabled by default) to maintain backward compatibility. Enable it +with ``--enable=missing-return-type-annotation``. + +The check automatically skips: + +- ``__init__`` methods (which implicitly return None) +- Abstract methods (``@abstractmethod``, ``@abstractproperty``) +- Properties and their setters/deleters +- Overload stub definitions (``@typing.overload``) diff --git a/doc/data/messages/m/missing-return-type-annotation/good.py b/doc/data/messages/m/missing-return-type-annotation/good.py new file mode 100644 index 0000000000..740c8e7e58 --- /dev/null +++ b/doc/data/messages/m/missing-return-type-annotation/good.py @@ -0,0 +1,11 @@ +def calculate_sum(numbers: list[int]) -> int: + return sum(numbers) + + +async def fetch_data(url: str) -> dict: + return await get(url) + + +class Calculator: + def __init__(self, initial: int): # __init__ doesn't need return type + self.value = initial diff --git a/doc/data/messages/m/missing-return-type-annotation/pylintrc b/doc/data/messages/m/missing-return-type-annotation/pylintrc new file mode 100644 index 0000000000..9e8a126276 --- /dev/null +++ b/doc/data/messages/m/missing-return-type-annotation/pylintrc @@ -0,0 +1,6 @@ +[MAIN] +load-plugins = pylint.extensions.type_annotations + +[MESSAGES CONTROL] +disable = missing-param-type-annotation +enable = missing-return-type-annotation diff --git a/doc/whatsnew/fragments/3853.new_check b/doc/whatsnew/fragments/3853.new_check new file mode 100644 index 0000000000..5d5bfab207 --- /dev/null +++ b/doc/whatsnew/fragments/3853.new_check @@ -0,0 +1,5 @@ +Add ``missing-return-type-annotation`` and ``missing-param-type-annotation`` checks to enforce type annotation presence in functions and methods. + +These new convention-level checks help teams enforce type annotation standards. Both checks are opt-in (disabled by default) and can be enabled independently for granular control. The checks intelligently skip ``self``/``cls`` parameters, ``__init__`` methods (return type only), and methods decorated with ``@abstractmethod``, ``@property``, or ``@typing.overload``. + +Closes #3853 diff --git a/pylint/extensions/type_annotations.py b/pylint/extensions/type_annotations.py new file mode 100644 index 0000000000..a76d351734 --- /dev/null +++ b/pylint/extensions/type_annotations.py @@ -0,0 +1,169 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Checker for type annotations in function definitions.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint import checkers +from pylint.checkers import utils + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class TypeAnnotationChecker(checkers.BaseChecker): + """Checker for enforcing type annotations on functions and methods. + + This checker verifies that functions and methods have appropriate + type annotations for return values and parameters. + """ + + name = "type-annotation" + msgs = { + "C3801": ( + "Missing return type annotation for function %r", + "missing-return-type-annotation", + "Used when a function or method does not have a return type annotation. " + "Type annotations improve code readability and help with static type checking.", + ), + "C3802": ( + "Missing type annotation for parameter %r in function %r", + "missing-param-type-annotation", + "Used when a function or method parameter does not have a type annotation. " + "Type annotations improve code readability and help with static type checking.", + ), + } + + @utils.only_required_for_messages( + "missing-return-type-annotation", "missing-param-type-annotation" + ) + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + """Check for missing type annotations in regular functions.""" + self._check_return_type_annotation(node) + self._check_param_type_annotations(node) + + visit_asyncfunctiondef = visit_functiondef + + def _check_return_type_annotation( + self, node: nodes.FunctionDef | nodes.AsyncFunctionDef + ) -> None: + """Check if a function has a return type annotation. + + Args: + node: The function definition node to check + """ + if node.returns is not None: + return + + if node.type_comment_returns: + return + + if node.name == "__init__": + return + + if utils.decorated_with(node, ["abc.abstractmethod", "abc.abstractproperty"]): + return + + if utils.decorated_with( + node, ["typing.overload", "typing_extensions.overload"] + ): + return + + if utils.decorated_with(node, ["property", "builtins.property"]): + return + + if utils.is_property_setter_or_deleter(node): + return + + self.add_message("missing-return-type-annotation", node=node, args=(node.name,)) + + def _check_param_type_annotations( + self, node: nodes.FunctionDef | nodes.AsyncFunctionDef + ) -> None: + """Check if function parameters have type annotations. + + Args: + node: The function definition node to check + """ + if utils.decorated_with(node, ["abc.abstractmethod", "abc.abstractproperty"]): + return + + if utils.decorated_with( + node, ["typing.overload", "typing_extensions.overload"] + ): + return + + if utils.is_property_setter_or_deleter(node): + return + + arguments = node.args + + # Check positional-only args + if arguments.posonlyargs: + annotations = arguments.posonlyargs_annotations or [] + for idx, arg in enumerate(arguments.posonlyargs): + if arg.name in {"self", "cls"}: + continue + if idx >= len(annotations) or annotations[idx] is None: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arg.name, node.name), + ) + + # Check regular args (skip self/cls for methods) + if arguments.args: + annotations = arguments.annotations or [] + start_idx = 0 + if ( + arguments.args + and arguments.args[0].name in {"self", "cls"} + and isinstance(node.parent, nodes.ClassDef) + ): + start_idx = 1 + + for idx, arg in enumerate(arguments.args[start_idx:], start=start_idx): + if idx >= len(annotations) or annotations[idx] is None: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arg.name, node.name), + ) + + # Check *args + if arguments.vararg and not arguments.varargannotation: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arguments.vararg, node.name), + ) + + # Check keyword-only args + if arguments.kwonlyargs: + annotations = arguments.kwonlyargs_annotations or [] + for idx, arg in enumerate(arguments.kwonlyargs): + if idx >= len(annotations) or annotations[idx] is None: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arg.name, node.name), + ) + + # Check **kwargs + if arguments.kwarg and not arguments.kwargannotation: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arguments.kwarg, node.name), + ) + + +def register(linter: PyLinter) -> None: + """Register the checker with the linter.""" + linter.register_checker(TypeAnnotationChecker(linter)) diff --git a/tests/extensions/test_type_annotations.py b/tests/extensions/test_type_annotations.py new file mode 100644 index 0000000000..375b6e05b2 --- /dev/null +++ b/tests/extensions/test_type_annotations.py @@ -0,0 +1,528 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Tests for the type_annotations extension.""" + +from __future__ import annotations + +import astroid + +from pylint.extensions.type_annotations import TypeAnnotationChecker +from pylint.testutils import CheckerTestCase, MessageTest + + +class TestTypeAnnotationChecker( + CheckerTestCase +): # pylint: disable=too-many-public-methods + """Tests for TypeAnnotationChecker.""" + + CHECKER_CLASS = TypeAnnotationChecker + + def test_missing_return_type_annotation(self) -> None: + """Test detection of missing return type annotation.""" + node = astroid.extract_node( + """ + def foo(x): #@ + return x + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-return-type-annotation", + args=("foo",), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ), + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ), + ): + self.checker.visit_functiondef(node) + + def test_function_with_return_type_annotation(self) -> None: + """Test that functions with return type annotations don't trigger warnings.""" + node = astroid.extract_node( + """ + def foo(x: int) -> int: #@ + return x + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_init_method_skipped(self) -> None: + """Test that __init__ methods are skipped for return type.""" + node = astroid.extract_node( + """ + class MyClass: + def __init__(self, x): #@ + self.x = x + """ + ) + # __init__ should skip return type check, but still check params + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "__init__"), + node=node, + line=3, + col_offset=4, + end_line=3, + end_col_offset=16, + ) + ): + self.checker.visit_functiondef(node) + + def test_async_function_missing_return_type(self) -> None: + """Test detection in async functions.""" + node = astroid.extract_node( + """ + async def foo(x): #@ + return x + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-return-type-annotation", + args=("foo",), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=13, + ), + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=13, + ), + ): + self.checker.visit_asyncfunctiondef(node) + + def test_missing_param_type_annotation(self) -> None: + """Test detection of missing parameter type annotation.""" + node = astroid.extract_node( + """ + def foo(x) -> int: #@ + return x + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_function_with_all_annotations(self) -> None: + """Test that fully annotated functions don't trigger warnings.""" + node = astroid.extract_node( + """ + def foo(x: int, y: str) -> bool: #@ + return True + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_method_self_parameter_skipped(self) -> None: + """Test that 'self' parameter is skipped in methods.""" + node = astroid.extract_node( + """ + class MyClass: + def foo(self, x: int) -> int: #@ + return x + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_classmethod_cls_parameter_skipped(self) -> None: + """Test that 'cls' parameter is skipped in class methods.""" + node = astroid.extract_node( + """ + class MyClass: + @classmethod + def foo(cls, x: int) -> int: #@ + return x + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_abstract_method_skipped(self) -> None: + """Test that abstract methods are skipped.""" + node = astroid.extract_node( + """ + from abc import abstractmethod + + class MyClass: + @abstractmethod + def foo(self, x): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_property_skipped(self) -> None: + """Test that property methods are skipped.""" + node = astroid.extract_node( + """ + class MyClass: + @property + def foo(self): #@ + return 42 + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_vararg_missing_annotation(self) -> None: + """Test detection of missing *args annotation.""" + node = astroid.extract_node( + """ + def foo(*args) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("args", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_kwarg_missing_annotation(self) -> None: + """Test detection of missing **kwargs annotation.""" + node = astroid.extract_node( + """ + def foo(**kwargs) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("kwargs", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_fully_annotated_with_varargs(self) -> None: + """Test that fully annotated functions with *args and **kwargs work.""" + node = astroid.extract_node( + """ + def foo(x: int, *args: str, **kwargs: bool) -> None: #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_keyword_only_args_missing_annotation(self) -> None: + """Test detection of missing keyword-only argument annotations.""" + node = astroid.extract_node( + """ + def foo(x: int, *, y) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("y", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_type_comment_returns_skipped(self) -> None: + """Test that functions with type comment returns are skipped.""" + node = astroid.extract_node( + """ + def foo(x): #@ + # type: (int) -> int + return x + """ + ) + # Should only check params, not return type + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_abstract_property_skipped(self) -> None: + """Test that abstract properties are skipped.""" + node = astroid.extract_node( + """ + from abc import abstractproperty + + class MyClass: + @abstractproperty + def foo(self): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_typing_overload_skipped(self) -> None: + """Test that typing.overload decorated functions are skipped.""" + node = astroid.extract_node( + """ + from typing import overload + + @overload + def foo(x): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_typing_extensions_overload_skipped(self) -> None: + """Test that typing_extensions.overload decorated functions are skipped.""" + node = astroid.extract_node( + """ + from typing_extensions import overload + + @overload + def foo(x): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_property_setter_skipped(self) -> None: + """Test that property setters are skipped.""" + node = astroid.extract_node( + """ + class MyClass: + @property + def foo(self) -> int: + return 42 + + @foo.setter + def foo(self, value): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_property_deleter_skipped(self) -> None: + """Test that property deleter methods are skipped.""" + node = astroid.extract_node( + """ + class MyClass: + @property + def foo(self) -> int: + return 42 + + @foo.deleter + def foo(self): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_builtins_property_skipped(self) -> None: + """Test that builtins.property decorated functions are skipped.""" + node = astroid.extract_node( + """ + import builtins + + class MyClass: + @builtins.property + def foo(self): #@ + return 42 + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_positional_only_args_missing_annotation(self) -> None: + """Test detection of missing positional-only argument annotations.""" + node = astroid.extract_node( + """ + def foo(x, y, /) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ), + MessageTest( + msg_id="missing-param-type-annotation", + args=("y", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ), + ): + self.checker.visit_functiondef(node) + + def test_positional_only_args_with_self_skipped(self) -> None: + """Test that self is skipped in positional-only args.""" + node = astroid.extract_node( + """ + class MyClass: + def foo(self, x, /) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=3, + col_offset=4, + end_line=3, + end_col_offset=11, + ) + ): + self.checker.visit_functiondef(node) + + def test_positional_only_args_fully_annotated(self) -> None: + """Test that fully annotated positional-only args don't trigger warnings.""" + node = astroid.extract_node( + """ + def foo(x: int, y: str, /) -> None: #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_positional_only_args_with_cls_skipped(self) -> None: + """Test that cls is skipped in positional-only args for class methods.""" + node = astroid.extract_node( + """ + class MyClass: + @classmethod + def foo(cls, x, /) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=4, + col_offset=4, + end_line=4, + end_col_offset=11, + ) + ): + self.checker.visit_functiondef(node) + + def test_method_with_second_arg_missing_annotation(self) -> None: + """Test that only self/cls is skipped, not subsequent args.""" + node = astroid.extract_node( + """ + class MyClass: + def foo(self, x, y: int) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=3, + col_offset=4, + end_line=3, + end_col_offset=11, + ) + ): + self.checker.visit_functiondef(node) + + def test_mixed_positional_and_regular_args(self) -> None: + """Test functions with both positional-only and regular args.""" + node = astroid.extract_node( + """ + def foo(x: int, /, y, z: str) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("y", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_keyword_only_args_fully_annotated(self) -> None: + """Test that fully annotated keyword-only args don't trigger warnings.""" + node = astroid.extract_node( + """ + def foo(*, x: int, y: str) -> None: #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) diff --git a/tests/message/conftest.py b/tests/message/conftest.py index 57567f4385..b56dade6b9 100644 --- a/tests/message/conftest.py +++ b/tests/message/conftest.py @@ -16,12 +16,12 @@ @pytest.fixture -def msgid(): +def msgid() -> str: return "W1234" @pytest.fixture -def symbol(): +def symbol() -> str: return "msg-symbol" diff --git a/tests/regrtest_data/type_annotations.py b/tests/regrtest_data/type_annotations.py new file mode 100644 index 0000000000..1197541cd3 --- /dev/null +++ b/tests/regrtest_data/type_annotations.py @@ -0,0 +1,51 @@ +"""Test file for type annotation checker.""" + + +def missing_return_type(x: int, y: int): # Missing return type + """Function missing return type annotation.""" + return x + y + + +def missing_param_types(x, y) -> int: # Missing parameter types + """Function missing parameter type annotations.""" + return x + y + + +def missing_all_annotations(x, y): # Missing both + """Function missing all type annotations.""" + return x + y + + +def fully_annotated(x: int, y: int) -> int: # OK - fully annotated + """Function with complete type annotations.""" + return x + y + + +class TestClass: + """Test class for type annotations.""" + + def __init__(self, value: int): # OK - __init__ doesn't need return type + """Initialize with value.""" + self.value = value + + def get_value(self): # Missing return type + """Get the value.""" + return self.value + + def set_value(self, value): # Missing parameter type and return type + """Set the value.""" + self.value = value + + def compute(self, x: int) -> int: # OK - fully annotated + """Compute something.""" + return self.value + x + + +async def async_missing_return(x: int): # Missing return type + """Async function missing return type.""" + return x * 2 + + +async def async_fully_annotated(x: int) -> int: # OK - fully annotated + """Async function with complete annotations.""" + return x * 2 diff --git a/tests/test_self.py b/tests/test_self.py index 99b50fcfd3..674a3a0f4f 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -270,7 +270,10 @@ def test_no_out_encoding(self) -> None: strio = StringIO() assert strio.encoding is None self._runtest( - [join(HERE, "regrtest_data", "no_stdout_encoding.py"), "--enable=all"], + [ + join(HERE, "regrtest_data", "no_stdout_encoding.py"), + "--enable=all", + ], out=strio, code=28, ) @@ -311,7 +314,13 @@ def test_enable_all_works(self) -> None: """ ) self._test_output( - [module, "--disable=I", "--enable=all", "-rn"], expected_output=expected + [ + module, + "--disable=I", + "--enable=all", + "-rn", + ], + expected_output=expected, ) def test_wrong_import_position_when_others_disabled(self) -> None: @@ -1156,7 +1165,13 @@ def test_one_module_fatal_error(self) -> None: ) def test_fail_on_info_only_exit_code(self, args: list[str], expected: int) -> None: path = join(HERE, "regrtest_data", "fail_on_info_only.py") - self._runtest([path, *args], code=expected) + self._runtest( + [ + path, + *args, + ], + code=expected, + ) @pytest.mark.parametrize( "output_format, expected_output", @@ -1421,7 +1436,13 @@ def test_line_too_long_useless_suppression(self) -> None: """ ) - self._test_output([module, "--enable=all"], expected_output=expected) + self._test_output( + [ + module, + "--enable=all", + ], + expected_output=expected, + ) def test_output_no_header(self) -> None: module = join(HERE, "data", "clientmodule_test.py") @@ -1445,6 +1466,55 @@ def test_no_name_in_module(self) -> None: [module, "-E"], expected_output="", unexpected_output=unexpected ) + def test_type_annotation_checker(self) -> None: + """Test that the type annotation checker works correctly when enabled.""" + module = join(HERE, "regrtest_data", "type_annotations.py") + expected = textwrap.dedent( + f""" + ************* Module type_annotations + {module}:4:0: C3801: Missing return type annotation for function """ + f"""'missing_return_type' (missing-return-type-annotation) + {module}:9:0: C3802: Missing type annotation for parameter 'x' in """ + f"""function 'missing_param_types' (missing-param-type-annotation) + {module}:9:0: C3802: Missing type annotation for parameter 'y' in """ + f"""function 'missing_param_types' (missing-param-type-annotation) + {module}:14:0: C3801: Missing return type annotation for function """ + f"""'missing_all_annotations' (missing-return-type-annotation) + {module}:14:0: C3802: Missing type annotation for parameter 'x' in """ + f"""function 'missing_all_annotations' (missing-param-type-annotation) + {module}:14:0: C3802: Missing type annotation for parameter 'y' in """ + f"""function 'missing_all_annotations' (missing-param-type-annotation) + {module}:31:4: C3801: Missing return type annotation for function """ + f"""'get_value' (missing-return-type-annotation) + {module}:35:4: C3801: Missing return type annotation for function """ + f"""'set_value' (missing-return-type-annotation) + {module}:35:4: C3802: Missing type annotation for parameter 'value' """ + f"""in function 'set_value' (missing-param-type-annotation) + {module}:44:0: C3801: Missing return type annotation for function """ + f"""'async_missing_return' (missing-return-type-annotation) + """ + ) + # Test with the extension loaded and checker explicitly enabled + self._test_output( + [ + module, + "--load-plugins=pylint.extensions.type_annotations", + "--enable=missing-return-type-annotation,missing-param-type-annotation", + "-rn", + ], + expected_output=expected, + ) + + def test_type_annotation_checker_disabled_by_default(self) -> None: + """Test that the type annotation checker is disabled by default.""" + module = join(HERE, "regrtest_data", "type_annotations.py") + # Without explicitly enabling the checker, no type annotation messages should appear + out = StringIO() + self._runtest([module], out=out, code=0) + output = out.getvalue() + assert "missing-return-type-annotation" not in output + assert "missing-param-type-annotation" not in output + class TestCallbackOptions: """Test for all callback options we support."""