Files
TheRock/compiler/amd-llvm/clang/docs/ghlinks.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

274 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""Script to link #GH123 to our github issue tracker.
We use MyST and reST, so this Sphinx plugin operates on the shared doctree
representation of the documentation, which is effectively a documentation-AST.
Docutils doctree node reference:
https://docutils.sourceforge.io/docs/ref/doctree.html
"""
import argparse
import re
import sys
import tempfile
import textwrap
from pathlib import Path
def _find_docs_requirements() -> str:
for parent in Path(__file__).resolve().parents:
docs_requirements = parent / "llvm/docs/requirements.txt"
if docs_requirements.exists():
return str(docs_requirements)
return "llvm/docs/requirements.txt"
try:
from docutils import nodes
from sphinx.application import Sphinx
except ImportError as err:
print(
textwrap.dedent(
f"""
ghlinks.py requires the LLVM documentation build dependencies.
Import failed with:
{err}
The standard requirements file is:
{_find_docs_requirements()}
From an llvm-project checkout, a typical pip setup is:
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install -r llvm/docs/requirements.txt
python3 clang/docs/ghlinks.py --test
With uv, a typical one-shot command is:
uv run --with-requirements llvm/docs/requirements.txt \\
python clang/docs/ghlinks.py --test
"""
).strip(),
file=sys.stderr,
)
raise
__version__ = "1.0"
GH_LINK_RE = re.compile("#GH([1-9][0-9]+)")
GH_LINK_TMPL = "https://github.com/llvm/llvm-project/issues/{}"
SKIP_NODES: tuple[type[nodes.Node], ...] = (
nodes.FixedTextElement,
nodes.literal,
nodes.raw,
nodes.reference,
)
def make_gh_link(issue: str) -> nodes.reference:
"""Create the docutils node that writers render as an external link."""
return nodes.reference("", "#" + issue, refuri=GH_LINK_TMPL.format(issue))
def replace_gh_links(node: nodes.Text) -> None:
"""Replace one text node with text fragments and GitHub issue links.
docutils text nodes cannot contain child nodes, so a single string like
"See #GH123." has to become a sibling list: Text("See "), reference(...),
Text("."). The parent node owns that sibling list.
"""
remaining = str(node)
replacements = []
while GH_LINK_RE.search(remaining):
before, issue, remaining = GH_LINK_RE.split(remaining, maxsplit=1)
if before:
replacements.append(nodes.Text(before))
replacements.append(make_gh_link(issue))
# If we found matches, do the replacement.
if replacements:
if remaining:
replacements.append(nodes.Text(remaining))
node.parent.replace(node, replacements)
def replace_gh_links_in_subtree(node: nodes.Node) -> None:
"""Rewrite linkable text nodes under node, pruning ignored subtrees.
The doctree is the markup-language neutral documentation AST, so it
handles both reST and MyST (markdown). It helps us avoid rewriting #GH123
in code and literal blocks.
"""
# DFS cutoff for code blocks etc.
if isinstance(node, SKIP_NODES):
return
# Find #GH123 links in text blocks and linkify them.
if isinstance(node, nodes.Text):
if GH_LINK_RE.search(str(node)):
replace_gh_links(node)
return
# Recursive DFS traversal of children.
if isinstance(node, nodes.Element):
for child in list(node.children):
replace_gh_links_in_subtree(child)
def subst_gh_links(_app: Sphinx, doctree: nodes.document) -> None:
"""Link #GH123 references after the source has been parsed."""
replace_gh_links_in_subtree(doctree)
def setup(app: Sphinx) -> dict[str, object]:
app.connect("doctree-read", subst_gh_links)
return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True)
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--test", action="store_true", help="run ghlinks self-tests")
args = parser.parse_args(argv)
if args.test:
run_tests()
print(
"ghlinks.py: tests passed; next, rebuild docs-clang-html and spot check the release notes"
)
return 0
parser.print_help(sys.stderr)
return 0
# -----------------------------------------------------------------------------
# Test code only below:
# -----------------------------------------------------------------------------
REST_TEST_DOC = r"""
GHLink reST Test
================
A paragraph links #GH123 and #GH456.
No leading zero link #GH0123.
Inline literal ``#GH333`` stays text.
Existing link `#GH666 <https://example.com/rst>`_ stays existing.
Code block::
#GH777
.. raw:: html
<span>#GH888</span>
"""
MARKDOWN_TEST_DOC = r"""
# GHLink Markdown Test
A paragraph links #GH234 and #GH567.
No leading zero link #GH0234.
Inline code `#GH444` stays text.
Existing link [#GH999](https://example.com/md) stays existing.
```c
#GH778
```
"""
def _build_test_docs() -> tuple[str, str]:
"""Build in-file reST and Markdown test strings and return their HTML."""
conf = f"""
import sys
sys.path.insert(0, {str(Path(__file__).parent)!r})
extensions = ["ghlinks", "myst_parser"]
master_doc = "index"
project = "ghlinks test"
source_suffix = {{
".rst": "restructuredtext",
".md": "markdown",
}}
"""
index = """
GHLink Tests
============
.. toctree::
rest
markdown
"""
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
srcdir = tmp_path / "src"
outdir = tmp_path / "out"
doctreedir = tmp_path / "doctrees"
srcdir.mkdir()
(srcdir / "conf.py").write_text(textwrap.dedent(conf), encoding="utf-8")
(srcdir / "index.rst").write_text(textwrap.dedent(index), encoding="utf-8")
(srcdir / "rest.rst").write_text(
textwrap.dedent(REST_TEST_DOC), encoding="utf-8"
)
(srcdir / "markdown.md").write_text(
textwrap.dedent(MARKDOWN_TEST_DOC), encoding="utf-8"
)
app = Sphinx(
srcdir=srcdir,
confdir=srcdir,
outdir=outdir,
doctreedir=doctreedir,
buildername="html",
freshenv=True,
warningiserror=True,
status=None,
warning=None,
)
app.build()
return (
(outdir / "rest.html").read_text(encoding="utf-8"),
(outdir / "markdown.html").read_text(encoding="utf-8"),
)
def _issue_href(issue: str) -> str:
return f'href="{GH_LINK_TMPL.format(issue)}"'
def _check_contains(html: str, needle: str) -> None:
if needle not in html:
raise AssertionError(f"expected HTML to contain: {needle}")
def run_tests() -> None:
rest_html, markdown_html = _build_test_docs()
for issue in ("123", "456"):
_check_contains(rest_html, _issue_href(issue))
for issue in ("0123", "333", "666", "777", "888"):
_check_contains(rest_html, f"#GH{issue}")
_check_contains(rest_html, 'href="https://example.com/rst"')
for issue in ("234", "567"):
_check_contains(markdown_html, _issue_href(issue))
for issue in ("0234", "444", "999", "778"):
_check_contains(markdown_html, f"#GH{issue}")
_check_contains(markdown_html, 'href="https://example.com/md"')
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))