Skip to content

Commit 93d7c09

Browse files
authored
--write: Introduce TransformMixin for Rules (#2023)
1 parent 8d14bde commit 93d7c09

File tree

2 files changed

+64
-3
lines changed

2 files changed

+64
-3
lines changed

src/ansiblelint/rules/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from importlib.abc import Loader
1313
from typing import Any, Dict, Iterator, List, Optional, Set, Union
1414

15+
from ruamel.yaml.comments import CommentedMap, CommentedSeq
16+
1517
import ansiblelint.skip_utils
1618
import ansiblelint.utils
1719
import ansiblelint.yaml_utils
@@ -200,6 +202,44 @@ def matchyaml(self, file: Lintable) -> List[MatchError]:
200202
return matches
201203

202204

205+
class TransformMixin: # pylint: disable=too-few-public-methods
206+
"""A mixin for AnsibleLintRule to enable transforming files.
207+
208+
If ansible-lint is started with the ``--write`` option, then the ``Transformer``
209+
will call the ``transform()`` method for every MatchError identified if the rule
210+
that identified it subclasses this ``TransformMixin``. Only the rule that identified
211+
a MatchError can do transforms to fix that match.
212+
"""
213+
214+
def transform(
215+
self,
216+
match: MatchError,
217+
lintable: Lintable,
218+
data: Union[CommentedMap, CommentedSeq, str],
219+
) -> None:
220+
"""Transform ``data`` to try to fix the MatchError identified by this rule.
221+
222+
The ``match`` was generated by this rule in the ``lintable`` file.
223+
When ``transform()`` is called on a rule, the rule should either fix the
224+
issue, if possible, or make modifications that make it easier to fix manually.
225+
226+
For YAML files, ``data`` is an editable YAML dict/array that preserves
227+
any comments that were in the original file.
228+
229+
.. code:: python
230+
231+
data[0]["tasks"][0]["when"] = False
232+
233+
For any files that aren't YAML, data is the loaded file's content as a string.
234+
To edit non-YAML files, save the updated contents update ``lintable.content``:
235+
236+
.. code:: python
237+
238+
new_data = self.do_something_to_fix_the_match(data)
239+
lintable.content = new_data
240+
"""
241+
242+
203243
def is_valid_rule(rule: Any) -> bool:
204244
"""Check if given rule is valid or not."""
205245
return issubclass(rule, AnsibleLintRule) and bool(rule.id) and bool(rule.shortdesc)

src/ansiblelint/transformer.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Transformer implementation."""
22
import logging
3-
from typing import Dict, List, Set, Union
3+
from typing import Dict, List, Optional, Set, Union
44

55
from ruamel.yaml.comments import CommentedMap, CommentedSeq
66

77
from ansiblelint.errors import MatchError
88
from ansiblelint.file_utils import Lintable
9+
from ansiblelint.rules import TransformMixin
910
from ansiblelint.runner import LintResult
1011
from ansiblelint.yaml_utils import FormattedYAML
1112

@@ -50,7 +51,7 @@ def __init__(self, result: LintResult):
5051

5152
def run(self) -> None:
5253
"""For each file, read it, execute transforms on it, then write it."""
53-
for file, _ in self.matches_per_file.items():
54+
for file, matches in self.matches_per_file.items():
5455
# str() convinces mypy that "text/yaml" is a valid Literal.
5556
# Otherwise, it thinks base_kind is one of playbook, meta, tasks, ...
5657
file_is_yaml = str(file.base_kind) == "text/yaml"
@@ -62,17 +63,37 @@ def run(self) -> None:
6263
data = ""
6364
file_is_yaml = False
6465

66+
ruamel_data: Optional[Union[CommentedMap, CommentedSeq]] = None
6567
if file_is_yaml:
6668
# We need a fresh YAML() instance for each load because ruamel.yaml
6769
# stores intermediate state during load which could affect loading
6870
# any other files. (Based on suggestion from ruamel.yaml author)
6971
yaml = FormattedYAML()
7072

71-
ruamel_data: Union[CommentedMap, CommentedSeq] = yaml.loads(data)
73+
ruamel_data = yaml.loads(data)
7274
if not isinstance(ruamel_data, (CommentedMap, CommentedSeq)):
7375
# This is an empty vars file or similar which loads as None.
7476
# It is not safe to write this file or data-loss is likely.
7577
# Only maps and sequences can preserve comments. Skip it.
7678
continue
79+
80+
self._do_transforms(file, ruamel_data or data, matches)
81+
82+
if file_is_yaml:
83+
# noinspection PyUnboundLocalVariable
7784
file.content = yaml.dumps(ruamel_data)
85+
86+
if file.updated:
7887
file.write()
88+
89+
@staticmethod
90+
def _do_transforms(
91+
file: Lintable,
92+
data: Union[CommentedMap, CommentedSeq, str],
93+
matches: List[MatchError],
94+
) -> None:
95+
"""Do Rule-Transforms handling any last-minute MatchError inspections."""
96+
for match in sorted(matches):
97+
if not isinstance(match.rule, TransformMixin):
98+
continue
99+
match.rule.transform(match, file, data)

0 commit comments

Comments
 (0)