Skip to content

Linter: Implement erb-prefer-do-end-blocks rule - #1973

Merged
marcoroth merged 2 commits into
mainfrom
erb-prefer-do-end-blocks
Aug 3, 2026
Merged

marcoroth merged 2 commits into
mainfrom
erb-prefer-do-end-blocks

Conversation

@marcoroth

@marcoroth marcoroth commented Aug 3, 2026 •

Copy link
Copy Markdown
Owner

This pull request implements erb-prefer-do-end-blocks, a new rule that flags a block opened with { in one ERB tag and closed with <% } %> in another, and points at the do ... end form instead.

<% @users.each { |user| %>
  <p><%= user.name %></p>
<% } %>
Avoid using `{ ... }` for a block that spans multiple ERB tags. Use `do ... end` instead.

A block that spans ERB tags is a multi-line block, and do ... end is the typical 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, since <% } %> is a single character of signal surrounded by ERB punctuation and looks the same whether it closes a block, a hash, or an interpolation.

A brace block written entirely inside one ERB tag is not reported, so <%= @users.map { |user| user.name }.join(", ") %> stays as it is.

Autofix

The rule is autocorrectable. The fix replaces { with do in the opening tag and } with end in the closing tag, keeping trim markers intact and inserting the missing space for {|user|:

<%= form_with(model: @user) { |form| %>
  <%= form.submit %>
<% } %>
<%= form_with(model: @user) do |form| %>
  <%= form.submit %>
<% end %>

Swapping the delimiters is not unconditionally safe, because { binds to the closest method call while do binds to the outermost one. The fix is marked safe only when both are the same call, which it checks on the Prism node: the outermost node has to be a CallNode that owns the block, after unwrapping an assignment so that <% names = @users.map { |user| %> still qualifies.

<% puts @users.map { |user| %>
  <p><%= user.name %></p>
<% } %>

Here the block belongs to map, but a do would hand it to puts, so the offense is still reported while no autofix is offered.

@github-actions github-actions Bot added documentation Improvements or additions to documentation linter @herb-tools/linter for HTML+ERB templates typescript TypeScript source across the javascript/ packages linter-rule Individual linter rules and their documentation linter-autofix Linter autofix behavior labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

🌿 Interactive Playground and Documentation Preview

A preview deployment has been built for this pull request. Try out the changes live in the interactive playground:


🌱 Grown from commit c4746a3


✅ Preview deployment has been cleaned up.

@pkg-pr-new

pkg-pr-new Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
npx https://pkg.pr.new/@herb-tools/formatter@1973
npx https://pkg.pr.new/@herb-tools/language-server@1973
npx https://pkg.pr.new/@herb-tools/linter@1973

commit: c4746a3

@marcoroth
marcoroth merged commit b34f834 into main Aug 3, 2026
22 checks passed
@marcoroth
marcoroth deleted the erb-prefer-do-end-blocks branch August 3, 2026 11:57
joaoGabriel55 pushed a commit to joaoGabriel55/herb that referenced this pull request Aug 3, 2026
This pull request implements `erb-prefer-do-end-blocks`, a new rule that
flags a block opened with `{` in one ERB tag and closed with `<% } %>`
in another, and points at the `do ... end` form instead.

```erb
<% @users.each { |user| %>
  <p><%= user.name %></p>
<% } %>
```

```
Avoid using `{ ... }` for a block that spans multiple ERB tags. Use `do ... end` instead.
```

A block that spans ERB tags is a multi-line block, and `do ... end` is
the typical 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, since `<% } %>` is a single character of
signal surrounded by ERB punctuation and looks the same whether it
closes a block, a hash, or an interpolation.

A brace block written entirely inside one ERB tag is not reported, so
`<%= @users.map { |user| user.name }.join(", ") %>` stays as it is.

#### Autofix

The rule is autocorrectable. The fix replaces `{` with `do` in the
opening tag and `}` with `end` in the closing tag, keeping trim markers
intact and inserting the missing space for `{|user|`:

```erb
<%= form_with(model: @user) { |form| %>
  <%= form.submit %>
<% } %>
```

```erb
<%= form_with(model: @user) do |form| %>
  <%= form.submit %>
<% end %>
```

Swapping the delimiters is not unconditionally safe, because `{` binds
to the closest method call while `do` binds to the outermost one. The
fix is marked safe only when both are the same call, which it checks on
the Prism node: the outermost node has to be a `CallNode` that owns the
block, after unwrapping an assignment so that `<% names = @users.map {
|user| %>` still qualifies.

```erb
<% puts @users.map { |user| %>
  <p><%= user.name %></p>
<% } %>
```

Here the block belongs to `map`, but a `do` would hand it to `puts`, so
the offense is still reported while no autofix is offered.
joaoGabriel55 pushed a commit to joaoGabriel55/herb that referenced this pull request Aug 3, 2026
This pull request implements `erb-prefer-do-end-blocks`, a new rule that
flags a block opened with `{` in one ERB tag and closed with `<% } %>`
in another, and points at the `do ... end` form instead.

```erb
<% @users.each { |user| %>
  <p><%= user.name %></p>
<% } %>
```

```
Avoid using `{ ... }` for a block that spans multiple ERB tags. Use `do ... end` instead.
```

A block that spans ERB tags is a multi-line block, and `do ... end` is
the typical 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, since `<% } %>` is a single character of
signal surrounded by ERB punctuation and looks the same whether it
closes a block, a hash, or an interpolation.

A brace block written entirely inside one ERB tag is not reported, so
`<%= @users.map { |user| user.name }.join(", ") %>` stays as it is.

#### Autofix

The rule is autocorrectable. The fix replaces `{` with `do` in the
opening tag and `}` with `end` in the closing tag, keeping trim markers
intact and inserting the missing space for `{|user|`:

```erb
<%= form_with(model: @user) { |form| %>
  <%= form.submit %>
<% } %>
```

```erb
<%= form_with(model: @user) do |form| %>
  <%= form.submit %>
<% end %>
```

Swapping the delimiters is not unconditionally safe, because `{` binds
to the closest method call while `do` binds to the outermost one. The
fix is marked safe only when both are the same call, which it checks on
the Prism node: the outermost node has to be a `CallNode` that owns the
block, after unwrapping an assignment so that `<% names = @users.map {
|user| %>` still qualifies.

```erb
<% puts @users.map { |user| %>
  <p><%= user.name %></p>
<% } %>
```

Here the block belongs to `map`, but a `do` would hand it to `puts`, so
the offense is still reported while no autofix is offered.

This branch was successfully deployed

1 active deployment
herb-tools (Preview) — c4746a33 Deployed Aug 3, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation linter @herb-tools/linter for HTML+ERB templates linter-autofix Linter autofix behavior linter-rule Individual linter rules and their documentation typescript TypeScript source across the javascript/ packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant