fix: measure MD038's code span against its own delimiter - #402
Merged
Conversation
`MD038` compared `code.literal` against a content length derived from the span's columns: `end.column - start.column - 1`. That arithmetic assumes a one-backtick delimiter, so for a span delimited by N backticks it overshoots by `2N - 2` and the comparison can never hold. Every span written with two or more backticks was reported, and there is no way to author around it — two backticks are how `CommonMark` writes a span that contains one. The same arithmetic subtracts columns belonging to two different lines when a span is broken across them, which reported those too. Read the content out of the source instead. `sourcepos` covers the delimiters and comrak carries `num_backticks`, so the content is what is left after dropping that many bytes from each end of a single line's span; a span crossing lines is skipped, as `markdownlint` skips it. That leaves what counts as padding. `CommonMark` removes one space from each end of a span that both begins and ends with one, which is the only way to write a span that starts or ends with a backtick — ``` `` ` `` ``` — so a removed pair is reported only when it shields something other than a backtick, and any space the removal leaves behind is reported as before. The three fixtures upstream marks for this rule are unaffected: `spaces_inside_codespan_elements.md` still reports on the same lines, including the two its own text says kramdown cannot see. Closes #391. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWNoFSfDhBzXEX9UL68oqf
Remove `.issue-346-comment.md`, an unrelated draft `git add -A` swept into the previous commit. It was linted along with everything else, which is why four `tests/command_check.rs` cases failed. Measure the span rather than slice the line. `sourcepos` is not always an index into `doc.lines`, and reading the line back was wrong twice over: - comrak unescapes a table cell before parsing its inlines, so every `\|` earlier in the cell shifts the columns after it by a byte. `| x\|y `` pad `` |` went unreported, and a slice could as easily read padding that is not there. - comrak measures an inline against the paragraph's content, so a continuation line stripped of its indentation moves the columns off the line as well. Both survive a width, because the two ends shift together: the span is `2 * num_backticks` bytes wider than its content, and against `literal` that width is equal or two bytes longer — the pair `CommonMark` removes. On the GitLab corpus this drops MD038 from 56 reports to none; all 56 were spans with no padding in them. A span whose delimiters sit on different lines has no width to take, so it is still read out of the lines — no table cell reaches across a line ending — but only after the delimiters are found where `sourcepos` says they are. Without that check `doc/migrate_ci_to_ce/README.md:134` reported padding its source does not contain. Where the check fails the span goes unjudged, which is a miss rather than a report its author cannot act on. Padding written across a line ending is otherwise reported now, where the previous commit skipped every such span. Justify the rule's decisions on their own terms rather than by citing another linter, here and in the CHANGELOG. Tests: multiple backticks in a table cell after `\|`, a padded span across lines, one the indentation check declines, and a padded span behind a multibyte prefix — which previously asserted an empty result and so proved nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWNoFSfDhBzXEX9UL68oqf
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #402 +/- ##
========================================
Coverage 99.78% 99.79%
========================================
Files 72 72
Lines 7485 7715 +230
========================================
+ Hits 7469 7699 +230
Misses 16 16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`same_line_content` rejected a width `CommonMark` cannot produce, which no document reaches and no test could, so codecov reported the line missing and both its gates went red on a project that sits at 99.78%. The rejection was not earning its place. `CommonMark` takes a space off each end of a code span or nothing at all, so the width tells those two apart on its own; a width outside that pair is not a third case to decide between but an assumption that did not hold, and judging `literal` as it stands is the honest answer to it — it reports what a reader can see and nothing more. `src/rule/md038.rs` is back to 100% line coverage. Behaviour is unchanged: the GitLab corpus, markdownlint's fixtures and every case in #391 report exactly what they did before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWNoFSfDhBzXEX9UL68oqf
`multi_line_content` read the content back out of `doc.lines`. Review found two
more documents it gets wrong, on top of the one it was written to handle:
``
`x`
``
Both ends are empty, so joining them rebuilds the content as a single space,
`strip_padding` reads that as nothing but spaces, and the span is reported —
a span whose spaces are the only way `CommonMark` gives to write it, which
`is_padded` documents as the one thing this rule must not do. The content was on
the line between the delimiters, which no slice of the two end lines contains.
> `text
>`
The `>` sits between the start of the line and the closing delimiter, so it
lands inside the slice taken up to that column and stands in for the line ending
that is really there. The padding goes unreported. `> `text` / `> `` is the same
document a byte at a time and does report, because `> ` happens to end in a
space.
That is four ways this reconstruction fails, each found in a real document and
none suggested by the last. Nothing recorded about a multi-line span describes
what was written against its delimiters: `literal` has already turned the line
endings into spaces and taken the padding off, and a width cannot be had from
columns that index two different lines. So the span is skipped, and the doc
comment says which four shapes made that the answer.
The cost is a miss on padding written across a line ending. The alternative is
reporting spans no author can act on, which is what #391 is about, so the miss
is the better half of the trade. Tests cover all four shapes and the missed one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWNoFSfDhBzXEX9UL68oqf
This was referenced Sep 5, 2026
Three of these assertions record what mado does rather than what it should, and read as intentional without saying so. `check_errors_with_escaped_pipe_in_table` asserts column 7 for a backtick that is at column 8. comrak unescapes a table cell before parsing its inlines, so every `\|` earlier in the cell moves the recorded column one to the left. The rule forwards `sourcepos` and the shift comes with it; MD037 reports the same row short by one, while MD039 does not, so this is comrak's and not MD038's. Now noted against #403. `check_errors_with_only_spaces` justified itself with "a space the author can see and remove", which is only true if deleting the span counts as a fix. A span of one space is the only way to write a code span that renders as one, so this is the shape this rule was just fixed for, reported anyway because that is what mado has always done here. The comment now says so instead of arguing one side. The multi-line skip points at #404, which covers the spans `literal` can still prove padding on by itself — two spaces at an end, where a line ending can only account for one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWNoFSfDhBzXEX9UL68oqf
Review asked whether a space between a delimiter and an embedded backtick should
be exempt the way the removed pair is, since it cannot be deleted either:
`` ` embedded``
``embedded ` ``
It should not, and the rendering says why. comrak returns
`" ` embedded"` and `"embedded ` "` for those two — the space is in the output,
because the other end has none for `CommonMark` to pair it with and nothing was
removed. That is the opposite of ``` `` ` `` ```, where the pair comes off and
the reader sees a bare backtick.
Deleting the space is indeed not the fix; supplying the missing one is.
`` ` embedded ` `` renders as `` `embedded` ``, and the test carries all three
so the difference is visible in one place. So the report is actionable, which is
the line this rule was fixed along: ``` ``ok`` ``` had no space anywhere and
nothing to act on, and this has a space the reader can see.
The exception is the removal, not the backtick, and the doc comment now says
that rather than leaving it to be inferred from "a pair of spaces".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWNoFSfDhBzXEX9UL68oqf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MD038comparedcode.literalagainst a content length derived from the span'scolumns:
That arithmetic assumes a one-backtick delimiter. For a span delimited by N
backticks it overshoots by
2N - 2, socode.literal.len() != content_lencannever hold and every such span was reported. There is no way to author around
it: two backticks are how
CommonMarkwrites a span that contains one.What this changes
Measure the span rather than slice the line.
sourceposcovers the delimitersand comrak carries
num_backticks, so the content is2 * num_backticksbytesnarrower than the span; against
literalthat width is either equal — nothingwas removed — or two bytes longer, which is the one space
CommonMarktakes offeach end.
A width is used rather than the line itself because
sourceposis not always anindex into it. comrak unescapes a table cell before parsing its inlines, so
inside one every
\|earlier in the cell shifts the columns after it by a byte.A width survives that, because both ends shift together.
That leaves what counts as padding.
CommonMarkremoves one space from each endof a span that both begins and ends with one, and that pair is the only way to
write a span that starts or ends with a backtick:
So a removed pair is reported only when it shields something other than a
backtick, and any space the removal leaves behind —
`` wide ``— isreported as before.
Behavior
Every line of the report from #391, before and after:
`ok```ok````a `b` c`````lit```` pad `A span whose delimiters sit on different lines is not judged
Nothing recorded about one describes what was written against its delimiters.
literalhas already turned the line endings into spaces and taken the paddingoff, and a width cannot be had from columns that index two different lines.
Reading the lines back was tried and fails four ways, each found in a real
document:
indentation, and point past the closing delimiter
>or list marker lands inside the slice and stands in for theline ending that is really there
padding on a span whose spaces are the only way to write it
So the span is skipped. The cost is a miss on padding written across a line
ending; the alternative is reporting spans no author can act on, which is what
this pull request is about.
Verification
cargo test --all-features --workspace,cargo clippy --all-targets --all-features --workspace -- -D warningsandcargo fmt --all --checkallpass.
src/rule/md038.rskeeps 100% line coverage. The two tests this rulealready had are unchanged; twelve cases were added for multiple backticks,
backtick content, a table cell after
\|, a multibyte prefix, a span ofnothing but a space, and each of the four multi-line shapes above.
mado check .on this repository still passes.scripts/benchmarks/data— 10k+ files — MD038 goesfrom 56 reports to none. Every one of the 56 was a span with no padding in it.
scripts/acceptance/data, MD038 reportsthe same three lines of
spaces_inside_codespan_elements.mdas it did before.Closes #391.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JWNoFSfDhBzXEX9UL68oqf