Skip to content

Commit 28801cd

Browse files
Copilotjeduden
andauthored
plan: add 128/129 for undefined and unused reference-link labels
Agent-Logs-Url: https://github.com/jeduden/mdsmith/sessions/60d0ccb6-54d1-4e76-8ef2-45e329bf398b Co-authored-by: jeduden <1117699+jeduden@users.noreply.github.com>
1 parent 5ec598a commit 28801cd

4 files changed

Lines changed: 456 additions & 3 deletions

File tree

PLAN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ footer: |
3636
| 125 | 🔲 | sonnet | [No space inside link text rule](plan/125_no-space-in-link-text.md) |
3737
| 126 | 🔲 | sonnet | [Proper-name capitalization rule](plan/126_proper-names.md) |
3838
| 127 | 🔲 | sonnet | [Single H1 per file rule](plan/127_single-h1.md) |
39+
| 128 | 🔲 | sonnet | [Reject undefined reference-link labels](plan/128_no-undefined-reference-labels.md) |
40+
| 129 | 🔲 | sonnet | [Flag unused or duplicate link reference definitions](plan/129_no-unused-link-definitions.md) |
3941
| 52 || | [Archetype / Template Library for Agentic Patterns](plan/52_archetype-template-library.md) |
4042
| 61 || | [Required Structure Rule Hardening](plan/61_required-structure-hardening.md) |
4143
| 65 || | [Spike WASM-Embedded Weasel Inference](plan/65_spike-wasm-embedded-inference.md) |

docs/background/markdown-linters.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,9 @@ Weaknesses:
255255
| Required headings | [MDS020][mds020] (via schema) | [MD043][md043] | no |
256256
| Single H1 | planned ([plan 127][plan127]) | [MD047][md047] | no |
257257
| Link fragments | [MDS027][mds027] (cross-file) | [MD051][md051] | no |
258-
| Reference links | planned ([plan 107][plan107]) | [MD052][md052], [MD053][md053] | no |
258+
| Forbid ref-style | planned ([plan 107][plan107]) | no | no |
259+
| Undefined ref label | planned ([plan 128][plan128]) | [MD052][md052] | no |
260+
| Unused/dup ref def | planned ([plan 129][plan129]) | [MD053][md053] | no |
259261

260262
All three cover core structural rules. markdownlint has
261263
the broadest rule set. Plans cover inline HTML
@@ -267,8 +269,10 @@ emphasis ([plan 111][plan111]), reference-style links
267269
([plan 107][plan107]), no-space-in-code-spans
268270
([plan 124][plan124]), no-space-in-link-text
269271
([plan 125][plan125]), proper names
270-
([plan 126][plan126]), and single H1
271-
([plan 127][plan127]).
272+
([plan 126][plan126]), single H1
273+
([plan 127][plan127]), undefined reference labels
274+
([plan 128][plan128]), and unused link reference
275+
definitions ([plan 129][plan129]).
272276
Image alt text ([MDS032][mds032]) and OL numbering
273277
([MDS046][mds046]) are already implemented.
274278

@@ -753,3 +757,5 @@ you need a stable rule set while these land.
753757
[plan125]: ../../plan/125_no-space-in-link-text.md
754758
[plan126]: ../../plan/126_proper-names.md
755759
[plan127]: ../../plan/127_single-h1.md
760+
[plan128]: ../../plan/128_no-undefined-reference-labels.md
761+
[plan129]: ../../plan/129_no-unused-link-definitions.md
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
---
2+
id: 128
3+
title: Reject undefined reference-link labels
4+
status: "🔲"
5+
summary: >-
6+
New rule MDS052 that flags reference-style links and
7+
images whose label has no matching link reference
8+
definition in the file. Closes the parity gap with
9+
markdownlint MD052 and catches a class of broken link
10+
that MDS027 silently misses today.
11+
model: sonnet
12+
---
13+
# Reject undefined reference-link labels
14+
15+
## Goal
16+
17+
Flag `[text][label]`, `[label][]`, and `![alt][label]`
18+
forms whose `label` has no matching `[label]: url`
19+
definition in the same file. These are broken links.
20+
They render as literal text in most viewers (GitHub
21+
keeps the brackets visible). They currently pass
22+
mdsmith's lint without any diagnostic.
23+
24+
## Background
25+
26+
### Why MDS027 misses this
27+
28+
[MDS027][mds027] (cross-file-reference-integrity) walks
29+
`*ast.Link` nodes and verifies the destination resolves
30+
on disk. goldmark only constructs an `*ast.Link` for a
31+
reference-style usage *when a matching link reference
32+
definition exists*. When the definition is missing,
33+
goldmark leaves the bracketed text as plain
34+
`*ast.Text`. MDS027's walk never sees the broken link.
35+
No diagnostic is emitted.
36+
37+
The `docs/background/markdown-linters.md` comparison
38+
page surfaced this failure. It contained
39+
`[plan 12N][planNNN]` references with no matching
40+
`[planNNN]: ...` definition. The lint passed.
41+
markdownlint's [MD052][md052] catches this case.
42+
43+
### Why a separate rule
44+
45+
[Plan 107][plan107] proposes forbidding reference-style
46+
links entirely (`no-reference-style`). That is a
47+
stylistic policy. Some projects opt out (including
48+
the comparison page in this repo) because reference
49+
style keeps wide tables readable.
50+
51+
MDS052 must work *with* reference-style links by
52+
verifying their labels resolve. The two rules compose.
53+
Plan 107 lets you ban reference style; MDS052 lets
54+
projects that allow reference style catch typos.
55+
56+
### CommonMark normalization
57+
58+
CommonMark normalizes reference labels. It folds
59+
case, collapses inner whitespace, and trims the
60+
ends. goldmark's `parser.Context.Reference(label)`
61+
takes a normalized label. The rule must normalize the
62+
same way. Else `[Foo][BAR]` matching `[bar]: ...`
63+
flags as a false positive.
64+
65+
### Shortcut references
66+
67+
`[label]` (with no following `[ref]` or `(url)`) is a
68+
*shortcut* reference. The bracketed text is also the
69+
label. The rule should treat shortcut references the
70+
same as `[label][]` collapsed references. Flag
71+
when the label has no definition.
72+
73+
False positives are a real concern. `[just brackets]`
74+
in prose is technically a shortcut reference under
75+
CommonMark. Forbidding all of them would be too
76+
aggressive. The mitigation: only flag shortcut
77+
references when the surrounding text suggests link
78+
intent. For example, when the bracketed text contains
79+
characters typical of an anchor label (digits,
80+
hyphens, underscores), or is followed by `[]`. The
81+
default behavior is:
82+
83+
1. Always flag `[text][label]` and `[label][]` with no
84+
matching definition (these are unambiguously link
85+
syntax).
86+
2. Flag bare `[label]` only when the label looks like
87+
a reference target, *or* when configured to do so.
88+
89+
A `shortcut` setting (`always` | `collapsed-only` |
90+
`heuristic`, default `heuristic`) gives users control.
91+
92+
## Design
93+
94+
### Configuration
95+
96+
```yaml
97+
rules:
98+
no-undefined-reference-labels:
99+
enabled: true
100+
shortcut: heuristic
101+
placeholders: []
102+
```
103+
104+
Category: `link`. Enabled by default.
105+
106+
`placeholders` follows the [placeholder
107+
grammar][placeholder-grammar] convention used by
108+
MDS027 and other link rules. A label whose normalized
109+
text matches a configured placeholder token is treated
110+
as opaque and never flagged.
111+
112+
### Detection
113+
114+
1. Re-use the lint parser context obtained from
115+
`lint.NewParser()` so processing-instruction
116+
blocks are excluded the same way other PI-aware
117+
rules handle them. This mirrors
118+
`tocdirective.hasTOCLinkReference`.
119+
2. Walk the AST and collect every `*ast.Link` and
120+
`*ast.Image` whose `ReferenceType` is not
121+
`LinkRegular` (i.e. full, collapsed, or shortcut
122+
reference).
123+
3. For each, look up the normalized label in
124+
`parser.Context.Reference(label)`. If absent, emit
125+
a diagnostic at the bracket position.
126+
127+
Because goldmark drops *undefined* references back to
128+
text, step 2 alone misses them. To catch the dropped
129+
cases, add a source-level scan. A regex over the raw
130+
bytes that finds `[...](...)`-free bracket pairs of
131+
the forms `[X][Y]` and `[X][]`, then re-checks the
132+
label against the reference store. The scan must
133+
honor code spans, code fences, link destinations, and
134+
HTML blocks.
135+
136+
### Auto-fix
137+
138+
No auto-fix. The right repair is project-specific: add
139+
the missing definition, remove the broken link, or
140+
correct the label spelling.
141+
142+
### Error messages
143+
144+
```text
145+
reference label "plan128" has no matching link
146+
reference definition
147+
```
148+
149+
## Tasks
150+
151+
1. Scaffold `internal/rules/noundefinedreferencelabels/`
152+
with `rule.go`, `rule_test.go`, and the `init()`
153+
`rule.Register` call.
154+
2. Implement `Check()` that:
155+
a. Re-parses with `lint.NewParser` to get the
156+
`parser.Context` (so PI blocks are skipped) and
157+
collect link reference definitions.
158+
b. Walks `*ast.Link` and `*ast.Image` nodes for
159+
reference-typed usages and verifies each label.
160+
c. Performs a guarded source-level scan for the
161+
bracket forms goldmark drops, emitting a
162+
diagnostic per unresolved label.
163+
3. Implement `rule.Configurable` for `shortcut` and
164+
`placeholders`.
165+
4. Implement `rule.Defaultable` returning `true`.
166+
5. Register as MDS052 in category `link`.
167+
6. Add fixture tests in
168+
`internal/rules/MDS052-no-undefined-reference-labels/`
169+
covering: matching full reference (clean), matching
170+
collapsed reference (clean), matching shortcut
171+
(clean), undefined full reference (flagged),
172+
undefined collapsed reference (flagged), undefined
173+
shortcut with link-shaped label (flagged under
174+
`heuristic`), bracketed prose (clean under
175+
`heuristic`), case-fold/whitespace-normalized match
176+
(clean), placeholder label (clean), label inside
177+
code span (clean), label inside fenced code
178+
(clean), label inside PI block (clean).
179+
7. Add rule README following the MDS027 template,
180+
including a "See also" link to plan 107
181+
(no-reference-style) and to MDS027.
182+
8. Reproduce the original failure: re-introduce a
183+
`[plan128][planXYZ]` reference with no `[planXYZ]:`
184+
definition in `docs/background/markdown-linters.md`,
185+
confirm the rule flags it, then remove the test
186+
reference.
187+
188+
## Acceptance Criteria
189+
190+
- [ ] `[a][b]` with `[b]: url` defined emits no
191+
diagnostic.
192+
- [ ] `[b][]` with `[b]: url` defined emits no
193+
diagnostic.
194+
- [ ] `[b]` with `[b]: url` defined emits no
195+
diagnostic.
196+
- [ ] `[a][b]` with no `[b]: ...` definition emits one
197+
diagnostic on the link position.
198+
- [ ] `[b][]` with no `[b]: ...` definition emits one
199+
diagnostic.
200+
- [ ] `[plan128]` with no definition emits a
201+
diagnostic under `shortcut: heuristic` because
202+
the label looks like a reference target.
203+
- [ ] `[just brackets]` in prose emits no diagnostic
204+
under `shortcut: heuristic`.
205+
- [ ] `[Foo Bar]` resolves to `[foo bar]: ...`
206+
(CommonMark-normalized).
207+
- [ ] A label listed in `placeholders` is never
208+
flagged regardless of definition state.
209+
- [ ] Reference-style links inside code spans, fenced
210+
code, indented code, and `<?...?>` PI blocks are
211+
not flagged.
212+
- [ ] Re-running the rule on
213+
`docs/background/markdown-linters.md` (with a
214+
deliberately undefined `[plan999]` reference)
215+
flags exactly one diagnostic; removing the
216+
reference makes the file clean again.
217+
- [ ] Rule is enabled by default in the standard
218+
convention.
219+
- [ ] All tests pass: `go test ./...`
220+
- [ ] `go tool golangci-lint run` reports no issues
221+
- [ ] `mdsmith check .` passes on the repo.
222+
223+
## See also
224+
225+
- [MDS027 cross-file-reference-integrity][mds027]
226+
- [Plan 107: no-reference-style][plan107]
227+
- [Plan 129: no-unused-link-definitions][plan129]
228+
- [Placeholder grammar][placeholder-grammar]
229+
230+
[md052]: https://github.com/DavidAnson/markdownlint/blob/main/doc/md052.md
231+
[mds027]: ../internal/rules/MDS027-cross-file-reference-integrity/README.md
232+
[placeholder-grammar]: ../docs/background/concepts/placeholder-grammar.md
233+
[plan107]: 107_no-reference-style.md
234+
[plan129]: 129_no-unused-link-definitions.md

0 commit comments

Comments
 (0)