diff options
Diffstat (limited to 'src/ansiblelint/rules/yaml_rule.py')
-rw-r--r-- | src/ansiblelint/rules/yaml_rule.py | 210 |
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") |