Skip to content

Linter Rule: Disallow local_assigns reads that the strict locals declaration already answers

Rule: actionview-no-redundant-local-assigns

Description

Detects local_assigns lookups in a partial that already has a <%# locals: (...) %> declaration, where the declaration makes the lookup either redundant or dead.

Rationale

local_assigns is the documented way to read locals in a partial that has no strict locals declaration, and it stays useful in a partial that has one. Once a declaration exists, though, some of those lookups are answered by the declaration itself and only obscure what the template does.

A required local is already a local variable, so reading it back out of the hash is a longer way to write the name. A required local is also always present, so asking local_assigns.key? about it is a condition that can only take one branch. And a name the declaration does not mention can never arrive, because Rails raises ActionView::StrictLocalsError for callers that pass an undeclared local, so a lookup for it is dead code.

Optional locals are left alone. local_assigns.key?(:size) is the only way to tell "not passed" apart from "passed as nil", which a default value cannot express, so that check is a legitimate pattern rather than an offense. Partials with a ** keyword rest in the declaration are skipped as well, since undeclared locals can legitimately arrive there.

Partials without a strict locals declaration are not checked at all.

Examples

✅ Good

erb
<%# locals: (user:) %>

<%= user.name %>
erb
<%# locals: (user:, size: nil) %>

<%= user.name %>
<% if local_assigns.key?(:size) %>
  <span><%= size %></span>
<% end %>
erb
<%# locals: (user:, **) %>

<%= render "row", **local_assigns %>
The partial `row` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)

🚫 Bad

erb
<%# locals: (user:) %>

<%= local_assigns[:user].name %>
Strict local `user` is already a local variable in this partial, so `local_assigns[:user]` reads back a value that is already in scope. Use `user` instead. (actionview-no-redundant-local-assigns)
erb
<%# locals: (user:) %>

<% if local_assigns.key?(:user) %>
Strict local `user` is required, so `local_assigns.key?(:user)` is always `true`. Remove the condition, or give `user` a default value to make it optional. (actionview-no-redundant-local-assigns)
<%= user.name %> <% end %>
erb
<%# locals: (user:) %>

<%= user.name %>
<%= local_assigns.fetch(:size, "large") %>
`size` is not declared in the `locals:` declaration, so Rails raises if a caller passes it and `local_assigns.fetch(:size, ...)` can never find it. Declare `size:` in the declaration, or remove the lookup. (actionview-no-redundant-local-assigns)

References

Released under the MIT License.