mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 13:15:06 +00:00

Co-authored-by: Oscar Gustafsson <oscar.gustafsson@gmail.com> Co-authored-by: Micha Reiser <micha@reiser.io>
300 lines
9.7 KiB
Python
300 lines
9.7 KiB
Python
"""Generate an MkDocs-compatible `docs` and `mkdocs.yml` from the README.md."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from collections.abc import Sequence
|
|
from itertools import chain
|
|
from pathlib import Path
|
|
from typing import NamedTuple
|
|
|
|
import mdformat
|
|
import yaml
|
|
|
|
|
|
class Section(NamedTuple):
|
|
"""A section to include in the MkDocs documentation."""
|
|
|
|
title: str
|
|
filename: str
|
|
generated: bool
|
|
# If subsections is present, the `filename` and `generated` value is unused.
|
|
subsections: Sequence[Section] | None = None
|
|
|
|
|
|
SECTIONS: list[Section] = [
|
|
Section("Overview", "index.md", generated=True),
|
|
Section("Tutorial", "tutorial.md", generated=False),
|
|
Section("Installing Ruff", "installation.md", generated=False),
|
|
Section("The Ruff Linter", "linter.md", generated=False),
|
|
Section("The Ruff Formatter", "formatter.md", generated=False),
|
|
Section(
|
|
"Editors",
|
|
"",
|
|
generated=False,
|
|
subsections=[
|
|
Section("Editor Integration", "editors/index.md", generated=False),
|
|
Section("Setup", "editors/setup.md", generated=False),
|
|
Section("Features", "editors/features.md", generated=False),
|
|
Section("Settings", "editors/settings.md", generated=False),
|
|
Section("Migrating from ruff-lsp", "editors/migration.md", generated=False),
|
|
],
|
|
),
|
|
Section("Configuring Ruff", "configuration.md", generated=False),
|
|
Section("Preview", "preview.md", generated=False),
|
|
Section("Rules", "rules.md", generated=True),
|
|
Section("Settings", "settings.md", generated=True),
|
|
Section("Versioning", "versioning.md", generated=False),
|
|
Section("Integrations", "integrations.md", generated=False),
|
|
Section("FAQ", "faq.md", generated=False),
|
|
Section("Contributing", "contributing.md", generated=True),
|
|
]
|
|
|
|
LINK_REWRITES: dict[str, str] = {
|
|
"https://docs.astral.sh/ruff/": "index.md",
|
|
"https://docs.astral.sh/ruff/configuration/": "configuration.md",
|
|
"https://docs.astral.sh/ruff/configuration/#config-file-discovery": (
|
|
"configuration.md#config-file-discovery"
|
|
),
|
|
"https://docs.astral.sh/ruff/contributing/": "contributing.md",
|
|
"https://docs.astral.sh/ruff/editors": "editors/index.md",
|
|
"https://docs.astral.sh/ruff/editors/setup": "editors/setup.md",
|
|
"https://docs.astral.sh/ruff/faq/#how-does-ruffs-linter-compare-to-flake8": (
|
|
"faq.md#how-does-ruffs-linter-compare-to-flake8"
|
|
),
|
|
"https://docs.astral.sh/ruff/faq/#how-does-ruffs-formatter-compare-to-black": (
|
|
"faq.md#how-does-ruffs-formatter-compare-to-black"
|
|
),
|
|
"https://docs.astral.sh/ruff/installation/": "installation.md",
|
|
"https://docs.astral.sh/ruff/rules/": "rules.md",
|
|
"https://docs.astral.sh/ruff/settings/": "settings.md",
|
|
"#whos-using-ruff": "https://github.com/astral-sh/ruff#whos-using-ruff",
|
|
}
|
|
|
|
|
|
def clean_file_content(content: str, title: str) -> str:
|
|
"""Add missing title, fix the header depth, and remove trailing empty lines."""
|
|
lines = content.splitlines()
|
|
if lines[0].startswith("# "):
|
|
return content
|
|
|
|
in_code_block = False
|
|
for i, line in enumerate(lines):
|
|
if line.startswith("```"):
|
|
in_code_block = not in_code_block
|
|
if not in_code_block and line.startswith("#"):
|
|
lines[i] = line[1:]
|
|
|
|
# Remove trailing empty lines.
|
|
for line in reversed(lines):
|
|
if line == "":
|
|
del lines[-1]
|
|
else:
|
|
break
|
|
|
|
content = "\n".join(lines) + "\n"
|
|
|
|
# Add a missing title.
|
|
return f"# {title}\n\n" + content
|
|
|
|
|
|
def generate_rule_metadata(rule_doc: Path) -> None:
|
|
"""Add frontmatter metadata containing a rule's code and description.
|
|
|
|
For example:
|
|
```yaml
|
|
---
|
|
description: Checks for abstract classes without abstract methods or properties.
|
|
tags:
|
|
- B024
|
|
---
|
|
```
|
|
"""
|
|
# Read the rule doc into lines.
|
|
with rule_doc.open("r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
|
|
# Get the description and rule code from the rule doc lines.
|
|
rule_code = None
|
|
description = None
|
|
what_it_does_found = False
|
|
for line in lines:
|
|
if line == "\n":
|
|
continue
|
|
|
|
# Assume that the only first-level heading is the rule title and code.
|
|
#
|
|
# For example: given `# abstract-base-class-without-abstract-method (B024)`,
|
|
# extract the rule code (`B024`).
|
|
if line.startswith("# "):
|
|
rule_code = line.strip().rsplit("(", 1)
|
|
rule_code = rule_code[1][:-1]
|
|
|
|
if line.startswith("## What it does"):
|
|
what_it_does_found = True
|
|
continue # Skip the '## What it does' line
|
|
|
|
if what_it_does_found and not description:
|
|
description = line.removesuffix("\n")
|
|
|
|
if all([rule_code, description]):
|
|
break
|
|
else:
|
|
if not rule_code:
|
|
raise ValueError("Missing title line")
|
|
|
|
if not what_it_does_found:
|
|
raise ValueError(f"Missing '## What it does' in {rule_doc}")
|
|
|
|
with rule_doc.open("w", encoding="utf-8") as f:
|
|
f.writelines(
|
|
"\n".join(
|
|
[
|
|
"---",
|
|
"description: |-",
|
|
f" {description}",
|
|
"tags:",
|
|
f"- {rule_code}",
|
|
"---",
|
|
"",
|
|
"",
|
|
]
|
|
)
|
|
)
|
|
f.writelines(lines)
|
|
|
|
|
|
def main() -> None:
|
|
"""Generate an MkDocs-compatible `docs` and `mkdocs.yml`."""
|
|
subprocess.run(["cargo", "dev", "generate-docs"], check=True)
|
|
|
|
with Path("README.md").open(encoding="utf8") as fp:
|
|
content = fp.read()
|
|
|
|
# Rewrite links to the documentation.
|
|
for src, dst in LINK_REWRITES.items():
|
|
before = content
|
|
after = content.replace(f"({src})", f"({dst})")
|
|
if before == after:
|
|
msg = f"Unexpected link rewrite in README.md: {src}"
|
|
raise ValueError(msg)
|
|
content = after
|
|
|
|
if m := re.search(r"\(https://docs.astral.sh/ruff/.*\)", content):
|
|
msg = f"Unexpected absolute link to documentation: {m.group(0)}"
|
|
raise ValueError(msg)
|
|
|
|
Path("docs").mkdir(parents=True, exist_ok=True)
|
|
|
|
# Split the README.md into sections.
|
|
for title, filename, generated, _ in SECTIONS:
|
|
if not generated:
|
|
continue
|
|
|
|
with Path(f"docs/{filename}").open("w+", encoding="utf8") as f:
|
|
if filename == "contributing.md":
|
|
# Copy the CONTRIBUTING.md.
|
|
shutil.copy("CONTRIBUTING.md", "docs/contributing.md")
|
|
continue
|
|
|
|
if filename == "settings.md":
|
|
file_content = subprocess.check_output(
|
|
["cargo", "dev", "generate-options"],
|
|
encoding="utf-8",
|
|
)
|
|
else:
|
|
block = content.split(f"<!-- Begin section: {title} -->\n\n")
|
|
if len(block) != 2:
|
|
msg = f"Section {title} not found in README.md"
|
|
raise ValueError(msg)
|
|
|
|
block = block[1].split(f"\n<!-- End section: {title} -->")
|
|
if len(block) != 2:
|
|
msg = f"Section {title} not found in README.md"
|
|
raise ValueError(msg)
|
|
|
|
file_content = block[0]
|
|
|
|
if filename == "rules.md":
|
|
file_content += "\n" + subprocess.check_output(
|
|
["cargo", "dev", "generate-rules-table"],
|
|
encoding="utf-8",
|
|
)
|
|
|
|
f.write(clean_file_content(file_content, title))
|
|
|
|
for rule_doc in Path("docs/rules").glob("*.md"):
|
|
# Format rules docs. This has to be completed before adding the meta description
|
|
# otherwise the meta description will be formatted in a way that mkdocs does not
|
|
# support.
|
|
mdformat.file(rule_doc, extensions=["mkdocs"])
|
|
|
|
generate_rule_metadata(rule_doc)
|
|
|
|
with Path("mkdocs.template.yml").open(encoding="utf8") as fp:
|
|
config = yaml.safe_load(fp)
|
|
|
|
# Add the redirect section to mkdocs.yml.
|
|
rules = json.loads(
|
|
subprocess.check_output(
|
|
[
|
|
"cargo",
|
|
"run",
|
|
"-p",
|
|
"ruff",
|
|
"--",
|
|
"rule",
|
|
"--all",
|
|
"--output-format",
|
|
"json",
|
|
],
|
|
),
|
|
)
|
|
config["plugins"].append(
|
|
{
|
|
"redirects": {
|
|
"redirect_maps": dict(
|
|
chain.from_iterable(
|
|
[
|
|
(f"rules/{rule['code']}.md", f"rules/{rule['name']}.md"),
|
|
(
|
|
f"rules/{rule['code'].lower()}.md",
|
|
f"rules/{rule['name']}.md",
|
|
),
|
|
]
|
|
for rule in rules
|
|
)
|
|
),
|
|
},
|
|
},
|
|
)
|
|
|
|
# Add the nav section to mkdocs.yml.
|
|
config["nav"] = []
|
|
for section in SECTIONS:
|
|
if section.subsections is None:
|
|
config["nav"].append({section.title: section.filename})
|
|
else:
|
|
config["nav"].append(
|
|
{
|
|
section.title: [
|
|
{subsection.title: subsection.filename}
|
|
for subsection in section.subsections
|
|
]
|
|
}
|
|
)
|
|
|
|
with Path("mkdocs.generated.yml").open("w+", encoding="utf8") as fp:
|
|
yaml.safe_dump(config, fp)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate an MkDocs-compatible `docs` and `mkdocs.yml`.",
|
|
)
|
|
args = parser.parse_args()
|
|
main()
|