Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow linting plugin EXAMPLES as playbooks #3309

Merged
merged 11 commits into from
Oct 3, 2023
1 change: 1 addition & 0 deletions .config/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ pipx
pkgcache # linux
pkgs
placefolder
plainexamples
pluggy
pluginmanager
pmrun
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
env:
# Number of expected test passes, safety measure for accidental skip of
# tests. Update value if you add/remove tests.
PYTEST_REQPASS: 829
PYTEST_REQPASS: 831
steps:
- uses: actions/checkout@v4
with:
Expand Down
8 changes: 8 additions & 0 deletions plugins/modules/fake_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
"""
from ansible.module_utils.basic import AnsibleModule

EXAMPLES = r"""
- name: "playbook"
tasks:
- name: Hello
debug:
msg: 'world'
"""


def main() -> None:
"""Return the module instance."""
Expand Down
3 changes: 3 additions & 0 deletions src/ansiblelint/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ def __post_init__(self) -> None:
msg = "MatchError called incorrectly as column numbers start with 1"
raise RuntimeError(msg)

offset = getattr(self.lintable, "_line_offset", 0)
self.lineno += offset

@functools.cached_property
def level(self) -> str:
"""Return the level of the rule: error, warning or notice."""
Expand Down
40 changes: 40 additions & 0 deletions src/ansiblelint/file_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Utility functions related to file operations."""
from __future__ import annotations

import ast
import copy
import logging
import os
Expand All @@ -14,6 +15,7 @@
import pathspec
import wcmatch.pathlib
import wcmatch.wcmatch
from ansible.parsing.plugin_docs import read_docstring
from yaml.error import YAMLError

from ansiblelint.config import BASE_KINDS, Options, options
Expand Down Expand Up @@ -254,6 +256,25 @@ def __init__(
if self.kind == "yaml":
_ = self.data # pylint: disable=pointless-statement

if self.kind == "plugin":
# pylint: disable=consider-using-with
self.file = NamedTemporaryFile(
mode="w+",
suffix=f"_{name.name}.yaml",
dir=self.dir,
)
self.filename = self.file.name
self._content = self.parse_examples_from_plugin()
self.file.write(self._content)
self.file.flush()
self.path = Path(self.file.name)
self.base_kind = "text/yaml"

def __del__(self) -> None:
"""Clean up temporary files when the instance is cleaned up."""
if hasattr(self, "file"):
self.file.close()

def _guess_kind(self) -> None:
if self.kind == "yaml":
if (
Expand Down Expand Up @@ -372,6 +393,25 @@ def __repr__(self) -> str:
"""Return user friendly representation of a lintable."""
return f"{self.name} ({self.kind})"

def parse_examples_from_plugin(self) -> str:
"""Parse yaml inside plugin EXAMPLES string.

Store a line number offset to realign returned line numbers later
"""
parsed = ast.parse(self.content)
for child in parsed.body:
if isinstance(child, ast.Assign):
label = child.targets[0]
if isinstance(label, ast.Name) and label.id == "EXAMPLES":
self._line_offset = child.lineno - 1
break

docs = read_docstring(str(self.path))
examples = docs["plainexamples"]
# Ignore the leading newline and lack of document start
# as including those in EXAMPLES would be weird.
return f"---{examples}" if examples else ""

@property
def data(self) -> Any:
"""Return loaded data representation for current file, if possible."""
Expand Down
15 changes: 15 additions & 0 deletions test/test_file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ def test_discover_lintables_umlaut(monkeypatch: MonkeyPatch) -> None:
"playbook",
id="43",
), # content should determine it as a play
pytest.param(
"plugins/modules/fake_module.py",
"plugin",
id="44",
),
),
)
def test_kinds(path: str, kind: FileType) -> None:
Expand Down Expand Up @@ -536,3 +541,13 @@ def test_bug_2513(
results = Runner(filename, rules=default_rules_collection).run()
assert len(results) == 1
assert results[0].rule.id == "name"


def test_examples_content() -> None:
"""Test that a module loads the correct content."""
filename = Path("plugins/modules/fake_module.py")
lintable = Lintable(filename)
# Lintable is now correctly purporting to be a YAML file
assert lintable.base_kind == "text/yaml"
# Lintable content should be contents of EXAMPLES
assert lintable.content == "---" + BASIC_PLAYBOOK