Fix: cluster of formatting bugs surfaced by Rails idioms#102
Merged
sorafujitani merged 1 commit intomainfrom Apr 24, 2026
Merged
Fix: cluster of formatting bugs surfaced by Rails idioms#102sorafujitani merged 1 commit intomainfrom
sorafujitani merged 1 commit intomainfrom
Conversation
A sweep across common Rails-style Ruby code (controllers, models, routes,
concerns, services, jobs, migrations, specs, factories, endless defs,
heredocs, rescue modifiers, block comments) turned up several latent
bugs. This change fixes all of them in one pass and ships regression
coverage.
A. Trailing whitespace on blank lines
Every class / module / def body turned `""` blank lines into
`"<indent>"` (two trailing spaces), tripping RuboCop
Layout/TrailingWhitespace on virtually every formatted file. The
printer's `hardline` always writes `"\n" + indent_str`, so two
consecutive hardlines leak the indent onto the line between them.
Post-process the final buffer to strip per-line trailing spaces and
tabs.
C. Ruby 3+ endless defs and the inline one-liner idiom
`def active? = status == "active"`, `def simple; 1; end`, and
`class Error < StandardError; end` (pervasive in Rails exception
hierarchies) were all expanded into multi-line `def … end` /
`class … end` forms. `format_body_end` now emits such single-line
constructs verbatim when `start_line == end_line`, preserving both
the endless-def syntax and the compact one-liners.
D. Method-chain + block body indentation
A chain that ends with `.foo do |x| … end` printed the block body
one indent level below the `.foo` receiver (bodies aligned with the
chain start instead of nesting under the chain's last line; `end`
floated above the opener). When `reformat_chain_lines` re-indents a
chain, wrap the block doc in `Align(indent_width, …)` so the body
and matching `end` sit one level under the chain's tail.
E. Spurious blank lines around heredocs
Two symptoms, one cause:
- `foo(<<~SQL)\n…\nSQL` as the last statement in a body produced
a blank line before `end`.
- `CONST = <<~TEXT\n…\nTEXT\nNEXT = …` swallowed or duplicated
separators between consecutive heredoc-valued constants.
Two-part fix:
- Source-extracting rules (Call, Fallback, VariableWrite) now
strip at most one trailing `\n` from the extracted slice via
a shared `strip_one_trailing_newline` helper — the trailing
newline Prism includes past the terminator was compounding with
the surrounding hardline. Using the full `trim_end` would
instead eat a blank separator that legitimately belongs inside
the node's range.
- The bridge's heredoc `closing_loc` projection was reporting
`<terminator_line + 1>:0` as the end, which made
`format_statements`' `line_diff` check treat the blank line
after the terminator as "adjacent", collapsing the separator.
Snap the reported `end_line` back to the terminator's own line.
B1. Comment immediately before `rescue`/`ensure`
An annotation like `# rescue if auth fails` written on the line
before `rescue` used to get picked up as a leading comment of the
first statement *inside* the rescue body, visually moving the
annotation into the wrong branch. `format_rescue` now consumes
leading comments attached to the rescue line itself.
B2. Blank-line accounting in statement gaps
Two issues resolved together:
- A standalone `# note` comment on the line between two
statements was counted as a blank line (line_diff > 1),
producing a spurious blank line between the comment and the
statement below. `format_statements` and the top-level
`format_children_with_spacing` now subtract comment-occupied
lines before deciding whether to emit a blank-line hardline.
- `format_comments_before_end` used to pass `hard_line_after:
true` even for the last comment in the run, which combined
with the caller's own `hardline + "end"` to produce a blank
line between the comment and `end`. Drop the trailing
hardline for the final entry.
B3. `=begin … =end` block comments
Embdoc comments were being mis-attached and mis-indented:
- The bridge inherited Prism's convention that an embdoc's
end_line sits at `<line after =end>:0`. That overlapped the
next statement and caused the comment to re-attach to a
deeper node, sometimes hoisting a comment that lived *inside*
a class out to the program body. Clamp the reported end_line
back to the `=end` line.
- Once attached correctly, emitting the comment through the
normal leading-comment path printed `=begin` at the surrounding
indent, which is a *syntax error* (embdocs must live in
column 0). Route block comments through `literalline + text +
hardline` so they break out to column 0. Strip the trailing
newline inside the comment slice and deduct one blank-line
hardline from the surrounding gap so the `literalline` doesn't
double up.
Tests
- spec/rails_idioms_spec.rb exercises every category above as a
round-trip + idempotence check: inline def / class / endless def,
blank-line ws, chain + block body alignment, heredoc in a call
argument, heredoc between constants, comment-in-gap, trailing
body comment, and embdoc attachment/position.
- All 127 cargo unit tests and 119 rspec examples (100 existing +
19 new) pass. `cargo clippy -D warnings`, `cargo fmt --check`,
and `rubocop` are clean.
Out of scope
- The aligned → indented chain reformat introduced in PR #100 still
fires for plain assignments (`@x = User.chain.chain`). That's
existing design and should be guarded by a config option in a
follow-up.
- A second and third embdoc in the same file can still leave one
extra blank line between them; uncommon enough to defer.
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.
Summary
A systematic pass across Rails-style Ruby (controllers, models, routes, concerns, services, jobs, migrations, specs, factories, endless defs, heredocs, rescue modifiers, block comments) surfaced several latent formatter bugs. This PR fixes them all in one shot and ships regression coverage.
Bugs fixed
Layout/TrailingWhitespaceeverywhere).hardlinealways writes"\n" + indent_str; two hardlines in a row leak the indent onto the blank line. Post-process the printer output to strip per-line trailing spaces/tabs.def active? = status == "active"), inline methods (def simple; 1; end), and inline exception-hierarchy classes (class Error < StandardError; end) were always expanded into multi-linedef … end/class … end.format_body_end, whenstart_line == end_line, emit the source slice verbatim instead of forcing the<header><newline><body><newline>endlayout.endafter a heredoc argument (foo(<<~SQL) … SQL end) and dropped separator line between two heredoc-valued constants.\nvia a newstrip_one_trailing_newlinehelper — a fulltrim_endwould eat a blank separator that legitimately belongs inside the node's range. (2) The bridge's heredocclosing_locprojection was reporting<terminator_line + 1>:0as the end, soformat_statements'line_difftreated the blank line after the terminator as "adjacent". Snap the reportedend_lineback to the terminator's own line.# note on the rescuecomment written just beforerescuewas absorbed as a leading comment of the first statement inside the rescue body.format_rescuenow claims leading comments of the rescue line itself.format_comments_before_end's last entry ended withhard_line_after: trueand left a blank line beforeend.format_statements/format_children_with_spacingsubtract comment-occupied lines from the blank-line count, and deduct one more if the gap contains a block comment (whoseliterallinealready supplies a break). (2) Drop the trailing hardline on the final comment returned byformat_comments_before_end.=begin … =endcomments were attributed to the wrong node (often hoisted out of the enclosing class) and, once attached, printed=beginat the surrounding indent — a syntax error.end_linefrom<end>:0back to the=endline so it no longer overlaps the next statement. (2) Block comments emit throughliteralline + text + hardlineso=begin/=endland at column 0. Strip the trailing newline in the slice and deduct one blank-line hardline from the surrounding gap so theliterallinedoesn't double up.Before / after highlights
Changes
ext/rfmt/src/doc/printer.rs— per-line trailing-whitespace strip (strip_trailing_line_whitespace).ext/rfmt/src/format/rule.rs—push_leading_commentblock-comment path;strip_one_trailing_newlinehelper; comment-aware blank-line accounting informat_statements; last-comment tail suppression informat_comments_before_end; extendedCommentRefwithis_block.ext/rfmt/src/format/formatter.rs— same comment-aware blank-line accounting for the program-root / statements-root loop.ext/rfmt/src/format/rules/body_end.rs— single-line construct shortcut (C).ext/rfmt/src/format/rules/begin.rs— leading comments on rescue (B1).ext/rfmt/src/format/rules/call.rs—Align(indent_width, …)around chain-with-block; heredoc tail trim (D, E).ext/rfmt/src/format/rules/fallback.rs,…/variable_write.rs— same heredoc tail trim for the other source-extracting paths (E).lib/rfmt/prism_bridge.rb—end_lineclamp for heredocclosing_locand embdoc comments (E, B3).spec/rails_idioms_spec.rb— 12 new end-to-end + idempotence tests, one per category.Test plan
cargo test --manifest-path ext/rfmt/Cargo.toml --lib— 127 passedbundle exec rspec— 119 passed (100 existing + 19 new)cargo clippy -- -D warnings— cleancargo fmt --check— cleanbundle exec rubocop— cleanOut of scope
@x = User.chain.chain). That is existing design and deserves a config switch, which I'd like to split off.=begin … =endcomments appear in the same file with additional blank lines between them, one extra blank line can still slip in between consecutive embdocs. Block comments are uncommon enough that this can be tightened in a follow-up.