diff --git a/pylint/plugins/README.md b/pylint/plugins/README.md index 25f080ab97bc..71f7da47d915 100644 --- a/pylint/plugins/README.md +++ b/pylint/plugins/README.md @@ -1042,3 +1042,13 @@ The light provides `color_mode` but no `supported_color_modes`. At runtime ("does not set supported color modes") from both `state_attributes` and `capability_attributes` whenever `supported_color_modes` is `None`. Set `_attr_supported_color_modes` or override the `supported_color_modes` property. + + +## `home_assistant_enforce_config_flow_no_connection_class` checker + +Detects config flow classes that set `CONNECTION_CLASS`. + +### `W7438`: `home-assistant-config-flow-connection-class` + +`CONNECTION_CLASS` is no longer used by Home Assistant and should not be set +on config flows. Remove the attribute. diff --git a/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_connection_class.py b/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_connection_class.py new file mode 100644 index 000000000000..c11881e7b7f9 --- /dev/null +++ b/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_connection_class.py @@ -0,0 +1,56 @@ +"""Checker for the removed CONNECTION_CLASS attribute in config flows.""" + +from astroid import nodes +from pylint.checkers import BaseChecker +from pylint.lint import PyLinter + +from pylint_home_assistant.const import Module +from pylint_home_assistant.helpers.module_info import parse_module + +_CONNECTION_CLASS = "CONNECTION_CLASS" + + +class HassEnforceConfigFlowNoConnectionClassChecker(BaseChecker): + """Checker for CONNECTION_CLASS in config flows.""" + + name = "home_assistant_enforce_config_flow_no_connection_class" + priority = -1 + msgs = { + "W7438": ( + "Config flow should not set CONNECTION_CLASS", + "home-assistant-config-flow-connection-class", + "Used when a config flow class sets CONNECTION_CLASS. The attribute " + "is no longer used by Home Assistant and should not be set.", + ), + } + options = () + + def visit_assign(self, node: nodes.Assign) -> None: + """Check class-level assignments.""" + if any( + isinstance(target, nodes.AssignName) and target.name == _CONNECTION_CLASS + for target in node.targets + ): + self._check(node) + + def visit_annassign(self, node: nodes.AnnAssign) -> None: + """Check annotated class-level assignments.""" + if ( + isinstance(node.target, nodes.AssignName) + and node.target.name == _CONNECTION_CLASS + ): + self._check(node) + + def _check(self, node: nodes.Assign | nodes.AnnAssign) -> None: + """Add a message if the assignment is in a config flow class body.""" + parsed = parse_module(node.root().name) + if parsed is None or parsed.module != Module.CONFIG_FLOW: + return + if not isinstance(node.parent, nodes.ClassDef): + return + self.add_message("home-assistant-config-flow-connection-class", node=node) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassEnforceConfigFlowNoConnectionClassChecker(linter)) diff --git a/tests/pylint/config_flow/test_no_connection_class.py b/tests/pylint/config_flow/test_no_connection_class.py new file mode 100644 index 000000000000..051df6abdf49 --- /dev/null +++ b/tests/pylint/config_flow/test_no_connection_class.py @@ -0,0 +1,86 @@ +"""Tests for the config flow no CONNECTION_CLASS checker.""" + +from __future__ import annotations + +import astroid +from pylint.checkers import BaseChecker +from pylint.testutils.unittest_linter import UnittestLinter +import pytest + +from tests.pylint import assert_no_messages, walk_checker + +CONFIG_FLOW_MODULE = "homeassistant.components.test.config_flow" + + +@pytest.mark.parametrize( + ("code", "module_name"), + [ + pytest.param( + """ + class TestFlow: + VERSION = 1 + """, + CONFIG_FLOW_MODULE, + id="no_connection_class", + ), + pytest.param( + """ + class TestFlow: + CONNECTION_CLASS = "local_polling" + """, + "homeassistant.components.test.sensor", + id="other_module", + ), + pytest.param( + """ + CONNECTION_CLASS = "local_polling" + """, + CONFIG_FLOW_MODULE, + id="module_level", + ), + ], +) +def test_no_connection_class( + linter: UnittestLinter, + enforce_config_flow_no_connection_class_checker: BaseChecker, + code: str, + module_name: str, +) -> None: + """Good test cases.""" + root_node = astroid.parse(code, module_name) + + with assert_no_messages(linter): + walk_checker(linter, enforce_config_flow_no_connection_class_checker, root_node) + + +@pytest.mark.parametrize( + "code", + [ + pytest.param( + """ + class TestFlow: + CONNECTION_CLASS = "local_polling" + """, + id="assign", + ), + pytest.param( + """ + class TestFlow: + CONNECTION_CLASS: str = "local_polling" + """, + id="annotated_assign", + ), + ], +) +def test_no_connection_class_bad( + linter: UnittestLinter, + enforce_config_flow_no_connection_class_checker: BaseChecker, + code: str, +) -> None: + """Bad test cases.""" + root_node = astroid.parse(code, CONFIG_FLOW_MODULE) + + walk_checker(linter, enforce_config_flow_no_connection_class_checker, root_node) + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-config-flow-connection-class" diff --git a/tests/pylint/conftest.py b/tests/pylint/conftest.py index 4b60bfbb8d73..10f21afdde76 100644 --- a/tests/pylint/conftest.py +++ b/tests/pylint/conftest.py @@ -3,6 +3,9 @@ from pylint.checkers import BaseChecker from pylint.testutils.unittest_linter import UnittestLinter from pylint_home_assistant.checkers.class_module import HassEnforceClassModule +from pylint_home_assistant.checkers.config_flow.no_connection_class import ( + HassEnforceConfigFlowNoConnectionClassChecker, +) from pylint_home_assistant.checkers.config_flow.no_name import ( HassEnforceConfigFlowNoNameChecker, ) @@ -109,6 +112,17 @@ def enforce_serial_port_selector_usb_checker_fixture( return checker +@pytest.fixture(name="enforce_config_flow_no_connection_class_checker") +def enforce_config_flow_no_connection_class_checker_fixture( + linter: UnittestLinter, +) -> BaseChecker: + """Fixture to provide a config_flow_no_connection_class checker.""" + clear_caches() + checker = HassEnforceConfigFlowNoConnectionClassChecker(linter) + checker.module = "homeassistant.components.pylint_test" + return checker + + @pytest.fixture(name="enforce_config_flow_no_name_checker") def enforce_config_flow_no_name_checker_fixture( linter: UnittestLinter,