Skip to content

Linter Rule: Disallow passing a content argument to a helper that is given a block

Rule: actionview-no-content-argument-with-block

Description

Detects Action View helpers that are given both a positional content argument and a block, such as tag.div "Hello" do, content_tag :section, "Intro" do or link_to "Go", root_path do.

Rationale

An Action View helper that takes its content from either an argument or a block only reads one of them. When a block is given, the block wins and the argument goes somewhere the caller did not intend.

For tag, content_tag, button_tag, label_tag, mail_to, phone_to and sms_to the argument is silently discarded:

ruby
content_tag(:section, "Intro") { "Welcome" }
# => "<section>Welcome</section>"

Nothing warns, nothing raises, and "Intro" never reaches the page. The template reads as if both strings render.

For link_to and button_to it is worse. Those helpers shift their arguments when a block is given, so the first argument becomes the URL and the second becomes the HTML attributes hash:

ruby
link_to("Go", "/dashboard") { "Go now" }
# => NoMethodError: undefined method 'stringify_keys' for an instance of String

Both helpers are correct with a block as long as the content argument is left out.

The rule reads which argument holds the content, and how many positional arguments survive a block, from the Action View helper registry, so a helper that gains the same metadata is covered without a change to the rule.

An argument whose value cannot be determined statically is never reported, because Action View treats a hash in that position as the options hash. content_tag :div, wrapper_options do may well be passing options and is left alone. Only literal strings, interpolated strings, symbols and numbers are reported.

Helpers whose first argument is not content are unaffected. field_set_tag "Account" do renders the legend and the block body, link_to_if only calls its block when the condition fails, and truncate appends its block to the truncated text.

Examples

✅ Good

erb
<%= tag.div "Hello" %>
erb
<%= tag.div do %>
  Hello
<% end %>
erb
<%= content_tag :div, class: "card" do %>
  Hello
<% end %>
erb
<%= link_to "Dashboard", root_path %>
erb
<%= link_to root_path do %>
  Dashboard
<% end %>
erb
<%= button_tag class: "primary" do %>
  Save
<% end %>

🚫 Bad

erb
<%= tag.div "Hello" do %>
The `tag.div` helper renders either its content argument or its block, never both, and the block wins, so `"Hello"` is silently discarded and never reaches the page. Remove `"Hello"`, or remove the block and let the argument render the content. (actionview-no-content-argument-with-block)
World <% end %>
erb
<%= content_tag :section, "Intro" do %>
The `content_tag` helper renders either its content argument or its block, never both, and the block wins, so `"Intro"` is silently discarded and never reaches the page. Remove `"Intro"`, or remove the block and let the argument render the content. (actionview-no-content-argument-with-block)
Welcome <% end %>
erb
<%= link_to "Go", root_path do %>
The `link_to` helper shifts its arguments when it is given a block, so `"Go"` is read as `options` and `root_path` as `html_options` instead of as content. Rails expects a Hash in `html_options` and raises when it is not one. Remove `"Go"` and let the block render the content. (actionview-no-content-argument-with-block)
Go now <% end %>
erb
<%= button_tag "Save" do %>
The `button_tag` helper renders either its content argument or its block, never both, and the block wins, so `"Save"` is silently discarded and never reaches the page. Remove `"Save"`, or remove the block and let the argument render the content. (actionview-no-content-argument-with-block)
Submit <% end %>
erb
<%= label_tag :email, "Email" do %>
The `label_tag` helper renders either its content argument or its block, never both, and the block wins, so `"Email"` is silently discarded and never reaches the page. Remove `"Email"`, or remove the block and let the argument render the content. (actionview-no-content-argument-with-block)
Email address <% end %>

References

Released under the MIT License.