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
|