Compare commits

..

3 Commits

Author SHA1 Message Date
Markus Tuominen 53211759cb Document missing pylint rules in plugin README (#172925) 2026-06-03 18:54:20 +03:00
Abílio Costa 4593059db2 Add "review" claude skill and use it in "gitbhub-pr-review" (#172797)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-03 15:57:21 +01:00
epenet 593ae9eb80 Add pylint plugin for correct use of DOMAIN constants in tests (#172693)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
2026-06-03 16:53:15 +02:00
7 changed files with 686 additions and 140 deletions
+2 -33
View File
@@ -8,39 +8,8 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
## Follow these steps:
1. Use 'gh pr view' to get the PR details and description.
2. Use 'gh pr diff' to see all the changes in the PR.
3. Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
4. Ensure any existing review comments have been addressed.
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
## Verification:
- After the review, run parallel subagents for each finding to double check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
3. Review the changes following the `review` skill. It is VERY IMPORTANT to follow the `review` skill instructions.
4. Check if all existing review comments have been addressed.
## IMPORTANT:
- Just review. DO NOT make any changes
- Be constructive and specific in your comments
- Suggest improvements where appropriate
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.
+38
View File
@@ -0,0 +1,38 @@
---
name: review
description: Reviews code changes and provides constructive feedback. Should be used when a review is requested to provide a consistent review behavior and output format. This skill can be used for code reviews in general, not just for GitHub pull requests.
---
# Review Code Changes
## Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
## Verification:
- After the review, run parallel subagents for each finding to double-check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
## IMPORTANT:
- Just review. DO NOT make any changes.
- Be constructive and specific in your comments.
- Suggest improvements where appropriate.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.
@@ -980,24 +980,7 @@ async def handle_subscribe_trigger(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle subscribe trigger command."""
# Validating the trigger config can fail on bad user input. Handle those
# errors here so they are reported to the client without being logged as
# unexpected errors by the default websocket error handler.
try:
trigger_config = await async_validate_trigger_config(hass, msg["trigger"])
except vol.Invalid as err:
connection.send_error(msg["id"], const.ERR_INVALID_FORMAT, str(err))
return
except HomeAssistantError as err:
connection.send_error(
msg["id"],
const.ERR_HOME_ASSISTANT_ERROR,
str(err),
translation_domain=err.translation_domain,
translation_key=err.translation_key,
translation_placeholders=err.translation_placeholders,
)
return
trigger_config = await async_validate_trigger_config(hass, msg["trigger"])
@callback
def forward_triggers(
+242
View File
@@ -99,11 +99,31 @@ Every check has a code following the
| `W7407` | [`home-assistant-config-flow-polling-field`](#w7407-home-assistant-config-flow-polling-field) | Config flow should not include polling interval fields |
| `W7408` | [`home-assistant-config-flow-name-field`](#w7408-home-assistant-config-flow-name-field) | Config flow should not include name fields |
| `R7402` | [`home-assistant-unused-test-fixture-argument`](#r7402-home-assistant-unused-test-fixture-argument) | Unused test function argument should use `@pytest.mark.usefixtures` |
| `C7415` | [`home-assistant-domain-argument`](#c7415-home-assistant-domain-argument) | Domain argument in tests should be a domain constant or variable |
| `W7418` | [`home-assistant-tests-direct-async-setup-entry`](#w7418-home-assistant-tests-direct-async-setup-entry) | Tests should not call an integration's `async_setup_entry` directly |
| `W7420` | [`home-assistant-tests-direct-platform-async-setup-entry`](#w7420-home-assistant-tests-direct-platform-async-setup-entry) | Tests should not call a platform's `async_setup_entry` directly |
| `W7421` | [`home-assistant-tests-direct-async-migrate-entry`](#w7421-home-assistant-tests-direct-async-migrate-entry) | Tests should not call an integration's `async_migrate_entry` directly |
| `W7422` | [`home-assistant-tests-direct-async-setup`](#w7422-home-assistant-tests-direct-async-setup) | Tests should not call an integration's `async_setup` directly |
| `C7414` | [`home-assistant-enforce-utcnow`](#c7414-home-assistant-enforce-utcnow) | Use `homeassistant.util.dt.utcnow` instead of `datetime.now(UTC)` |
| `C7412` | [`home-assistant-entity-description-redundant-default`](#c7412-home-assistant-entity-description-redundant-default) | Setting an EntityDescription field to its default value is redundant |
| `C7413` | [`home-assistant-duplicate-const`](#c7413-home-assistant-duplicate-const) | Constant duplicates one in `homeassistant.const` with the same value |
| `E7405` | [`home-assistant-action-swallowed-exception`](#e7405-home-assistant-action-swallowed-exception) | Action handler must not swallow exceptions |
| `W7414` | [`home-assistant-service-registered-in-setup-entry`](#w7414-home-assistant-service-registered-in-setup-entry) | Services should be registered in `async_setup`, not `async_setup_entry` |
| `W7417` | [`home-assistant-exception-not-translated`](#w7417-home-assistant-exception-not-translated) | `HomeAssistantError` should use `translation_key`/`translation_domain` |
| `W7419` | [`home-assistant-exception-message-with-translation`](#w7419-home-assistant-exception-message-with-translation) | Don't pass a positional message when `translation_key` is set |
| `E7406` | [`home-assistant-exception-translation-key-missing`](#e7406-home-assistant-exception-translation-key-missing) | Translation key not found in `strings.json` exceptions section |
| `E7408` | [`home-assistant-exception-translation-key-domain-mismatch`](#e7408-home-assistant-exception-translation-key-domain-mismatch) | Only one of `translation_key` / `translation_domain` is set |
| `E7418` | [`home-assistant-exception-placeholder-mismatch`](#e7418-home-assistant-exception-placeholder-mismatch) | Translation placeholders in code don't match `strings.json` |
| `E7409` | [`home-assistant-mdi-icon-not-found`](#e7409-home-assistant-mdi-icon-not-found) | MDI icon string does not exist in the Material Design Icons set |
| `E7410` | [`home-assistant-mdi-icon-json-not-found`](#e7410-home-assistant-mdi-icon-json-not-found) | MDI icon in `icons.json` does not exist in the Material Design Icons set |
| `R7403` | [`home-assistant-tests-redundant-usefixtures`](#r7403-home-assistant-tests-redundant-usefixtures) | `@pytest.mark.usefixtures` redundant when `pytestmark` already applies it |
| `W7409` | [`home-assistant-test-non-deterministic`](#w7409-home-assistant-test-non-deterministic) | Test contains `if`/`match` creating non-deterministic execution |
| `W7410` | [`home-assistant-missing-reauthentication-flow`](#w7410-home-assistant-missing-reauthentication-flow) | Config flow should implement `async_step_reauth` |
| `W7411` | [`home-assistant-missing-parallel-updates`](#w7411-home-assistant-missing-parallel-updates) | Platform module should define `PARALLEL_UPDATES` |
| `W7412` | [`home-assistant-missing-diagnostics`](#w7412-home-assistant-missing-diagnostics) | Integration diagnostics module should implement a diagnostics function |
| `W7413` | [`home-assistant-missing-config-entry-unloading`](#w7413-home-assistant-missing-config-entry-unloading) | Integration should implement `async_unload_entry` |
| `W7415` | [`home-assistant-sequential-executor-jobs`](#w7415-home-assistant-sequential-executor-jobs) | Sequential `async_add_executor_job` calls should be grouped |
| `W7416` | [`home-assistant-missing-has-entity-name`](#w7416-home-assistant-missing-has-entity-name) | Entity class should set `_attr_has_entity_name = True` |
## `home_assistant_logger` checker
@@ -346,6 +366,27 @@ only needed for its side effects.
This rule only applies to `test_*` functions, not to fixture functions.
## `home_assistant_domain_constant` checker
Encourages using `DOMAIN` constants (or variables) when passing a domain to common test helpers.
String literals are allowed for cases where the constant is not imported.
Only runs on test modules.
### `C7415`: `home-assistant-domain-argument`
The domain (or handler) argument to test helpers such as
`async_setup_component`, `async_mock_service`, `MockConfigEntry`,
`hass.services.async_call`, `hass.services.call`, and
`hass.config_entries.flow.async_init` should use a domain constant or variable when available.
The following are accepted:
* a `DOMAIN`/`domain` attribute or one ending in `_DOMAIN`/`_domain`
(e.g. `sensor.DOMAIN`),
* a `DOMAIN`/`domain` name or one ending in `_DOMAIN`/`_domain`,
* a string literal (for cases where the constant is not imported),
* a subscript expression (e.g. `data["key"]`).
## `home_assistant_tests_direct_async_setup_entry` checker
Detects tests that call an integration's `async_setup_entry` directly.
@@ -413,3 +454,204 @@ The helper is implemented as
`functools.partial(datetime.datetime.now, UTC)` and avoids the global
lookup of `UTC` on every call, while keeping the codebase consistent in
how the current UTC time is obtained.
## `home_assistant_entity_description_defaults` checker
Detects fields in `EntityDescription` (and subclasses) that are explicitly
set to their default value.
### `C7412`: `home-assistant-entity-description-redundant-default`
An EntityDescription field is set equal to a default already declared
anywhere in the class hierarchy; the assignment can be removed. Only the
literal defaults `None`, `True`, and `False` are checked; other default
values are not flagged.
## `home_assistant_duplicate_const` checker
Detects constants in integration modules that duplicate one already exported
from `homeassistant.const` with the same value.
### `C7413`: `home-assistant-duplicate-const`
Import the constant from `homeassistant.const` instead of redefining it.
## `home_assistant_actions_swallowed_exceptions` checker
Detects action handlers that catch exceptions without re-raising. Swallowed
exceptions are not surfaced to the user.
### `E7405`: `home-assistant-action-swallowed-exception`
Action handlers must re-raise so the user is notified of the failure.
The checker detects empty `except` blocks, blocks that only log,
`contextlib.suppress(...)`, and equivalent patterns on decorators. It does
not validate the *type* of exception being raised; a separate rule covers
that.
## `home_assistant_actions_service_registration` checker
Detects services registered inside `async_setup_entry` rather than
`async_setup`.
### `W7414`: `home-assistant-service-registered-in-setup-entry`
Services should be registered in `async_setup` so they are available for
automation validation even when no config entry is loaded. Registrations
inside helper functions that are called from `async_setup_entry` are
caught too.
See the [action-setup quality scale rule](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/action-setup).
## `home_assistant_exception_translations` checker
Ensures `HomeAssistantError` and its subclasses use the translation system
(`translation_domain`, `translation_key`) instead of hardcoded English
strings. Also verifies that referenced translation keys exist in the
integration's `strings.json` and that placeholders match.
### `W7417`: `home-assistant-exception-not-translated`
A `HomeAssistantError` subclass is raised with a hardcoded message; use
`translation_domain` and `translation_key` instead. Quality-scale-gated.
### `W7419`: `home-assistant-exception-message-with-translation`
Don't pass a positional message argument when `translation_key` is also set;
the translation system supplies the message.
### `E7406`: `home-assistant-exception-translation-key-missing`
The translation key referenced from code is missing from `strings.json`
under the `exceptions` section.
### `E7408`: `home-assistant-exception-translation-key-domain-mismatch`
Both `translation_key` and `translation_domain` must be set together; only
one of the two was provided.
### `E7418`: `home-assistant-exception-placeholder-mismatch`
The placeholders passed in code (e.g. `translation_placeholders={...}`)
don't match the `{placeholder}` slots in the `strings.json` message.
## `home_assistant_mdi_icons` checker
Validates that `mdi:` icon references in code and `icons.json` refer to
icons that actually exist in the Material Design Icons set.
### `E7409`: `home-assistant-mdi-icon-not-found`
MDI icon reference in Python code does not exist in the Material Design
Icons set.
### `E7410`: `home-assistant-mdi-icon-json-not-found`
MDI icon reference in `icons.json` does not exist in the Material Design
Icons set.
## `home_assistant_tests_redundant_usefixtures` checker
Detects `@pytest.mark.usefixtures(...)` decorators that duplicate a fixture
already applied module-wide, either through a module-level `pytestmark`
or via `autouse=True` on a fixture defined in a parent `conftest.py`.
### `R7403`: `home-assistant-tests-redundant-usefixtures`
Drop the redundant `@pytest.mark.usefixtures` decorator; the fixture is
already applied to every test in the module.
## `home_assistant_test_determinism` checker
`if` and `match` statements inside test functions create non-deterministic
execution paths: some branches may never run, silently hiding failures.
### `W7409`: `home-assistant-test-non-deterministic`
Test function contains an `if` or `match` statement. Use
`@pytest.mark.parametrize` to cover cases explicitly, or split into separate
test functions. `if` statements have several exemptions: guard clauses
(`return`/`raise`/`pytest.skip`/`pytest.xfail`/`pytest.fail`),
conditions that reference a function parameter, and branches that contain
no `assert`. `match` statements have no exemptions.
## `home_assistant_reauthentication_flow` checker
Quality-scale-gated checker for the
[`reauthentication-flow`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/reauthentication-flow)
Silver rule. Fires only when the integration claims the rule as `done`.
### `W7410`: `home-assistant-missing-reauthentication-flow`
Integration's `config_flow.py` should implement `async_step_reauth`.
## `home_assistant_parallel_updates` checker
Quality-scale-gated checker for the
[`parallel-updates`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/parallel-updates)
Silver rule. Fires only when the integration claims the rule as `done`.
### `W7411`: `home-assistant-missing-parallel-updates`
Platform module should define a module-level `PARALLEL_UPDATES` constant.
## `home_assistant_diagnostics` checker
Quality-scale-gated checker for the
[`diagnostics`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/diagnostics)
Gold rule. Fires only when the integration claims the rule as `done`.
### `W7412`: `home-assistant-missing-diagnostics`
Integration's `diagnostics.py` should implement
`async_get_config_entry_diagnostics` or `async_get_device_diagnostics`.
## `home_assistant_config_entry_unloading` checker
Quality-scale-gated checker for the
[`config-entry-unloading`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/config-entry-unloading)
Silver rule. Fires only when the integration claims the rule as `done`.
### `W7413`: `home-assistant-missing-config-entry-unloading`
Integration's `__init__.py` should implement `async_unload_entry`.
## `home_assistant_sequential_executor_jobs` checker
Detects consecutive `async_add_executor_job` calls in integration modules
that could be grouped into a single executor job.
### `W7415`: `home-assistant-sequential-executor-jobs`
Two or more `async_add_executor_job` calls appearing as consecutive
statements (uninterrupted by control flow such as `if`/`try`/`with`/`for`)
should be combined into a single executor job that performs all the work,
avoiding unnecessary context switches back to the event loop between
blocking calls. The rule applies to integration modules only.
## `home_assistant_has_entity_name` checker
Quality-scale-gated checker for the
[`has-entity-name`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/has-entity-name)
Bronze rule. Fires only when the integration claims the rule as `done`.
### `W7416`: `home-assistant-missing-has-entity-name`
Entity class should statically guarantee `_attr_has_entity_name = True`:
either set at class level, set unconditionally at the top of a method, or
supplied by an `entity_description` whose class sets `has_entity_name = True`.
Conditional patterns are rejected.
@@ -0,0 +1,149 @@
"""Plugin to encourage correct use of DOMAIN constants in tests."""
from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.lint import PyLinter
from pylint_home_assistant.helpers.module_info import is_test_module
_FUNCTION_CHECKS: list[tuple[str, int | None, str]] = [
("async_setup_component", 1, "domain"),
("async_mock_service", 1, "domain"),
("MockConfigEntry", None, "domain"),
]
_METHOD_CHECKS: list[tuple[str, str, int | None, str]] = [
("hass.services", "async_call", 0, "domain"),
("hass.services", "call", 0, "domain"),
("hass.config_entries.flow", "async_init", 0, "handler"),
]
_DOMAIN_CONSTANTS: set[str] = {"DOMAIN", "domain"}
_DOMAIN_SUFFIXES: tuple[str, ...] = ("_DOMAIN", "_domain")
def _check_call_node_domain_arguments(node: nodes.Call) -> nodes.NodeNG | None:
"""Ensure the call node arguments are valid domain constant or variable.
Return None if the argument node is valid, or the argument node if it is invalid.
"""
match node.func:
case nodes.Attribute():
for (
method_source,
method_name,
arg_position,
kwarg_name,
) in _METHOD_CHECKS:
if (
node.func.attrname == method_name
and node.func.expr.as_string() == method_source
):
return _check_call_node_domain_argument(
node, arg_position=arg_position, kwarg_name=kwarg_name
)
case nodes.Name():
for func_name, arg_position, kwarg_name in _FUNCTION_CHECKS:
if node.func.name == func_name:
return _check_call_node_domain_argument(
node, arg_position=arg_position, kwarg_name=kwarg_name
)
return None
def _check_call_node_domain_argument(
call_node: nodes.Call,
*,
arg_position: int | None,
kwarg_name: str,
) -> nodes.NodeNG | None:
"""Ensure the argument node is a domain constant or variable.
Return None if the argument node is valid, or the argument node if it is invalid.
"""
if arg_position is not None and len(call_node.args) > arg_position:
argument_node = call_node.args[arg_position]
else:
argument_node = next(
iter(
keyword.value
for keyword in call_node.keywords
if keyword.arg == kwarg_name
),
None,
)
if argument_node and not _check_domain_argument(argument_node):
return argument_node
return None
def _check_domain_argument(arg_node: nodes.NodeNG) -> bool:
"""Ensure the argument node is a domain constant or variable.
We allow:
- x.DOMAIN/x.domain attribute (including *_DOMAIN/*_domain)
- DOMAIN/domain name (including *_DOMAIN/*_domain)
- string literals
- subscript expressions (e.g. data["key"])
Return True if the argument is valid, False otherwise.
"""
match arg_node:
case nodes.Attribute():
if (
attrname := arg_node.attrname
) in _DOMAIN_CONSTANTS or attrname.endswith(_DOMAIN_SUFFIXES):
return True
case nodes.Const():
if isinstance(arg_node.value, str):
return True
case nodes.Name():
if (node_name := arg_node.name) in _DOMAIN_CONSTANTS or node_name.endswith(
_DOMAIN_SUFFIXES
):
return True
case nodes.Subscript():
# Ignore subscripts like dict["key"]
return True
return False
class DomainConstantChecker(BaseChecker):
"""Checker for correct use of DOMAIN constants in tests."""
name = "home_assistant_domain_constant"
priority = -1
msgs = {
"C7415": (
"Argument %s should be a domain constant or variable in %s",
"home-assistant-domain-argument",
"Used when argument should be a domain constant/variable.",
),
}
options = ()
_in_test_module: bool
def visit_module(self, node: nodes.Module) -> None:
"""Visit Module node."""
self._in_test_module = is_test_module(node.name)
def visit_call(self, node: nodes.Call) -> None:
"""Visit Call node."""
if not self._in_test_module:
return
if invalid_arg_node := _check_call_node_domain_arguments(node):
self.add_message(
"home-assistant-domain-argument",
node=invalid_arg_node,
args=(invalid_arg_node.as_string(), node.func.as_string()),
)
def register(linter: PyLinter) -> None:
"""Register the checker."""
linter.register_checker(DomainConstantChecker(linter))
@@ -2745,95 +2745,6 @@ async def test_subscribe_trigger(
assert sum(hass.bus.async_listeners().values()) == init_count
@pytest.mark.parametrize(
("trigger", "expected_error"),
[
# Unknown trigger platform
(
{"platform": "nonexistent"},
{
"code": "invalid_format",
"message": "Invalid trigger 'nonexistent' specified",
},
),
# Missing mandatory config for the trigger platform
(
{"platform": "numeric_state"},
{
"code": "invalid_format",
"message": "required key not provided @ data['entity_id']",
},
),
],
)
async def test_subscribe_trigger_config_error(
hass: HomeAssistant,
websocket_client: MockHAClientWebSocket,
caplog: pytest.LogCaptureFixture,
trigger: dict,
expected_error: dict,
) -> None:
"""Test trigger config errors are reported to the client without logging."""
caplog.set_level(logging.ERROR)
await websocket_client.send_json_auto_id(
{"type": "subscribe_trigger", "trigger": trigger}
)
msg = await websocket_client.receive_json()
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert msg["error"] == expected_error
# The expected error is not logged by the default websocket error handler
assert "Error handling message" not in caplog.text
async def test_subscribe_trigger_template_error_spams_log(
hass: HomeAssistant,
websocket_client: MockHAClientWebSocket,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that a failing trigger template spams the log.
This documents unwanted behavior. Unlike subscribe_condition and
test_condition, subscribe_trigger does not suppress repeated template
variable errors: a trigger is evaluated event-driven by its platform (here
via async_track_template_result), outside any trace context, so the
trace-based suppression used for conditions does not apply. This should be
fixed in the future so the error is suppressed/forwarded instead of being
logged on every re-render.
"""
caplog.set_level(logging.WARNING)
hass.states.async_set("sensor.test", "1")
await websocket_client.send_json_auto_id(
{
"type": "subscribe_trigger",
"trigger": {
"platform": "template",
"value_template": "{{ states('sensor.test') }}{{ undefined_variable }}",
},
}
)
msg = await websocket_client.receive_json()
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
# Each re-render of the template logs the undefined variable error again
for state in ("2", "3", "4"):
hass.states.async_set("sensor.test", state)
await hass.async_block_till_done()
assert (
caplog.text.count(
"Template variable warning: 'undefined_variable' is undefined"
)
> 1
)
async def test_test_condition(
hass: HomeAssistant, websocket_client: MockHAClientWebSocket
) -> None:
+254
View File
@@ -0,0 +1,254 @@
"""Tests for the domain constant checker."""
import astroid
from pylint.testutils import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
from pylint_home_assistant.checkers.domain_constant import DomainConstantChecker
import pytest
from . import assert_no_messages
@pytest.fixture(name="domain_constant_checker")
def domain_constant_checker_fixture(
linter: UnittestLinter,
) -> DomainConstantChecker:
"""Fixture to provide a domain constant checker."""
return DomainConstantChecker(linter)
@pytest.mark.parametrize(
"code",
[
pytest.param(
"""
async_setup_component(hass, DOMAIN, {})
""",
id="name_domain_constant",
),
pytest.param(
"""
async_setup_component(hass, domain, {})
""",
id="name_domain_variable",
),
pytest.param(
"""
async_setup_component(hass, SENSOR_DOMAIN, {})
""",
id="name_prefixed_domain_constant",
),
pytest.param(
"""
async_setup_component(hass, sensor_domain, {})
""",
id="name_prefixed_domain_variable",
),
pytest.param(
"""
async_setup_component(hass, sensor.DOMAIN, {})
""",
id="attribute_domain_constant",
),
pytest.param(
"""
async_setup_component(hass, config_entry.domain, {})
""",
id="attribute_domain",
),
pytest.param(
"""
async_setup_component(hass, sensor.SENSOR_DOMAIN, {})
""",
id="attribute_prefixed_domain_constant",
),
pytest.param(
"""
async_setup_component(hass, "sensor", {})
""",
id="string_literal",
),
pytest.param(
"""
async_setup_component(hass, config["domain"], {})
""",
id="subscript_positional",
),
pytest.param(
"""
MockConfigEntry(domain=entries["domain"])
""",
id="subscript_kwarg",
),
pytest.param(
"""
async_mock_service(hass, DOMAIN, "service")
""",
id="async_mock_service",
),
pytest.param(
"""
MockConfigEntry(domain=DOMAIN)
""",
id="mock_config_entry_kwarg",
),
pytest.param(
"""
MockConfigEntry("other")
""",
id="mock_config_entry_positional_not_checked",
),
pytest.param(
"""
hass.services.async_call(DOMAIN, "service")
""",
id="services_async_call",
),
pytest.param(
"""
hass.services.call(DOMAIN, "service")
""",
id="services_call",
),
pytest.param(
"""
hass.config_entries.flow.async_init(DOMAIN)
""",
id="flow_async_init_positional",
),
pytest.param(
"""
hass.config_entries.flow.async_init(handler=DOMAIN)
""",
id="flow_async_init_kwarg",
),
pytest.param(
"""
some_other_function(hass, "other", {})
""",
id="unrelated_function",
),
pytest.param(
"""
hass.services.unrelated("other")
""",
id="unrelated_method",
),
],
)
def test_no_warning(
linter: UnittestLinter,
domain_constant_checker: DomainConstantChecker,
code: str,
) -> None:
"""Test cases that should not trigger a warning."""
root_node = astroid.parse(code, "tests.components.test_integration.test_init")
walker = ASTWalker(linter)
walker.add_checker(domain_constant_checker)
with assert_no_messages(linter):
walker.walk(root_node)
@pytest.mark.parametrize(
("code", "args"),
[
pytest.param(
"""
async_setup_component(hass, OTHER, {})
""",
("OTHER", "async_setup_component"),
id="name_not_domain",
),
pytest.param(
"""
async_setup_component(hass, sensor.OTHER, {})
""",
("sensor.OTHER", "async_setup_component"),
id="attribute_not_domain",
),
pytest.param(
"""
async_setup_component(hass, 5, {})
""",
("5", "async_setup_component"),
id="non_string_constant",
),
pytest.param(
"""
async_mock_service(hass, OTHER, "service")
""",
("OTHER", "async_mock_service"),
id="async_mock_service",
),
pytest.param(
"""
MockConfigEntry(domain=OTHER)
""",
("OTHER", "MockConfigEntry"),
id="mock_config_entry_kwarg",
),
pytest.param(
"""
hass.services.async_call(OTHER, "service")
""",
("OTHER", "hass.services.async_call"),
id="services_async_call",
),
pytest.param(
"""
hass.services.call(OTHER, "service")
""",
("OTHER", "hass.services.call"),
id="services_call",
),
pytest.param(
"""
hass.config_entries.flow.async_init(OTHER)
""",
("OTHER", "hass.config_entries.flow.async_init"),
id="flow_async_init_positional",
),
pytest.param(
"""
hass.config_entries.flow.async_init(handler=OTHER)
""",
("OTHER", "hass.config_entries.flow.async_init"),
id="flow_async_init_kwarg",
),
],
)
def test_domain_argument_flagged(
linter: UnittestLinter,
domain_constant_checker: DomainConstantChecker,
code: str,
args: tuple[str, str],
) -> None:
"""Test that non-domain arguments are flagged."""
root_node = astroid.parse(code, "tests.components.test_integration.test_init")
walker = ASTWalker(linter)
walker.add_checker(domain_constant_checker)
walker.walk(root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-domain-argument"
assert messages[0].args == args
def test_not_test_module_ignored(
linter: UnittestLinter,
domain_constant_checker: DomainConstantChecker,
) -> None:
"""Test that modules outside tests are ignored."""
root_node = astroid.parse(
"""
async_setup_component(hass, OTHER, {})
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(domain_constant_checker)
with assert_no_messages(linter):
walker.walk(root_node)