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
|