mirror of
https://github.com/home-assistant/core.git
synced 2026-06-03 18:03:43 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53211759cb | |||
| 4593059db2 | |||
| 593ae9eb80 |
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user