Add linter to check correct microchar use

This commit is contained in:
jbouwh 2025-05-20 05:48:22 +00:00
parent 7b607a2c1b
commit eaf536b52e
4 changed files with 184 additions and 0 deletions

View File

@ -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))

View File

@ -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",

View File

@ -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

View File

@ -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"