summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/loop_var_prefix.py
blob: 8f1bb5659ab4e8f2a6f8675f532fbcda592350e3 (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
101
102
103
104
105
106
107
108
109
110
111
112
113
"""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

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

if TYPE_CHECKING:
    from ansiblelint.errors import MatchError
    from ansiblelint.file_utils import Lintable
    from ansiblelint.utils import Task


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"
    _ids = {
        "loop-var-prefix[wrong]": "Loop variable name does not match regex.",
        "loop-var-prefix[missing]": "Replace unsafe implicit `item` loop variable.",
    }

    def matchtask(
        self,
        task: Task,
        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.raw_task
        for key in task.raw_task:
            if key.startswith("with_"):
                has_loop = True

        if has_loop:
            loop_control = task.raw_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",
                6,
                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