Class: RuboCop::Cop::Heatwave::SafeBufferInAttribute
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::Heatwave::SafeBufferInAttribute
- Extended by:
- AutoCorrector
- Defined in:
- lib/rubocop/cop/heatwave/safe_buffer_in_attribute.rb
Overview
Flags <%= EXPR %> ERB interpolations that emit html_safe /
ActiveSupport::SafeBuffer content (fa_icon, tag.*, content_tag,
link_to, button_to, any *_tag helper, safe_join, safe_concat,
sanitize, simple_format, highlight, raw, render,
<expr>.html_safe, or a h(...) / html_escape(...) /
ERB::Util.html_escape(...) wrapper — these are no-ops on SafeBuffer
and don't actually fix the bug) inside an HTML attribute value when
the expression is not wrapped in html_escape_once(...).
Rails ERB's auto-escape is a no-op on SafeBuffer, so the inner "
characters from the helper's HTML markup leak through and close the
attribute prematurely. ERB::Util.html_escape_once forces the escape
while leaving any already-encoded entities in the safe content alone,
which is what you want when the value will later be read out (e.g. via
dataset.foo for Turbo's data-turbo-submits-with swap).
The autocorrect emits the fully-qualified ERB::Util.html_escape_once,
not the bare html_escape_once helper: the bare form is only in scope
in app/views/** ERB and raises NoMethodError inside ViewComponent
templates (app/components/**/*.html.erb), which was AppSignal #4429.
The module function is identical in output and resolves in both.
The check is intentionally conservative — it only fires on a fixed
list of known-html_safe call patterns plus a .html_safe trailer, so
plain string interpolations like placeholder="<%= label %>" are not
flagged.
Constant Summary collapse
- MSG =
Msg.
'`html_safe` content interpolated into an HTML attribute value ' \ 'is not auto-escaped by Rails — wrap the expression in ' \ '`ERB::Util.html_escape_once(...)` so the inner `"` characters ' \ 'do not close the attribute prematurely.'
- DOUBLE_QUOTED_ATTR =
An attribute (
name="…"orname='…') whose value contains at least
one<%= … %>interpolation. The surrounding quote char is excluded
from the value content so we don't accidentally span across
neighbouring attributes; the<%= … %>body is.*?so it can carry
the opposite quote (e.g.'spinner'inside a double-quoted attr). /\b([a-zA-Z_][\w:-]*)\s*=\s*"((?:[^"]*?<%=.*?%>)+[^"]*?)"/m- SINGLE_QUOTED_ATTR =
/\b([a-zA-Z_][\w:-]*)\s*=\s*'((?:[^']*?<%=.*?%>)+[^']*?)'/m- ERB_INTERPOLATION =
A single ERB output tag inside an attribute value. Capture group 1
is the trimmed expression, used both for the html-safe heuristic and
the autocorrect'sERB::Util.html_escape_once(...)wrap. /<%=\s*(.+?)\s*%>/m- HTML_SAFE_HEAD =
Heuristic for "this expression returns html_safe / SafeBuffer".
Matches at the START of the expression so we don't false-positive on
helpers.fa_icon(already covered byRedundantHelpersFaIcon) or
arbitrary strings that happen to mention these names.h/html_escape/ERB::Util.html_escapeare intentionally
included: they preserve the SafeBuffer flag (no-op when input is
already html_safe) and therefore do not fix the attribute-quote-
collision bug. Wrappingh(fa_icon(...))in
ERB::Util.html_escape_once(...)is the correct fix even though it
looks redundant.handtagare the short-name traps:his thehtml_escape
alias andtagis theTagBuilder, but both are also plausible
local-variable names. Matching a bareh/tag(e.g.<%= h %>
for an Integer,<%= tag %>for a String) wrapped ordinary
auto-escaped values in a pointless — and, in a ViewComponent
template, crashing — call (AppSignal #4429). So they now only match
when actually invoked:h(as a call,tag.foo/tag(as the
builder. The longer names (raw,sanitize, …) are left as bare
\bmatches because they are rarely variable names and are commonly
called without parens (raw user_input). /\A(?: fa_icon | tag (?= \s* (?: \. \w | \( ) ) | content_tag | link_to | button_to | [a-z_]\w*_tag | safe_join | safe_concat | sanitize | simple_format | highlight | raw | render | h (?= \s* \( ) | html_escape | ERB::Util\.html_escape )\b/x- HTML_SAFE_TRAIL =
Trailing
.html_safeanywhere — coversfoo.html_safe,
bar.baz.html_safe,(stuff).html_safe. /\.html_safe\b/- ALREADY_ESCAPED =
Already wrapped in
html_escape_once(...)— skip. Both the bare
html_escape_once(valid inapp/views/**ERB) and the qualified
ERB::Util.html_escape_once(what the autocorrect now emits, and the
only form that also works inside ViewComponent templates) count as
already escaped. Note thath(...)/ERB::Util.html_escape(...)
are not treated as "already escaped" — they're no-ops on SafeBuffer
and don't fix the bug, so they're flagged viaHTML_SAFE_HEADabove. /\A(?:ERB::Util\.)?html_escape_once\s*[(\s]/- SCRIPT_BLOCK =
Skip matches inside
<script>…</script>blocks — those are JS
string-literal contents, not real HTML attributes. %r{<script\b[^>]*>(.*?)</script>}im- JAVASCRIPT_TAG_BLOCK =
Also skip
<%= javascript_tag do %> … <% end %>helper blocks. These
emit a<script>only at RENDER time — there is no literal<script>
in the ERB source, so SCRIPT_BLOCK misses them — but their bodies are
JS string literals just like a<script>block. Without this guard,
el.innerHTML = '<%= fa_icon(…) %>'(a JS assignment) matches the
attribute regex as ifinnerHTMLwere an HTML attribute, and the
autocorrect wraps it inhtml_escape_once, which HTML-escapes the SVG
so the icon renders as literal<svg>text (shipped as a bug once —
PR #1569). Non-greedy to the first<% end %>: javascript_tag blocks
in this codebase never nest an ERB block (verified 0/124), so the
first<% end %>always closes the block.do -%>trim variant and a
trimmed<%- end %>are both allowed. /<%=?\s*javascript_tag\b.*?\bdo\s*-?%>(.*?)<%-?\s*end\s*-?%>/m
Instance Method Summary collapse
-
#on_new_investigation ⇒ void
Invoked by RuboCop when investigating a Ruby-parseable file.
-
#on_other_file ⇒ void
Invoked by RuboCop when investigating a non-Ruby file (e.g. ERB).
Instance Method Details
#on_new_investigation ⇒ void
This method returns an undefined value.
Invoked by RuboCop when investigating a Ruby-parseable file.
135 136 137 |
# File 'lib/rubocop/cop/heatwave/safe_buffer_in_attribute.rb', line 135 def on_new_investigation scan_source end |
#on_other_file ⇒ void
This method returns an undefined value.
Invoked by RuboCop when investigating a non-Ruby file (e.g. ERB).
141 142 143 |
# File 'lib/rubocop/cop/heatwave/safe_buffer_in_attribute.rb', line 141 def on_other_file scan_source end |