Linter Rule: Prefer do ... end over { ... } for blocks that span multiple ERB tags
Rule: erb-prefer-do-end-blocks
Description
Disallow { ... } as the delimiters of a block that is opened in one ERB tag and closed in another.
<% @users.each { |user| %> <p><%= user.name %></p>
<% } %>Rationale
A block that spans ERB tags is a multi-line block, and do ... end is the convention for those in Ruby. In a template the argument is stronger than in plain Ruby: every other construct that wraps markup, like if, unless, case, or each with do, closes with <% end %>, so a lone <% } %> is the one closing tag that has to be read differently from the rest of the file.
The braces are also easy to lose. <% } %> is a single character of signal surrounded by ERB punctuation, and it looks the same whether it closes a block, a hash, or an interpolation, whereas <% end %> says what it does.
A brace block written entirely inside a single ERB tag is not reported. <%= @users.map { |user| user.name }.join(", ") %> stays on one line and reads fine, which is also what RuboCop's Style/BlockDelimiters prefers for single-line blocks.
Autofix
This rule provides an autofix that replaces { with do and the matching <% } %> with <% end %>:
<%= form_with(model: @user) { |form| %> <%= form.submit %>
<% } %><%= form_with(model: @user) do |form| %>
<%= form.submit %>
<% end %>{ binds to the closest method call while do binds to the outermost one, so swapping the delimiters only preserves the meaning when both are the same call. In <% puts @users.map { |user| %> they aren't: the block belongs to map, but a do would hand it to puts, which then prints an Enumerator instead of the mapped values.
Whenever the block is owned by anything other than the tag's outermost call, no fix is offered. The rewritten template would still run and quietly do something else, which is worse than leaving it alone. The offense is still reported, and the rewrite is left to you.
Examples
✅ Good
<% @users.each do |user| %>
<p><%= user.name %></p>
<% end %><%= form_with(model: @user) do |form| %>
<%= form.submit %>
<% end %><%= @users.map { |user| user.name }.join(", ") %>🚫 Bad
<% @users.each { |user| %> <p><%= user.name %></p>
<% } %><%= form_with(model: @user) { |form| %> <%= form.submit %>
<% } %><% @users.each { %> <p>Hello</p>
<% } %>