|
| 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