Add MDS064 rule for ATX heading whitespace and indentation - #347
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files
☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds the new MDS059 (atx-heading-whitespace) rule to mdsmith, implementing plan 176 by detecting and auto-fixing ATX heading spacing, optional closing-marker formatting, and heading indentation.
Changes:
- Added MDS059 rule implementation plus unit tests.
- Added rule documentation and integration fixtures (good/bad/fixed) and registered the rule for integration testing.
- Updated plan tracking and the markdownlint coverage matrix to reflect the new coverage.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| plan/176_atx-heading-whitespace.md | Marks plan 176 as complete and checks off acceptance criteria. |
| PLAN.md | Updates the plan index row for 176 to ✅. |
| internal/rules/MDS059-atx-heading-whitespace/README.md | Adds rule documentation and configuration guidance. |
| internal/rules/MDS059-atx-heading-whitespace/good/default.md | Adds “good” integration fixture coverage. |
| internal/rules/MDS059-atx-heading-whitespace/bad/*.md | Adds “bad” fixtures with expected diagnostics for each violation type. |
| internal/rules/MDS059-atx-heading-whitespace/fixed/*.md | Adds “fixed” fixtures asserting autofix output. |
| internal/rules/index.md | Registers MDS059 in the rules index catalog. |
| internal/rules/headingwhitespace/rule.go | Implements the MDS059 check + fix logic. |
| internal/rules/headingwhitespace/rule_test.go | Adds unit tests covering diagnostics and fix behavior. |
| internal/integration/rules_test.go | Registers the new rule package in the integration test harness. |
| docs/research/markdownlint-coverage/README.md | Updates the markdownlint coverage table to map MD018–MD021 and MD023 to MDS059. |
38e07b3 to
60bff34
Compare
Implements rule MDS059 covering the markdownlint ATX-heading whitespace family (MD018–MD021) and heading indentation (MD023). Detects missing space, multiple spaces, closed ATX markers, and leading indentation on raw ATX heading lines. Autofix normalizes every defect to open ATX with a single space and no leading indent. Skips fenced/indented code blocks and directive bodies. Closes plan 176. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
Three statement-coverage gaps in rule.go — the all-hash guard in checkClosingATX, the level-out-of-range guard in normalizeLine, and the empty-string early return in extractContent — are only reachable by calling the unexported functions directly. Add rule_coverage_test.go to hit all three paths and reach 100% statement coverage. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
- Add \r to TrimRight calls so CRLF line endings never produce spurious "missing space after # in heading" diagnostics on empty ATX headings (##\r\n). - Flag a tab immediately after opening hashes as "missing space" and normalise it to a single space (#\tHeading → # Heading). - Only treat a trailing # run as a closing ATX marker when preceded by whitespace (CommonMark rule). Previously, content like "# C#" was wrongly diagnosed and mangled by Fix. The case with no preceding space now returns nil cleanly. Consequence: #Heading# is fixed only for MD018 (missing opening space) → # Heading#; MD020 coverage downgraded to partial in the coverage matrix. - Update rule README to reflect accurate behaviour. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
- Coverage matrix: correct preamble to 41 of 52 (2 partial), 11 remaining; remove completed plan 176 from plan list. - normalizeLine: preserve trailing \r when the original line has one so CRLF files don't get mixed LF/CRLF after a partial fix. Test: TestFix_PreservesCRLFOnRewrittenLines. - Add TestCheck_SkipsPIBlock and TestFix_PreservesPIBlock to pin the directive-body (PI block) skip behaviour for Check and Fix. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
6cdfb2f to
8f3c3cd
Compare
shutdown() used request(), which reads exactly one frame. After a rename the server emits publishDiagnostics notifications that can arrive before the shutdown response, causing the id==99 assertion to see nil. Switch to requestPickResult(), which already loops past interleaved server frames, matching the pattern used by every other multi-step LSP test helper. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
internal/rules/headingwhitespace/rule.go:196
extractContenttreats trailing hashes without preceding whitespace as content and therefore normalizes#Heading#to# Heading#(retaining the trailing#). If the intent is to normalize closed-ATX headings to open style (plan 176), this needs to strip the trailing hash run for the MD020 case while still preserving legitimate content like# C#.
// Trailing hashes not preceded by whitespace are content, not a closing marker.
if s[hashStart-1] != ' ' && s[hashStart-1] != '\t' {
return s
}
return strings.TrimRight(s[:hashStart], " \t")
Split the acceptance criterion that covered both `# Heading #` and `#Heading#` into separate bullets. The `#Heading#` bullet now explicitly notes that per CommonMark a trailing `#` without preceding whitespace is content, not a closing marker, so MD020 is partial for that case. This matches the implementation and resolves the Copilot review comment about the checked-off criterion contradicting the fix output. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
…ading" leadingSpaces() counts tabs as well as spaces, so the check at line 71 fires for mixed whitespace like "# \tHeading" (space then tab). The old message "multiple spaces after # in heading" was inaccurate in that case. Rename to "multiple spaces or tabs after # in heading" and add tests for the space+tab pattern. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
plan/176_atx-heading-whitespace.md:8
- The plan front matter still describes this as a provisional “MDS060” rule (and likely has other MDS060 references elsewhere). Since the PR implements MDS064, update the plan text so the recorded design/summary matches what was actually shipped.
status: "✅"
model: sonnet
depends-on: []
summary: >-
New rule MDS060 (provisional) covering the markdownlint
… register in all.go The package name headingwhitespace did not follow the repo convention of deriving the directory name from the rule name with hyphens removed (blockquote-whitespace → blockquotewhitespace, list-marker-space → listmarkerspace). Rename to atxheadingwhitespace for consistency. Also fixes a production bug: the package was never blank-imported in internal/rules/all/all.go, so MDS064 was not registered and would never run in cmd/mdsmith. Add the import in alphabetical order. Also updates plan/176 front matter and design text to replace the provisional MDS060 ID with the shipped MDS064. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
…ssue refs Lines like " #22 \"Mandatory headings\"" and " #288.**" are GitHub issue/PR references soft-wrapped inside list items, not malformed ATX headings. Computing `after` before the MD023 check and returning nil when after[0] is a digit avoids flagging these lines. Add TestCheck_IssueReference, TestCheck_IssueReferenceAtColumn1, and TestFix_IssueReferenceUnchanged to pin the behaviour. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
The import was left at the position where headingwhitespace used to sit (after headingstyle) instead of its correct alphabetical slot after ambiguousemphasis. Moves it so gofmt/goimports is satisfied. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
… content The README said the opening hashes must be followed by "exactly one space" without qualification, implying empty headings like "##" are invalid. Reword to make clear the space check only applies when the heading has content; empty headings are valid and produce no diagnostic. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
The previous guard returned nil for any '#'+digit line regardless of indentation. That silenced MD018 for genuine malformed headings like #1Heading or ##22Title at column 1. Scope the guard to leading>0: a '#'+digit run on an indented line is almost certainly a soft-wrapped issue/PR reference (#22, #288) — at column 1 it is a missing-space defect and is flagged normally. Add TestCheck_DigitAtColumn1IsFlagged and TestFix_DigitAtColumn1 to pin the column-1 behaviour and prevent regression. https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz
Implements the MDS064 rule to check and fix ATX heading whitespace and indentation issues, completing plan 176.
Summary
This PR adds a new linting rule that validates ATX heading formatting, including:
#markers (MD018, MD019)#markers preceded by whitespace (MD020/MD021 — see note below)The rule provides automatic fixes that normalize headings to
# Content(open ATX style).Key Changes
New rule MDS064 (
internal/rules/atxheadingwhitespace/rule.go):#Heading) → fixes to# Heading# Heading) → fixes to# Heading#\tHeading) → fixes to# Heading# Heading #,# Heading #) → strips the closing marker#+digit on indented lines (avoids false positives on issue/PR references like#22in list continuations)MD020 note (
#Heading#): Per CommonMark, a trailing#run is only a closing ATX marker when preceded by whitespace.#Heading#has no space before the trailing#, so it is treated as content — MD018 fires (missing opening space), fix produces# Heading#. MD020 is therefore marked partial in the coverage matrix. This is intentional and matches CommonMark's spec.Comprehensive test suite (
internal/rules/atxheadingwhitespace/rule_test.go)Fixture tests under
internal/rules/MDS064-atx-heading-whitespace/Rule README with description, examples, and metadata
Updated markdownlint coverage matrix and rule index
Plan 176 marked complete
https://claude.ai/code/session_01KCANC7X1jkYcLdAUqmBeaz