diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index fc7be8ba8e8..62dc6feffc6 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -20,8 +20,8 @@ class TypeHintMatch: """Class for pattern matching.""" function_name: str - arg_types: dict[int, str] return_type: list[str] | str | None | object + arg_types: dict[int, str] | None = None check_return_type_inheritance: bool = False @@ -440,7 +440,42 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], ), - ] + ], +} +# Properties are normally checked by mypy, and will only be checked +# by pylint when --ignore-missing-annotations is False +_PROPERTY_MATCH: dict[str, list[ClassTypeHintMatch]] = { + "lock": [ + ClassTypeHintMatch( + base_class="LockEntity", + matches=[ + TypeHintMatch( + function_name="changed_by", + return_type=["str", None], + ), + TypeHintMatch( + function_name="code_format", + return_type=["str", None], + ), + TypeHintMatch( + function_name="is_locked", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="is_locking", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="is_unlocking", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="is_jammed", + return_type=["bool", None], + ), + ], + ), + ], } @@ -621,7 +656,12 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self._function_matchers.extend(function_matches) if class_matches := _CLASS_MATCH.get(module_platform): - self._class_matchers = class_matches + self._class_matchers.extend(class_matches) + + if not self.linter.config.ignore_missing_annotations and ( + property_matches := _PROPERTY_MATCH.get(module_platform) + ): + self._class_matchers.extend(property_matches) def visit_classdef(self, node: nodes.ClassDef) -> None: """Called when a ClassDef node is visited.""" @@ -659,14 +699,15 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ): return - # Check that all arguments are correctly annotated. - for key, expected_type in match.arg_types.items(): - if not _is_valid_type(expected_type, annotations[key]): - self.add_message( - "hass-argument-type", - node=node.args.args[key], - args=(key + 1, expected_type), - ) + # Check that all positional arguments are correctly annotated. + if match.arg_types: + for key, expected_type in match.arg_types.items(): + if not _is_valid_type(expected_type, annotations[key]): + self.add_message( + "hass-argument-type", + node=node.args.args[key], + args=(key + 1, expected_type), + ) # Check the return type. if not _is_valid_return_type(match, node.returns): diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index c07014add7f..1f601a881a6 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -469,3 +469,69 @@ def test_valid_config_flow_async_get_options_flow( with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_invalid_entity_properties( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check missing entity properties when ignore_missing_annotations is False.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, prop_node = astroid.extract_node( + """ + class LockEntity(): + pass + + class DoorLock( #@ + LockEntity + ): + @property + def changed_by( #@ + self + ): + pass + """, + "homeassistant.components.pylint_test.lock", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=prop_node, + args=["str", None], + line=9, + col_offset=4, + end_line=9, + end_col_offset=18, + ), + ): + type_hint_checker.visit_classdef(class_node) + + +def test_ignore_invalid_entity_properties( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check invalid entity properties are ignored by default.""" + class_node = astroid.extract_node( + """ + class LockEntity(): + pass + + class DoorLock( #@ + LockEntity + ): + @property + def changed_by( + self + ): + pass + """, + "homeassistant.components.pylint_test.lock", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node)