summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/loop_var_prefix.py
blob: cc909a338b5a05021953c977a1e44d963236dee5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
"""Optional Ansible-lint rule to enforce use of prefix on role loop vars."""
from __future__ import annotations

import re
import sys
from typing import TYPE_CHECKING, Any

from ansiblelint.config import LOOP_VAR_PREFIX, options
from ansiblelint.errors import MatchError
from ansiblelint.rules import AnsibleLintRule
from ansiblelint.text import toidentifier

if TYPE_CHECKING:
    from ansiblelint.file_utils import Lintable


class RoleLoopVarPrefix(AnsibleLintRule):
    """Role loop_var should use configured prefix."""

    id = "loop-var-prefix"
    link = (
        "https://docs.ansible.com/ansible/latest/playbook_guide/"
        "playbooks_loops.html#defining-inner-and-outer-variable-names-with-loop-var"
    )
    description = """\
Looping inside roles has the risk of clashing with loops from user-playbooks.\
"""

    tags = ["idiom"]
    prefix = re.compile("")
    severity = "MEDIUM"

    def matchtask(
        self, task: dict[str, Any], file: Lintable | None = None
    ) -> list[MatchError]:
        """Return matches for a task."""
        if not file or not file.role or not options.loop_var_prefix:
            return []

        self.prefix = re.compile(
            options.loop_var_prefix.format(role=toidentifier(file.role))
        )
        has_loop = "loop" in task
        for key in task.keys():
            if key.startswith("with_"):
                has_loop = True

        if has_loop:
            loop_control = task.get("loop_control", {})
            loop_var = loop_control.get("loop_var", "")

            if loop_var:
                if not self.prefix.match(loop_var):
                    return [
                        self.create_matcherror(
                            message=f"Loop variable name does not match /{options.loop_var_prefix}/ regex, where role={toidentifier(file.role)}.",
                            filename=file,
                            tag="loop-var-prefix[wrong]",
                        )
                    ]
            else:
                return [
                    self.create_matcherror(
                        message=f"Replace unsafe implicit `item` loop variable by adding a `loop_var` that is matching /{options.loop_var_prefix}/ regex.",
                        filename=file,
                        tag="loop-var-prefix[missing]",
                    )
                ]

        return []


# testing code to be loaded only with pytest or when executed the rule file
if "pytest" in sys.modules:
    import pytest

    from ansiblelint.rules import RulesCollection  # pylint: disable=ungrouped-imports
    from ansiblelint.runner import Runner  # pylint: disable=ungrouped-imports

    @pytest.mark.parametrize(
        ("test_file", "failures"),
        (
            pytest.param(
                "examples/playbooks/roles/loop_var_prefix/tasks/pass.yml", 0, id="pass"
            ),
            pytest.param(
                "examples/playbooks/roles/loop_var_prefix/tasks/fail.yml", 5, id="fail"
            ),
        ),
    )
    def test_loop_var_prefix(
        default_rules_collection: RulesCollection, test_file: str, failures: int
    ) -> None:
        """Test rule matches."""
        # Enable checking of loop variable prefixes in roles
        options.loop_var_prefix = LOOP_VAR_PREFIX
        results = Runner(test_file, rules=default_rules_collection).run()
        for result in results:
            assert result.rule.id == RoleLoopVarPrefix().id
        assert len(results) == failures