summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/generate_docs.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/generate_docs.py')
-rw-r--r--src/ansiblelint/generate_docs.py176
1 files changed, 176 insertions, 0 deletions
diff --git a/src/ansiblelint/generate_docs.py b/src/ansiblelint/generate_docs.py
new file mode 100644
index 0000000..2e518d1
--- /dev/null
+++ b/src/ansiblelint/generate_docs.py
@@ -0,0 +1,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())