Skip to content

fix: measure MD038's code span against its own delimiter - #402

Merged
akiomik merged 6 commits into
mainfrom
fix-md038-multi-backtick-code-spans
Sep 5, 2026
Merged

fix: measure MD038's code span against its own delimiter#402
akiomik merged 6 commits into
mainfrom
fix-md038-multi-backtick-code-spans

Conversation

@akiomik

@akiomik akiomik commented Sep 5, 2026

Copy link
Copy Markdown
Owner

MD038 compared code.literal against a content length derived from the span's
columns:

let content_len = position.end.column - position.start.column - 1;

That arithmetic assumes a one-backtick delimiter. For a span delimited by N
backticks it overshoots by 2N - 2, so code.literal.len() != content_len can
never hold and every such span was reported. There is no way to author around
it: two backticks are how CommonMark writes a span that contains one.

What this changes

Measure the span rather than slice the line. sourcepos covers the delimiters
and comrak carries num_backticks, so the content is 2 * num_backticks bytes
narrower than the span; against literal that width is either equal — nothing
was removed — or two bytes longer, which is the one space CommonMark takes off
each end.

A width is used rather than the line itself because sourcepos is not always an
index 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. CommonMark removes one space from each end
of 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 `` — is
reported as before.

Behavior

Every line of the report from #391, before and after:

Input Expected Before After
`ok` clean clean clean
``ok`` clean MD038 clean
``a `b` c`` clean MD038 clean
```lit``` clean MD038 clean
` pad ` MD038 MD038 MD038

A span whose delimiters sit on different lines is not judged

Nothing recorded about one 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.
Reading the lines back was tried and fails four ways, each found in a real
document:

  • the columns shift once a continuation line has been stripped of its
    indentation, and point past the closing delimiter
  • the lines between the delimiters are not part of any slice of the two ends
  • a container's > or list marker lands inside the slice and stands in for the
    line ending that is really there
  • a span whose ends are both empty rebuilds as a single space, which reads as
    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 warnings and cargo fmt --all --check all
    pass. src/rule/md038.rs keeps 100% line coverage. The two tests this rule
    already had are unchanged; twelve cases were added for multiple backticks,
    backtick content, a table cell after \|, a multibyte prefix, a span of
    nothing but a space, and each of the four multi-line shapes above.
  • mado check . on this repository still passes.
  • On the GitLab corpus under scripts/benchmarks/data — 10k+ files — MD038 goes
    from 56 reports to none. Every one of the 56 was a span with no padding in it.
  • Against the acceptance fixtures under scripts/acceptance/data, MD038 reports
    the same three lines of spaces_inside_codespan_elements.md as it did before.

Closes #391.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JWNoFSfDhBzXEX9UL68oqf

akiomik and others added 2 commits September 5, 2026 12:42
`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

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.79%. Comparing base (63deb47) to head (03c2bc6).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

akiomik and others added 2 commits September 5, 2026 13:16
`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
akiomik and others added 2 commits September 5, 2026 14:46
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
@akiomik akiomik changed the title fix: read MD038's code span from the source fix: measure MD038's code span against its own delimiter Sep 5, 2026
@akiomik
akiomik merged commit 6479490 into main Sep 5, 2026
18 checks passed
@akiomik
akiomik deleted the fix-md038-multi-backtick-code-spans branch September 5, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MD038 false-positives on every multi-backtick code span

1 participant