#!/usr/bin/env python3 """Audit the docs' source anchors against the working tree. For every "`src/foo.rs:123`" style anchor in the docs, check that the file exists, the line is in range, and the line is not blank. LIMITATION: this only checks that the anchor TARGETS A REAL, NON-BLANK LINE. It does not check that the line says what the doc claims. A doc could say "foo.rs:123 defines get_by_key" when line 123 is actually `fn get_by_name`. This audit would pass. Someone still has to read the anchors to verify they're accurate. The one exception is a *phantom identifier*: if a backticked name sits right next to an anchor, we check the name actually appears in that file. Catches docs claiming an identifier that doesn't exist. This is a loose substring check, so it has the same limitation as the anchor check above. It catches the obvious case but not a near-miss name. Two shapes of claim live next to an anchor, and they are checked with deliberately different strictness: - Numbered anchor (`src/state.rs:283`). Kept as a loose substring check on the member name. PAIR's EXT only matches a name followed by a colon and a line number, so the anchor already pins down the location and the reader can verify the line. Widening the verdict here would change the count of findings in this branch, i.e. perturb the established baseline, for no gain: any doc written next to an anchor names the place and the name. - Bare file name (`src/channel.rs`, no line number). Checked with the strict `member_present` test and reported as a `phantom member`. No line number means the reader has nothing to verify, so the only thing this check can prove is that the named member actually exists in the file. And a loose test provably fails here: a doc can write `TelegramClient::send` and satisfy a substring -- even a word-boundary -- test twice over, because `channel.rs` contains `send_text` and reqwest's `.send()` is `send` at a word boundary. Hence definition/unqualified-reference semantics. """ import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] DOCS = ['AGENTS.md', 'README.md', 'README.zh-CN.md', 'docs/design.md', 'docs/deployment.md', 'docs/reference.md', 'docs/retired.md', 'docs/roadmap.md', 'docs/charter.md', 'cloudflare-worker/README.md'] # `Ident` or `Module::Ident` inside backticks, optional () after. IDENT = r'[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)?' # A namespaced path: at least two segments joined by `::`. Required for the # bare-file-name branch -- PAIR's IDENT would also match a bare `Foo` and # produce a phantom-member hit on every ordinary mention. IDENT_NSP = (r'[A-Za-z_][A-Za-z0-9_]*' + r'::' + r'(?:[A-Za-z_][A-Za-z0-9_]*::)*[A-Za-z_][A-Za-z0-9_]*') # Optional directory prefix, then the bare file stem. FILE = r'((?:[A-Za-z0-9_.-]+/)?[A-Za-z0-9_-]+)' # Line anchor: `.rs:123`. The optional range suffix lives in REF, not here. EXT = r'\.(rs|js):(\d+)' # Bare file name: `.rs` not immediately followed by a colon. The negative # lookahead keeps PAIR_BARE from re-matching anchors PAIR already consumed # (`channel.rs:110`), so a numbered anchor is never reported twice. EXT_BARE = r'\.(rs|js)(?!:)' REF = re.compile( r'((?:src|cloudflare-worker/src|web)(?:/[A-Za-z0-9_.-]+)*/)?' + FILE + EXT + r'(?:\s*[,–\-]\s*(\d+))?' ) # "Backticked identifier, some slack, right next to a file name" -- the shape # shared by PAIR and PAIR_BARE. They differ only in the file-name half (EXT vs # EXT_BARE) and in how strict the identifier is allowed to be. GAP = (r'`(' + IDENT + r')(?:\(\))?`' r'[^`\n|]{0,40}?`?[^`\n|]{0,40}?' r'(? len(src): errors.append(f'{d}:{i} {p.name}:{a}-{b} out of range (file has {len(src)} lines)') continue lo = int(a) targets = {lo} | ({hi} if b else set()) for n in sorted(targets): checked += 1 if not src[n - 1].strip(): errors.append(f'{d}:{i} {p.name}:{a}{"-" + b if b else ""} -> line {n} is blank') print(f'=== ANCHOR AUDIT ({checked} line-refs checked, {len(errors)} errors) ===') for e in errors: print(e) sys.exit(1 if errors else 0)