From eaf536b52e0a0694d1e839486f3603669292c16c Mon Sep 17 00:00:00 2001 From: jbouwh Date: Tue, 20 May 2025 05:48:22 +0000 Subject: [PATCH] Add linter to check correct microchar use --- .../plugins/hass_enforce_greek_micro_char.py | 48 ++++++++ pyproject.toml | 1 + tests/pylint/conftest.py | 21 ++++ tests/pylint/test_enforce_greek_micro_char.py | 114 ++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 pylint/plugins/hass_enforce_greek_micro_char.py create mode 100644 tests/pylint/test_enforce_greek_micro_char.py diff --git a/pylint/plugins/hass_enforce_greek_micro_char.py b/pylint/plugins/hass_enforce_greek_micro_char.py new file mode 100644 index 00000000000..0fb4d897b17 --- /dev/null +++ b/pylint/plugins/hass_enforce_greek_micro_char.py @@ -0,0 +1,48 @@ +"""Plugin for checking correct micro unicode card is used.""" + +from __future__ import annotations + +from astroid import nodes +from pylint.checkers import BaseChecker +from pylint.lint import PyLinter + + +class HassEnforceGreekMicroCharChecker(BaseChecker): + """Checker for micro char.""" + + name = "hass-enforce-greek-micro-char" + priority = -1 + msgs = { + "W7452": ( + "Constants with the mico sign must be encoded as U+03BC (\u03bc), not as U+00B5 (\u00b5)", + "hass-enforce-greek-micro-char", + "According to [The Unicode Consortium](https://en.wikipedia.org/wiki/Unicode_Consortium), the Greek letter character is preferred [10].", + ), + } + options = () + + def visit_annassign(self, node: nodes.AnnAssign) -> None: + """Check for sorted PLATFORMS const with type annotations.""" + self._do_micro_check(node.target, node) + + def visit_assign(self, node: nodes.Assign) -> None: + """Check for sorted PLATFORMS const without type annotations.""" + for target in node.targets: + self._do_micro_check(target, node) + + def _do_micro_check( + self, target: nodes.NodeNG, node: nodes.Assign | nodes.AnnAssign + ) -> None: + """Check const assignment is not containing ANSI micro char.""" + if ( + isinstance(target, nodes.AssignName) + and isinstance(node.value, nodes.Const) + and isinstance(node.value.value, str) + and "\u00b5" in node.value.value + ): + self.add_message(self.name, node=node) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassEnforceGreekMicroCharChecker(linter)) diff --git a/pyproject.toml b/pyproject.toml index 25f4d6d4a1a..ed907549f88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,7 @@ load-plugins = [ "hass_async_load_fixtures", "hass_decorator", "hass_enforce_class_module", + "hass_enforce_greek_micro_char", "hass_enforce_sorted_platforms", "hass_enforce_super_call", "hass_enforce_type_hints", diff --git a/tests/pylint/conftest.py b/tests/pylint/conftest.py index 8ae291ac0b7..4ffbca6124a 100644 --- a/tests/pylint/conftest.py +++ b/tests/pylint/conftest.py @@ -138,3 +138,24 @@ def decorator_checker_fixture(hass_decorator, linter) -> BaseChecker: type_hint_checker = hass_decorator.HassDecoratorChecker(linter) type_hint_checker.module = "homeassistant.components.pylint_test" return type_hint_checker + + +@pytest.fixture(name="hass_enforce_greek_micro_char", scope="package") +def hass_enforce_greek_micro_checker_fixture() -> ModuleType: + """Fixture to the content for the hass_enforce_greek_micro_char check.""" + return _load_plugin_from_file( + "hass_enforce_greek_micro_char", + "pylint/plugins/hass_enforce_greek_micro_char.py", + ) + + +@pytest.fixture(name="enforce_greek_micro_char_checker") +def enforce_greek_micro_char_checker_fixture( + hass_enforce_greek_micro_char, linter +) -> BaseChecker: + """Fixture to provide a hass_enforce_greek_micro_char checker.""" + enforce_greek_micro_char_checker = ( + hass_enforce_greek_micro_char.HassEnforceGreekMicroCharChecker(linter) + ) + enforce_greek_micro_char_checker.module = "homeassistant.components.pylint_test" + return enforce_greek_micro_char_checker diff --git a/tests/pylint/test_enforce_greek_micro_char.py b/tests/pylint/test_enforce_greek_micro_char.py new file mode 100644 index 00000000000..22741542c5d --- /dev/null +++ b/tests/pylint/test_enforce_greek_micro_char.py @@ -0,0 +1,114 @@ +"""Tests for pylint hass_enforce_greek_micro_char plugin.""" + +from __future__ import annotations + +import astroid +from pylint.checkers import BaseChecker +from pylint.testutils.unittest_linter import UnittestLinter +from pylint.utils.ast_walker import ASTWalker +import pytest + +from . import assert_no_messages + + +@pytest.mark.parametrize( + "code", + [ + pytest.param( + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "μg/m³" # "μ" == "\u03bc" + """, + id="good_const_with_annotation", + ), + pytest.param( + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "\u03bcg/m³" # "μ" == "\u03bc" + """, + id="good_unicode_const_with_annotation", + ), + pytest.param( + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "μg/m³" # "μ" == "\u03bc" + """, + id="good_const_without_annotation", + ), + pytest.param( + """ + class UnitOfElectricPotential(StrEnum): + \"\"\"Electric potential units.\"\"\" + + MICROVOLT = "μV" # "μ" == "\u03bc" + MILLIVOLT = "mV" + VOLT = "V" + KILOVOLT = "kV" + MEGAVOLT = "MV" + """, + id="good_str_enum", + ), + ], +) +def test_enforce_greek_micro_char( + linter: UnittestLinter, + enforce_greek_micro_char_checker: BaseChecker, + code: str, +) -> None: + """Good test cases.""" + root_node = astroid.parse(code, "homeassistant.components.pylint_test") + walker = ASTWalker(linter) + walker.add_checker(enforce_greek_micro_char_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +@pytest.mark.parametrize( + "code", + [ + pytest.param( + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" # "μ" != "\u03bc" + """, + id="bad_const_with_annotation", + ), + pytest.param( + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "\u00b5g/m³" # "μ" != "\u03bc" + """, + id="bad_unicode_const_with_annotation", + ), + pytest.param( + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "µg/m³" # "μ" != "\u03bc" + """, + id="bad_const_without_annotation", + ), + pytest.param( + """ + class UnitOfElectricPotential(StrEnum): + \"\"\"Electric potential units.\"\"\" + + MICROVOLT = "µV" # "μ" != "\u03bc" + MILLIVOLT = "mV" + VOLT = "V" + KILOVOLT = "kV" + MEGAVOLT = "MV" + """, + id="bad_str_enum", + ), + ], +) +def test_enforce_greek_micro_char_assign_bad( + linter: UnittestLinter, + enforce_greek_micro_char_checker: BaseChecker, + code: str, +) -> None: + """Bad assignment test cases.""" + root_node = astroid.parse(code, "homeassistant.components.pylint_test") + walker = ASTWalker(linter) + walker.add_checker(enforce_greek_micro_char_checker) + + walker.walk(root_node) + messages = linter.release_messages() + assert len(messages) == 1 + message = next(iter(messages)) + assert message.msg_id == "hass-enforce-greek-micro-char"