summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/generate_docs.py
blob: 2e518d143674955576960f47e869fafe60901e04 (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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""Utils to generate rules documentation."""
import logging
from pathlib import Path
from typing import Iterable

from rich import box
from rich.console import RenderableType

# Remove this compatibility try-catch block once we drop support for rich < 10.7.0
try:
    from rich.console import group
except ImportError:
    from rich.console import render_group as group  # type: ignore

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

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_docs(rules: RulesCollection) -> str:
    """Dump documentation files for all rules, returns only confirmation message.

    That is internally used for building documentation and the API can change
    at any time.
    """
    result = ""
    dump_path = Path(".") / "docs" / "rules"
    if not dump_path.exists():
        raise RuntimeError(f"Failed to find {dump_path} folder for dumping rules.")

    with open(dump_path / ".." / "profiles.md", "w", encoding="utf-8") as f:
        f.write(profiles_as_md(header=True, docs_url="rules/"))

    for rule in rules.alphabetical():
        result = ""
        with open(dump_path / f"{rule.id}.md", "w", encoding="utf-8") as f:
            # 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}"):
                    raise RuntimeError(
                        f"Rule {rule.__class__} markdown help does not start with `# {rule.id}` header.\n{rule.help}"
                    )
                result = result[1:]
                result += f"{rule.help}"
            else:
                description = rule.description
                if rule.link:
                    description += f" [more]({rule.link})"

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

    return "All markdown files for rules were dumped!"


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 rule.tags:
            tag = f"[dim] ({', '.join(rule.tags)})[/dim]"
        else:
            tag = ""
        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}"):
                raise RuntimeError(
                    f"Rule {rule.__class__} markdown help does not start with `# {rule.id}` header.\n{rule.help}"
                )
            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}"

    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())