summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/no_free_form.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/rules/no_free_form.py')
-rw-r--r--src/ansiblelint/rules/no_free_form.py102
1 files changed, 96 insertions, 6 deletions
diff --git a/src/ansiblelint/rules/no_free_form.py b/src/ansiblelint/rules/no_free_form.py
index e89333b..13489ef 100644
--- a/src/ansiblelint/rules/no_free_form.py
+++ b/src/ansiblelint/rules/no_free_form.py
@@ -1,20 +1,25 @@
"""Implementation of NoFreeFormRule."""
+
from __future__ import annotations
+import functools
import re
import sys
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
from ansiblelint.constants import INCLUSION_ACTION_NAMES, LINE_NUMBER_KEY
-from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.rules import AnsibleLintRule, TransformMixin
+from ansiblelint.rules.key_order import task_property_sorter
if TYPE_CHECKING:
+ from ruamel.yaml.comments import CommentedMap, CommentedSeq
+
from ansiblelint.errors import MatchError
from ansiblelint.file_utils import Lintable
from ansiblelint.utils import Task
-class NoFreeFormRule(AnsibleLintRule):
+class NoFreeFormRule(AnsibleLintRule, TransformMixin):
"""Rule for detecting discouraged free-form syntax for action modules."""
id = "no-free-form"
@@ -75,7 +80,7 @@ class NoFreeFormRule(AnsibleLintRule):
"win_command",
"win_shell",
):
- if self.cmd_shell_re.match(action_value):
+ if self.cmd_shell_re.search(action_value):
fail = True
else:
fail = True
@@ -89,12 +94,97 @@ class NoFreeFormRule(AnsibleLintRule):
)
return results
+ def transform(
+ self,
+ match: MatchError,
+ lintable: Lintable,
+ data: CommentedMap | CommentedSeq | str,
+ ) -> None:
+ if "no-free-form" in match.tag:
+ task = self.seek(match.yaml_path, data)
+
+ def filter_values(
+ val: str,
+ filter_key: str,
+ filter_dict: dict[str, Any],
+ ) -> str:
+ """Pull out key=value pairs from a string and set them in filter_dict.
+
+ Returns unmatched strings.
+ """
+ if filter_key not in val:
+ return val
+
+ extra = ""
+ [k, v] = val.split(filter_key, 1)
+ if " " in k:
+ extra, k = k.rsplit(" ", 1)
+
+ if v[0] in "\"'":
+ # Keep quoted strings together
+ quote = v[0]
+ _, v, remainder = v.split(quote, 2)
+ v = f"{quote}{v}{quote}"
+ else:
+ try:
+ v, remainder = v.split(" ", 1)
+ except ValueError:
+ remainder = ""
+
+ filter_dict[k] = v
+
+ extra = " ".join(
+ (extra, filter_values(remainder, filter_key, filter_dict)),
+ )
+ return extra.strip()
+
+ if match.tag == "no-free-form":
+ module_opts: dict[str, Any] = {}
+ for _ in range(len(task)):
+ k, v = task.popitem(False)
+ # identify module as key and process its value
+ if len(k.split(".")) == 3 and isinstance(v, str):
+ cmd = filter_values(v, "=", module_opts)
+ if cmd:
+ module_opts["cmd"] = cmd
+
+ sorted_module_opts = {}
+ for key in sorted(
+ module_opts.keys(),
+ key=functools.cmp_to_key(task_property_sorter),
+ ):
+ sorted_module_opts[key] = module_opts[key]
+
+ task[k] = sorted_module_opts
+ else:
+ task[k] = v
+
+ match.fixed = True
+ elif match.tag == "no-free-form[raw]":
+ exec_key_val: dict[str, Any] = {}
+ for _ in range(len(task)):
+ k, v = task.popitem(False)
+ if isinstance(v, str) and "executable" in v:
+ # Filter the executable and other parts from the string
+ task[k] = " ".join(
+ [
+ item
+ for item in v.split(" ")
+ if filter_values(item, "=", exec_key_val)
+ ],
+ )
+ task["args"] = exec_key_val
+ else:
+ task[k] = v
+ match.fixed = True
+
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
+ # pylint: disable=ungrouped-imports
+ from ansiblelint.rules import RulesCollection
+ from ansiblelint.runner import Runner
@pytest.mark.parametrize(
("file", "expected"),