summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/yaml_rule.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/rules/yaml_rule.py')
-rw-r--r--src/ansiblelint/rules/yaml_rule.py210
1 files changed, 210 insertions, 0 deletions
diff --git a/src/ansiblelint/rules/yaml_rule.py b/src/ansiblelint/rules/yaml_rule.py
new file mode 100644
index 0000000..4da4d41
--- /dev/null
+++ b/src/ansiblelint/rules/yaml_rule.py
@@ -0,0 +1,210 @@
+"""Implementation of yaml linting rule (yamllint integration)."""
+from __future__ import annotations
+
+import logging
+import sys
+from collections.abc import Iterable
+from typing import TYPE_CHECKING
+
+from yamllint.linter import run as run_yamllint
+
+from ansiblelint.constants import LINE_NUMBER_KEY, SKIPPED_RULES_KEY
+from ansiblelint.file_utils import Lintable
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.yaml_utils import load_yamllint_config
+
+if TYPE_CHECKING:
+ from typing import Any
+
+ from ansiblelint.errors import MatchError
+
+_logger = logging.getLogger(__name__)
+
+
+class YamllintRule(AnsibleLintRule):
+ """Violations reported by yamllint."""
+
+ id = "yaml"
+ severity = "VERY_LOW"
+ tags = ["formatting", "yaml"]
+ version_added = "v5.0.0"
+ config = load_yamllint_config()
+ has_dynamic_tags = True
+ link = "https://yamllint.readthedocs.io/en/stable/rules.html"
+ # ensure this rule runs before most of other common rules
+ _order = 1
+ _ids = {
+ "yaml[anchors]": "",
+ "yaml[braces]": "",
+ "yaml[brackets]": "",
+ "yaml[colons]": "",
+ "yaml[commas]": "",
+ "yaml[comments-indentation]": "",
+ "yaml[comments]": "",
+ "yaml[document-end]": "",
+ "yaml[document-start]": "",
+ "yaml[empty-lines]": "",
+ "yaml[empty-values]": "",
+ "yaml[float-values]": "",
+ "yaml[hyphens]": "",
+ "yaml[indentation]": "",
+ "yaml[key-duplicates]": "",
+ "yaml[key-ordering]": "",
+ "yaml[line-length]": "",
+ "yaml[new-line-at-end-of-file]": "",
+ "yaml[new-lines]": "",
+ "yaml[octal-values]": "",
+ "yaml[quoted-strings]": "",
+ "yaml[trailing-spaces]": "",
+ "yaml[truthy]": "",
+ }
+
+ def matchyaml(self, file: Lintable) -> list[MatchError]:
+ """Return matches found for a specific YAML text."""
+ matches: list[MatchError] = []
+ if str(file.base_kind) != "text/yaml":
+ return matches
+
+ for problem in run_yamllint(
+ file.content,
+ YamllintRule.config,
+ filepath=file.path,
+ ):
+ self.severity = "VERY_LOW"
+ if problem.level == "error":
+ self.severity = "MEDIUM"
+ matches.append(
+ self.create_matcherror(
+ # yamllint does return lower-case sentences
+ message=problem.desc.capitalize(),
+ lineno=problem.line,
+ details="",
+ filename=file,
+ tag=f"yaml[{problem.rule}]",
+ ),
+ )
+ return matches
+
+
+def _combine_skip_rules(data: Any) -> set[str]:
+ """Return a consolidated list of skipped rules."""
+ result = set(data.get(SKIPPED_RULES_KEY, []))
+ tags = data.get("tags", [])
+ if tags and (
+ isinstance(tags, Iterable)
+ and "skip_ansible_lint" in tags
+ or tags == "skip_ansible_lint"
+ ):
+ result.add("skip_ansible_lint")
+ return result
+
+
+def _fetch_skips(data: Any, collector: dict[int, set[str]]) -> dict[int, set[str]]:
+ """Retrieve a dictionary with line: skips by looking recursively in given JSON structure."""
+ if hasattr(data, "get") and data.get(LINE_NUMBER_KEY):
+ rules = _combine_skip_rules(data)
+ if rules:
+ collector[data.get(LINE_NUMBER_KEY)].update(rules)
+ if isinstance(data, Iterable) and not isinstance(data, str):
+ if isinstance(data, dict):
+ for _entry, value in data.items():
+ _fetch_skips(value, collector)
+ else: # must be some kind of list
+ for entry in data:
+ if (
+ entry
+ and hasattr(entry, "get")
+ and LINE_NUMBER_KEY in entry
+ and SKIPPED_RULES_KEY in entry
+ and entry[SKIPPED_RULES_KEY]
+ ):
+ collector[entry[LINE_NUMBER_KEY]].update(entry[SKIPPED_RULES_KEY])
+ _fetch_skips(entry, collector)
+ return collector
+
+
+# testing code to be loaded only with pytest or when executed the rule file
+if "pytest" in sys.modules:
+ import pytest
+
+ # pylint: disable=ungrouped-imports
+ from ansiblelint.config import options
+ from ansiblelint.rules import RulesCollection
+ from ansiblelint.runner import Runner
+
+ @pytest.mark.parametrize(
+ ("file", "expected_kind", "expected"),
+ (
+ pytest.param(
+ "examples/yamllint/invalid.yml",
+ "yaml",
+ [
+ 'Missing document start "---"',
+ 'Duplication of key "foo" in mapping',
+ "Trailing spaces",
+ ],
+ id="invalid",
+ ),
+ pytest.param("examples/yamllint/valid.yml", "yaml", [], id="valid"),
+ pytest.param(
+ "examples/yamllint/line-length.yml",
+ "yaml",
+ ["Line too long (166 > 160 characters)"],
+ id="line-length",
+ ),
+ pytest.param(
+ "examples/yamllint/multi-document.yaml",
+ "yaml",
+ [],
+ id="multi-document",
+ ),
+ pytest.param(
+ "examples/yamllint/skipped-rule.yml",
+ "yaml",
+ [],
+ id="skipped-rule",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-yaml-fail.yml",
+ "playbook",
+ [
+ "Truthy value should be one of [false, true]",
+ "Truthy value should be one of [false, true]",
+ "Truthy value should be one of [false, true]",
+ ],
+ id="rule-yaml-fail",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-yaml-pass.yml",
+ "playbook",
+ [],
+ id="rule-yaml-pass",
+ ),
+ ),
+ )
+ @pytest.mark.filterwarnings("ignore::ansible_compat.runtime.AnsibleWarning")
+ def test_yamllint(file: str, expected_kind: str, expected: list[str]) -> None:
+ """Validate parsing of ansible output."""
+ lintable = Lintable(file)
+ assert lintable.kind == expected_kind
+
+ rules = RulesCollection(options=options)
+ rules.register(YamllintRule())
+ results = Runner(lintable, rules=rules).run()
+
+ assert len(results) == len(expected), results
+ for idx, result in enumerate(results):
+ assert result.filename.endswith(file)
+ assert expected[idx] in result.message
+ assert isinstance(result.tag, str)
+ assert result.tag.startswith("yaml[")
+
+ def test_yamllint_has_help(default_rules_collection: RulesCollection) -> None:
+ """Asserts that we loaded markdown documentation in help property."""
+ for rule in default_rules_collection:
+ if rule.id == "yaml":
+ assert rule.help is not None
+ assert len(rule.help) > 100
+ break
+ else: # pragma: no cover
+ pytest.fail("No yaml rule found")