Files
moonwell-client-core/scripts/doc-links.py
T
Sam Sung b7bc7a7e3f Contributor docs: the quick test loop, the reference pin, open gaps (#1283)
A fresh-clone walk-through as a new contributor found four gaps in the docs:

- No quick test loop: `scripts/check.sh` has nothing to check on a clean clone, and `cargo test -p <crate>` was not mentioned. "The loop" now says both.
- The doc-link lint is a full `cargo doc` of the workspace; METHOD and the script's header now say so.
- What to cite while a benilla-re gap is still open: the client address, with the benilla-re issue or pull request linked.
- When `reference/PIN` moves: only with a change that needs a newer benilla-re row, to a commit on its main. Stated in CONTRIBUTING and `reference/README.md`.

Docs only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:07:29 +07:00

103 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fail on a doc link whose target is declared nowhere in the workspace.
It runs `cargo doc` over the whole workspace (a full doc build, minutes on a cold tree) and reads
rustdoc's unresolved-link warnings and fails on a `crate::`, `super::`, `self::` or
`Self::` path whose leaf name is declared nowhere: the item is gone. Every other warning is a
convention here, not rot (a private module named through its parent, another crate's module
named with `crate::`, a bare name for an unimported bevy type).
scripts/doc-links.py # the gate: exit 1 on any dead-target link
scripts/doc-links.py --report # + the benign counts
The leaf test is a loose declaration grep: a name declared anywhere counts, since what it catches
is a deleted item, not a misspelled path.
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def run_rustdoc() -> str:
out = subprocess.run(
["cargo", "doc", "--workspace", "--no-deps", "--document-private-items"],
cwd=ROOT,
capture_output=True,
text=True,
)
return out.stderr + out.stdout
def declared_names() -> set[str]:
"""Every name the workspace declares, in one scan: a grep per link is too slow for a gate."""
names: set[str] = set()
# `fn` gets its own pattern: in one alternation, `const fn foo` captures "fn" and never
# indexes `foo`.
pats = [
re.compile(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)"),
re.compile(r"\b(?:struct|enum|trait|type|const|static|union|mod)\s+([A-Za-z_][A-Za-z0-9_]*)"),
re.compile(r"\bmacro_rules!\s+([A-Za-z_][A-Za-z0-9_]*)"),
]
for rs in (ROOT / "crates").rglob("*.rs"):
try:
text = rs.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for pat in pats:
names.update(pat.findall(text))
names.discard("fn")
return names
def main() -> int:
if any(a in ("-h", "--help") for a in sys.argv[1:]):
print(__doc__.strip())
return 0
report = "--report" in sys.argv
text = run_rustdoc()
found = re.findall(r"warning: unresolved link to `([^`]+)`\n\s*--> ([^\s:]+):(\d+)", text)
names = declared_names()
dead, benign = [], 0
for target, path, line in found:
# Only a `crate::`/`super::`/`self::`/`Self::` path asserts a local item; a bare name may
# be a bevy type not imported for linking (`SystemParam`), which is not rot.
if not re.match(r"^(crate|super|self|Self)::", target):
benign += 1
continue
leaf = target.split("::")[-1].split("(")[0].strip()
if not leaf or not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", leaf):
benign += 1
continue
if leaf in names:
benign += 1
else:
dead.append((target, path, line))
if report:
print(f"doc-links: {len(found)} unresolved-link warnings")
print(f"doc-links: {benign} name something that still exists (see this file's header)")
if not dead:
print(f"doc-links ok: no doc link points at a deleted item ({len(found)} benign)")
return 0
print(f"doc-links FAILED: {len(dead)} doc link(s) point at something that no longer exists:\n")
for target, path, line in dead:
print(f" {path}:{line}\n [`{target}`]")
print(
"\nEach names an item declared nowhere in the workspace. Repoint it at whatever replaced\n"
"the thing, or drop the brackets — a dangling link promises navigation that is not there."
)
return 1
if __name__ == "__main__":
raise SystemExit(main())