| id | 111 |
|---|---|
| title | Ambiguous emphasis rule |
| status | ✅ |
| summary | New rule MDS047 that flags emphasis runs whose meaning a human cannot predict at a glance even though CommonMark resolves them deterministically. Targets the parser-stress cases ("Exhibit A") and the ReDoS pattern shape called out in the bgslabs.org rant. |
| model | sonnet |
Let users forbid emphasis sequences that are
technically valid but unreadable. The CommonMark
emphasis algorithm runs a left-flanking and
right-flanking delimiter scan that can pair * and
_ runs in non-obvious ways. The output is
well-defined; the source is not. Examples from the
rant include *****\*a* and ***Peter* Piper** _Picked___a___Pack_. The rule names a small,
auditable set of "you cannot tell at a glance"
shapes and refuses them.
Three patterns cover most of the surprise:
- Long delimiter runs — three or more contiguous
*or_characters at one boundary (e.g.***,____,*****). CommonMark splits the run between bold and italic by counting from the matching closing run; the split rarely matches author intent. - Adjacent same-character delimiters across word
boundaries —
__a__b__or*a*b*. The flanking rules can pair these multiple ways depending on surrounding whitespace. Different parsers have historically disagreed on these. - Escaped emphasis adjacent to delimiter runs —
*\*x*,*****\*a*. Backslash-escapes inside a long run interact with the flanking scan in ways no human reads correctly. This is also the shape the markdown-it ReDoS CVE exploited.
Each pattern is a static check on the source bytes. The AST is not consulted because goldmark has already collapsed the ambiguity.
Goldmark resolves these patterns to a definite AST. Walking the AST tells the rule what the parser chose, not whether a human could have predicted that choice. The rule scans the raw source line for the suspicious shapes instead.
MDS042 (emphasis-style) pins which delimiter is used. MDS047 catches the ambiguous combinations that survive a delimiter pin. They can fire independently on the same line.
rules:
ambiguous-emphasis:
max-run: 2 # delimiter run length cap
forbid-escaped-in-run: true
forbid-adjacent-same-delim: trueCategory: meta. Disabled by default (opt-in).
Plan 112 ships profiles that auto-enable this rule:
profile: portableactivates withmax-run: 2,forbid-escaped-in-run: true, andforbid-adjacent-same-delim: true.profile: githubdoes not activate this rule.profile: plainactivates with the same settings asportable.
User overrides on top of the profile still win via deep-merge.
Read the source line by line, skipping ranges
covered by *ast.CodeSpan, *ast.FencedCodeBlock,
and *ast.CodeBlock.
For each non-code range:
- Find every contiguous run of
*or_. If the run length exceedsmax-run, emit one diagnostic. - If the run contains a backslash-escaped
*or_adjacent to it (*\*or_\_) andforbid-escaped-in-runis true, emit one diagnostic. - Find
<delim>word<delim>word<delim>patterns where the same single-character delimiter appears three times on the line with non-whitespace between them. Whenforbid-adjacent-same-delimis true, emit one diagnostic.
No auto-fix. The right rewrite depends on author intent, which the rule cannot recover. The diagnostic message suggests adding a space, an HTML entity, or splitting the run.
emphasis run of {n} delimiters; max is {max-run}
escaped delimiter inside emphasis run
adjacent same-delimiter emphasis is ambiguous
- Scaffold
internal/rules/ambiguousemphasis/. - Implement source-range computation that excludes code spans and code blocks.
- Implement the three pattern detectors.
- Implement
rule.Configurableformax-run,forbid-escaped-in-run, andforbid-adjacent-same-delim. - Register as MDS047 in category
meta. - Add fixture tests covering each pattern,
patterns inside code spans (must not flag),
patterns inside fenced code blocks (must not
flag), and the rant's exact strings
(
*****\*a*,***Peter* Piper**). - Add rule README.
-
**bold**emits no diagnostic. -
***bold-italic***emits one diagnostic whenmax-run: 2. The two symmetric***runs collapse into a single report by deduplicating diagnostics per(char, length)per line. -
*****\*a*emits diagnostics for both run length and escaped-in-run. -
__a__b__emits one adjacent-same-delim diagnostic. - The same patterns inside
`code`or a fenced block emit no diagnostic. - No auto-fix is attempted.
- Rule is disabled by default.
DefaultSettingsshipsmax-run: 0and both bool flagsfalseso the rule remains a no-op in any pipeline that wires it without explicit settings; profile activation in plan 112 supplies the active values. - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues