|
3 | 3 |
|
4 | 4 | import logging |
5 | 5 | import sys |
6 | | -from typing import Any |
| 6 | +from typing import TYPE_CHECKING, Any |
7 | 7 |
|
8 | 8 | from ansible.plugins.loader import module_loader |
9 | 9 |
|
10 | 10 | from ansiblelint.constants import LINE_NUMBER_KEY |
11 | 11 | from ansiblelint.errors import MatchError |
12 | | -from ansiblelint.file_utils import Lintable |
13 | | -from ansiblelint.rules import AnsibleLintRule, RulesCollection |
| 12 | +from ansiblelint.rules import AnsibleLintRule, TransformMixin |
| 13 | + |
| 14 | +if TYPE_CHECKING: |
| 15 | + from ruamel.yaml.comments import CommentedMap, CommentedSeq |
| 16 | + |
| 17 | + from ansiblelint.file_utils import Lintable # noqa: F811 |
14 | 18 |
|
15 | 19 | _logger = logging.getLogger(__name__) |
16 | 20 |
|
|
86 | 90 | ] |
87 | 91 |
|
88 | 92 |
|
89 | | -class FQCNBuiltinsRule(AnsibleLintRule): |
| 93 | +class FQCNBuiltinsRule(AnsibleLintRule, TransformMixin): |
90 | 94 | """Use FQCN for builtin actions.""" |
91 | 95 |
|
92 | 96 | id = "fqcn" |
@@ -176,9 +180,37 @@ def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]: |
176 | 180 | ] |
177 | 181 | return [] |
178 | 182 |
|
| 183 | + def transform( |
| 184 | + self, |
| 185 | + match: MatchError, |
| 186 | + lintable: Lintable, |
| 187 | + data: CommentedMap | CommentedSeq | str, |
| 188 | + ) -> None: |
| 189 | + if match.tag in {"fqcn[action-core]", "fqcn[action]", "fqcn[canonical]"}: |
| 190 | + target_task = self.seek(match.yaml_path, data) |
| 191 | + # Unfortunately, a lot of data about Ansible content gets lost here, you only get a simple dict. |
| 192 | + # For now, just parse the error messages for the data about action names etc. and fix this later. |
| 193 | + if match.tag == "fqcn[action-core]": |
| 194 | + # split at the first bracket, cut off the last bracket and dot |
| 195 | + current_action = match.message.split("(")[1][:-2] |
| 196 | + # This will always replace builtin modules with "ansible.builtin" versions, not "ansible.legacy". |
| 197 | + # The latter is technically more correct in what ansible has executed so far, the former is most likely better understood and more robust. |
| 198 | + new_action = match.details.split("`")[1] |
| 199 | + elif match.tag == "fqcn[action]": |
| 200 | + current_action = match.details.split("`")[1] |
| 201 | + new_action = match.message.split("`")[1] |
| 202 | + elif match.tag == "fqcn[canonical]": |
| 203 | + current_action = match.message.split("`")[3] |
| 204 | + new_action = match.message.split("`")[1] |
| 205 | + for _ in range(len(target_task)): |
| 206 | + k, v = target_task.popitem(False) |
| 207 | + target_task[new_action if k == current_action else k] = v |
| 208 | + match.fixed = True |
| 209 | + |
179 | 210 |
|
180 | 211 | # testing code to be loaded only with pytest or when executed the rule file |
181 | 212 | if "pytest" in sys.modules: |
| 213 | + from ansiblelint.rules import RulesCollection |
182 | 214 | from ansiblelint.runner import Runner # pylint: disable=ungrouped-imports |
183 | 215 |
|
184 | 216 | def test_fqcn_builtin_fail() -> None: |
|
0 commit comments