check-doc-refs: catch bare relative markdown links, and only judge a file rendered where it is stored
The rel arm required a leading `./` or `../`, so `[x](swarm/services.md)` — the form a sibling link is usually written in — matched none of the three arms. That is exactly the shape a directory move breaks: during #3911 a link in that form moved into a new directory, resolved to nothing, and the lint stayed green. Reproduced by putting the broken form back with `sed` (MUTATED_DIRTY=1) and re-running: exit 0, a control that fails to fail. Widening the pattern surfaces 116 links the arm never saw (76 -> 192 matches). 111 are in `.md` files and all 111 resolve today, so this adds coverage without a single new failure in the tree; the other 5 are the interesting part, and they are why the arm now selects on file type. A link is only resolvable against its own directory when the file is *rendered where it is stored*. The four in `nix/docs/default.nix` are inside a markdown string this derivation writes to `$out`, and the one in `hive-c0re/src/workers/knowledge.rs` is template text for another repo's README — resolving either from the source directory certifies a path no reader ever requests. `lint:allow` is the wrong tool for them: the marker lives inside the generated text, so it would ship in the published page. The `.rs` arm keeps its error, narrowed to doc-comment lines. Its premise still holds — the repo has zero relative links in `.rs` doc comments — but without the narrowing the widened pattern reports the knowledge.rs template string with a message about rustdoc that is wrong for it. Measured: the old arm's 76 hits are all in `.md` files, so restricting resolution to `.md` removes no existing coverage. Closes #4076
This commit is contained in:
parent
239354d205
commit
484b8b1129
1 changed files with 28 additions and 20 deletions
|
|
@ -3,15 +3,14 @@
|
||||||
#
|
#
|
||||||
# A pointer at a doc is the hive's preferred alternative to duplicating that
|
# A pointer at a doc is the hive's preferred alternative to duplicating that
|
||||||
# doc's prose next to the code. That trade only pays while the pointer
|
# doc's prose next to the code. That trade only pays while the pointer
|
||||||
# resolves, and nothing was checking. A `docs/` reorganisation left 15 distinct
|
# resolves, and nothing was checking.
|
||||||
# dead paths across nix, css, html, js, markdown and .prettierignore.
|
|
||||||
#
|
#
|
||||||
# Emits a CI error annotation per hit and exits 1 if any pointer is dead, 0
|
# Emits a CI error annotation per hit; exit 1 if any pointer is dead.
|
||||||
# otherwise. Runs as its own job; seconds, pure git+grep.
|
|
||||||
#
|
#
|
||||||
# Three arms, because a pointer is written three ways:
|
# Three arms, because a pointer is written three ways:
|
||||||
# root — a repo-root-relative path in prose or a comment, `docs/<dir>/<f>.md`
|
# root — a repo-root-relative path in prose or a comment, `docs/<dir>/<f>.md`
|
||||||
# rel — a markdown link resolved against the linking file, [x](../<dir>/<f>.md)
|
# rel — a markdown link resolved against the linking file, dot-prefixed
|
||||||
|
# `[x](../<dir>/<f>.md)` or bare `[x](<dir>/<f>.md)`
|
||||||
# path — any repo-relative source path, `<crate>/src/<f>.rs`, `nix/<f>.nix`
|
# path — any repo-relative source path, `<crate>/src/<f>.rs`, `nix/<f>.nix`
|
||||||
# (spelled with placeholders on purpose: a literal example path here would be
|
# (spelled with placeholders on purpose: a literal example path here would be
|
||||||
# a dead pointer this script then reports against itself)
|
# a dead pointer this script then reports against itself)
|
||||||
|
|
@ -31,7 +30,7 @@
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
root_pattern='docs/[A-Za-z0-9._/-]*\.md'
|
root_pattern='docs/[A-Za-z0-9._/-]*\.md'
|
||||||
rel_pattern='\]\(\.\.*/[A-Za-z0-9._/-]*\.md'
|
rel_pattern='\]\([A-Za-z0-9._][A-Za-z0-9._/-]*\.md'
|
||||||
|
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
trap 'rm -rf "$tmp"' EXIT
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
@ -66,25 +65,34 @@ while IFS=: read -r file lineno match; do
|
||||||
[ -n "${match:-}" ] || continue
|
[ -n "${match:-}" ] || continue
|
||||||
exempt "$file" "$lineno" && continue
|
exempt "$file" "$lineno" && continue
|
||||||
link="${match#](}"
|
link="${match#](}"
|
||||||
# A `.rs` doc comment has a second renderer, and it does not count `../`
|
# This arm resolves against the directory of the file the link is written
|
||||||
# the way the rest of this arm assumes. rustdoc emits the href verbatim
|
# in, so it can only judge a file that is rendered where it is stored — a
|
||||||
# onto a page at `target/doc/<crate>/<module…>/`, so the link resolves
|
# tracked `.md`. A link inside a nix or rust string literal is text for a
|
||||||
# against *that* directory, not the source file's. Resolving from the
|
# document generated somewhere else (`$out`, another repo), and resolving
|
||||||
# source therefore certifies links that render broken — a false pass, not
|
# it from the source directory certifies a path no reader ever requests.
|
||||||
# a miss, and worse than no check.
|
# `lint:allow` is not the answer for those: the marker would sit inside
|
||||||
|
# the generated text and ship in the published page.
|
||||||
#
|
#
|
||||||
# Refused rather than resolved rustdoc-aware: the repo has zero relative
|
# rustdoc is the one non-md renderer that earns an error instead of a
|
||||||
# links in `.rs` doc comments (measured on this commit), so nothing needs
|
# skip. It emits the href verbatim onto a page at
|
||||||
# the second reader, and the backticked repo-root form the root arm above
|
# `target/doc/<crate>/<module…>/`, so a relative link in a doc comment
|
||||||
# already gates works under both. Inferring module nesting would be
|
# resolves against *that* directory and renders broken; the backticked
|
||||||
# machinery in service of a form nothing uses.
|
# repo-root form the root arm gates works under both renderers. Doc
|
||||||
|
# comments only — inferring module nesting to resolve one properly would
|
||||||
|
# be machinery in service of a form nothing uses.
|
||||||
case "$file" in
|
case "$file" in
|
||||||
|
*.md) ;;
|
||||||
*.rs)
|
*.rs)
|
||||||
dead=$((dead + 1))
|
case "$(sed -n "${lineno}p" "$file")" in
|
||||||
printf '::error file=%s,line=%s::relative markdown link in a rust doc comment renders against the rustdoc output directory, not this file; use a backticked repo-root pointer instead: %s\n' \
|
*///* | *//!*)
|
||||||
"$file" "$lineno" "$link"
|
dead=$((dead + 1))
|
||||||
|
printf '::error file=%s,line=%s::relative markdown link in a rust doc comment renders against the rustdoc output directory, not this file; use a backticked repo-root pointer instead: %s\n' \
|
||||||
|
"$file" "$lineno" "$link"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
|
*) continue ;;
|
||||||
esac
|
esac
|
||||||
# Resolved from the linking file's directory, so `../` counts the same way
|
# Resolved from the linking file's directory, so `../` counts the same way
|
||||||
# the renderer counts it. `-m` keeps a link that escapes the repo root
|
# the renderer counts it. `-m` keeps a link that escapes the repo root
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue