Files
go/internal/docs/refs.go
T
endandClaude Sonnet 5.5 d579e38ba5 fix: address docs checker review findings
Check every link reference definition, decode destinations per CommonMark,
report correct lines for empty headings, autolinks and nested definitions,
reject symlinked record directories and wrong-case path components, and
refuse titles that cannot pass. Only top-level headings count (spec section 6).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 14:57:21 -07:00

189 lines
5.0 KiB
Go

package docs
import (
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
)
// kinds is the set of reference forms a frontmatter key accepts.
type kinds uint8
const (
kAdr kinds = 1 << iota
kPlan
kURL
kFile
)
type refSpec struct {
key string
kinds kinds
}
var (
schemeRe = regexp.MustCompile(`^([A-Za-z][A-Za-z0-9+.-]*):`)
durableRe = regexp.MustCompile(`^([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)?#\d+$`)
idPattern = `\d{8}T\d{9}Z-[a-z0-9]+(?:-[a-z0-9]+)*`
adrPathRe = regexp.MustCompile(`^/docs/adr/` + idPattern + `\.md$`)
planPathRe = regexp.MustCompile(`^/docs/plans/` + idPattern + `\.md$`)
)
type ref struct {
path string // decoded repo-root path; empty for URLs and anchors
remote bool
}
// parseRef checks the syntax of a reference. body allows #anchors, mailto: and #fragments.
func parseRef(v string, body bool) (ref, string) {
if v == "" {
return ref{}, "empty reference"
}
if strings.Contains(v, `\`) {
return ref{}, "backslashes are not allowed"
}
if body && v[0] == '#' {
return ref{}, ""
}
if m := schemeRe.FindStringSubmatch(v); m != nil {
switch strings.ToLower(m[1]) {
case "http", "https":
u, err := url.Parse(v)
switch {
case err != nil || u.Hostname() == "":
return ref{}, "URL must have a host"
case u.User != nil:
return ref{}, "URL must not contain credentials"
}
return ref{remote: true}, ""
case "mailto":
if body {
return ref{}, ""
}
}
return ref{}, fmt.Sprintf("scheme %q is not allowed", m[1])
}
if v[0] != '/' {
return ref{}, "relative paths are not allowed; use a repo-root path starting with /"
}
if strings.HasPrefix(v, "//") {
return ref{}, "protocol-relative references are not allowed"
}
p, frag, hasFrag := strings.Cut(v, "#")
if hasFrag && !body {
return ref{}, "fragments are not allowed here (" + frag + ")"
}
if strings.Contains(p, "?") {
return ref{}, "queries are not allowed"
}
dec, err := url.PathUnescape(p)
if err != nil {
return ref{}, "invalid percent-encoding"
}
if slices.Contains(strings.Split(dec, "/"), "..") || strings.Contains(dec, `\`) {
return ref{}, `".." segments and backslashes are not allowed`
}
return ref{path: dec}, ""
}
// exists reports why a repo-root path is not a regular file inside the repository.
func (c *checker) exists(p string) string {
// Match each component's case exactly, so case-insensitive filesystems agree with Linux.
dir := c.root
for _, part := range strings.Split(p, "/") {
if part == "" || part == "." {
continue
}
entries, err := os.ReadDir(dir)
if err != nil || !slices.ContainsFunc(entries, func(e os.DirEntry) bool { return e.Name() == part }) {
return "target " + p + " does not exist"
}
dir = filepath.Join(dir, part)
}
real, err := filepath.EvalSymlinks(filepath.Join(c.root, filepath.FromSlash(p)))
if err != nil {
return "target " + p + " does not exist"
}
if rel, err := filepath.Rel(c.realRoot, real); err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "target " + p + " resolves outside the repository"
}
if st, err := os.Stat(real); err != nil || !st.Mode().IsRegular() {
return "target " + p + " is not a regular file"
}
return ""
}
// checkLink validates a body link destination.
func (c *checker) checkLink(r *record, line int, dest string) {
ref, msg := parseRef(dest, true)
if msg == "" && ref.path != "" {
msg = c.exists(ref.path)
}
if msg != "" {
c.add(r.path, line, "link %q: %s", dest, msg)
}
}
// refs validates the reference lists of a record against their specs.
func (m *fm) refs(specs ...refSpec) {
self := "/" + m.r.path
for _, spec := range specs {
items, ok := m.list(spec.key)
if !ok {
continue
}
seen := map[string]bool{}
for _, it := range items {
if it.v == marker {
continue
}
if msg := m.checkRef(spec, it.v, self); msg != "" {
m.c.add(m.r.path, it.line, "%s: %s", spec.key, msg)
} else if seen[it.v] {
m.c.add(m.r.path, it.line, "%s: duplicate reference %q", spec.key, it.v)
}
seen[it.v] = true
}
}
}
func (m *fm) checkRef(spec refSpec, v, self string) string {
if durableRe.MatchString(v) || slices.Contains([]string{"this pr", "this pull request", "this commit"}, strings.ToLower(v)) {
return fmt.Sprintf("%q is not durable; use a full URL", v)
}
r, msg := parseRef(v, false)
if msg != "" {
return msg
}
k := spec.kinds
switch {
case r.remote:
if k&kURL == 0 {
return "URLs are not accepted here; accepts " + k.String()
}
return ""
case k&kFile == 0 && !(k&kAdr != 0 && adrPathRe.MatchString(v)) && !(k&kPlan != 0 && planPathRe.MatchString(v)):
return fmt.Sprintf("%q is not accepted here; accepts %s", v, k)
case r.path == self:
return "a record must not reference itself"
}
return m.c.exists(r.path)
}
func (k kinds) String() string {
var s []string
for _, e := range []struct {
k kinds
s string
}{{kAdr, "/docs/adr/<id>.md"}, {kPlan, "/docs/plans/<id>.md"}, {kURL, "http(s) URLs"}, {kFile, "repo-root file paths"}} {
if k&e.k != 0 {
s = append(s, e.s)
}
}
return strings.Join(s, ", ")
}