summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/generate_docs.py
blob: 6e319fb846a53533dcf9f4c475d4637e72381f40 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""Utils to generate rules documentation."""

import logging
from collections.abc import Iterable

from rich import box
from rich.console import RenderableType, group
from rich.markdown import Markdown
from rich.table import Table

from ansiblelint.config import PROFILES
from ansiblelint.constants import RULE_DOC_URL
from ansiblelint.rules import RulesCollection, TransformMixin

DOC_HEADER = """
# Default Rules

(lint_default_rules)=

Below you can see the list of default rules Ansible Lint use to evaluate playbooks and roles:

"""

_logger = logging.getLogger(__name__)


def rules_as_str(rules: RulesCollection) -> RenderableType:
    """Return rules as string."""
    table = Table(show_header=False, header_style="title", box=box.SIMPLE)
    for rule in rules.alphabetical():
        if issubclass(rule.__class__, TransformMixin):
            rule.tags.insert(0, "autofix")
        tag = f"[dim] ({', '.join(rule.tags)})[/dim]" if rule.tags else ""
        table.add_row(
            f"[link={RULE_DOC_URL}{rule.id}/]{rule.id}[/link]",
            rule.shortdesc + tag,
        )
    return table


def rules_as_md(rules: RulesCollection) -> str:
    """Return md documentation for a list of rules."""
    result = DOC_HEADER

    for rule in rules.alphabetical():
        # because title == rule.id we get the desired labels for free
        # and we do not have to insert `(target_header)=`
        title = f"{rule.id}"

        if rule.help:
            if not rule.help.startswith(f"# {rule.id}"):  # pragma: no cover
                msg = f"Rule {rule.__class__} markdown help does not start with `# {rule.id}` header.\n{rule.help}"
                raise RuntimeError(msg)
            result += f"\n\n{rule.help}"
        else:
            description = rule.description
            if rule.link:
                description += f" [more]({rule.link})"

            result += f"\n\n## {title}\n\n**{rule.shortdesc}**\n\n{description}"

        # Safety net for preventing us from adding autofix to rules and
        # forgetting to mention it inside their documentation.
        if "autofix" in rule.tags and "autofix" not in rule.description:
            msg = f"Rule {rule.id} is invalid because it has 'autofix' tag but this ability is not documented in its description."
            raise RuntimeError(msg)

    return result


@group()
def rules_as_rich(rules: RulesCollection) -> Iterable[Table]:
    """Print documentation for a list of rules, returns empty string."""
    width = max(16, *[len(rule.id) for rule in rules])
    for rule in rules.alphabetical():
        table = Table(show_header=True, header_style="title", box=box.MINIMAL)
        table.add_column(rule.id, style="dim", width=width)
        table.add_column(Markdown(rule.shortdesc))

        description = rule.help or rule.description
        if rule.link:
            description += f" [(more)]({rule.link})"
        table.add_row("description", Markdown(description))
        if rule.version_added:
            table.add_row("version_added", rule.version_added)
        if rule.tags:
            table.add_row("tags", ", ".join(rule.tags))
        if rule.severity:
            table.add_row("severity", rule.severity)
        yield table


def profiles_as_md(*, header: bool = False, docs_url: str = RULE_DOC_URL) -> str:
    """Return markdown representation of supported profiles."""
    result = ""

    if header:
        result += """<!---
Do not manually edit, generated from generate_docs.py
-->
# Profiles

Ansible-lint profiles gradually increase the strictness of rules as your Ansible content lifecycle.

!!! note

    Rules with `*` in the suffix are not yet implemented but are documented with linked GitHub issues.

"""

    for name, profile in PROFILES.items():
        extends = ""
        if profile.get("extends", None):
            extends = (
                f" It extends [{profile['extends']}](#{profile['extends']}) profile."
            )
        result += f"## {name}\n\n{profile['description']}{extends}\n"
        for rule, rule_data in profile["rules"].items():
            if "[" in rule:
                url = f"{docs_url}{rule.split('[')[0]}/"
            else:
                url = f"{docs_url}{rule}/"
            if not rule_data:
                result += f"- [{rule}]({url})\n"
            else:
                result += f"- [{rule}]({rule_data['url']})\n"

        result += "\n"
    return result


def profiles_as_rich() -> Markdown:
    """Return rich representation of supported profiles."""
    return Markdown(profiles_as_md())