Skip to content

Commit 110206a

Browse files
byrootHolyWalley
andcommitted
Ignore implicit locals if not declared by templates with strict locals
Fix: rails#49780 Fix: rails#49761 `CollectionRenderer` implictly inject `<name>_counter` and `<name>_iteration` locals, but in the overwhelming majority of cases they aren't used. So when the rendered template has strict locals we shouldn't require these to be declared, and if they aren't we should simply not pass them. Co-Authored-By: HolyWalley <[email protected]>
1 parent 4c5c904 commit 110206a

File tree

4 files changed

+51
-14
lines changed

4 files changed

+51
-14
lines changed

actionview/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Automatically discard the implicit locals injected by collection rendering for template that can't accept them
2+
3+
When rendering a collection, two implicit variables are injected, which breaks templates with strict locals.
4+
5+
Now they are only passed if the template will actually accept them.
6+
7+
*Yasha Krasnou*, *Jean Boussier*
8+
19
* Fix `@rails/ujs` calling `start()` an extra time when using bundlers
210

311
*Hartley McGuire*, *Ryunosuke Sato*

actionview/lib/action_view/renderer/collection_renderer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def collection_with_template(view, template, layout, collection)
194194

195195
_template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
196196

197-
content = _template.render(view, locals)
197+
content = _template.render(view, locals, implicit_locals: [counter, iteration])
198198
content = layout.render(view, locals) { content } if layout
199199
partial_iteration.iterate!
200200
build_rendered_template(content, _template)

actionview/lib/action_view/template.rb

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ def initialize(source, identifier, handler, locals:, format: nil, variant: nil,
201201
@variant = variant
202202
@compile_mutex = Mutex.new
203203
@strict_locals = NONE
204+
@strict_local_keys = nil
204205
@type = nil
205206
end
206207

@@ -244,9 +245,15 @@ def supports_streaming?
244245
# This method is instrumented as "!render_template.action_view". Notice that
245246
# we use a bang in this instrumentation because you don't want to
246247
# consume this in production. This is only slow if it's being listened to.
247-
def render(view, locals, buffer = nil, add_to_stack: true, &block)
248+
def render(view, locals, buffer = nil, implicit_locals: [], add_to_stack: true, &block)
248249
instrument_render_template do
249250
compile!(view)
251+
252+
if strict_locals? && @strict_local_keys && !implicit_locals.empty?
253+
locals_to_ignore = implicit_locals - @strict_local_keys
254+
locals.except!(*locals_to_ignore)
255+
end
256+
250257
if buffer
251258
view._run(method_name, self, locals, buffer, add_to_stack: add_to_stack, has_strict_locals: strict_locals?, &block)
252259
nil
@@ -474,23 +481,30 @@ def compile(mod)
474481

475482
return unless strict_locals?
476483

484+
parameters = mod.instance_method(method_name).parameters - [[:req, :output_buffer]]
477485
# Check compiled method parameters to ensure that only kwargs
478486
# were provided as strict locals, preventing `locals: (foo, *foo)` etc
479487
# and allowing `locals: (foo:)`.
480488

481-
non_kwarg_parameters =
482-
(mod.instance_method(method_name).parameters - [[:req, :output_buffer]]).
483-
select { |parameter| ![:keyreq, :key, :keyrest, :nokey].include?(parameter[0]) }
489+
non_kwarg_parameters = parameters.select do |parameter|
490+
![:keyreq, :key, :keyrest, :nokey].include?(parameter[0])
491+
end
484492

485-
return unless non_kwarg_parameters.any?
493+
unless non_kwarg_parameters.empty?
494+
mod.undef_method(method_name)
486495

487-
mod.undef_method(method_name)
496+
raise ArgumentError.new(
497+
"#{non_kwarg_parameters.map { |_, name| "`#{name}`" }.to_sentence} set as non-keyword " \
498+
"#{'argument'.pluralize(non_kwarg_parameters.length)} for #{short_identifier}. " \
499+
"Locals can only be set as keyword arguments."
500+
)
501+
end
488502

489-
raise ArgumentError.new(
490-
"#{non_kwarg_parameters.map { |_, name| "`#{name}`" }.to_sentence} set as non-keyword " \
491-
"#{'argument'.pluralize(non_kwarg_parameters.length)} for #{short_identifier}. " \
492-
"Locals can only be set as keyword arguments."
493-
)
503+
unless parameters.any? { |type, _| type == :keyrest }
504+
parameters.map!(&:first)
505+
parameters.sort!
506+
@strict_local_keys = parameters.freeze
507+
end
494508
end
495509

496510
def offset

actionview/test/template/template_test.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ def new_template(body = "<%= hello %>", details = {})
6262
ActionView::Template.new(body.dup, "hello template", details.delete(:handler) || ERBHandler, virtual_path: "hello", **details)
6363
end
6464

65-
def render(locals = {})
66-
@template.render(@context, locals)
65+
def render(implicit_locals: [], **locals)
66+
@template.render(@context, locals, implicit_locals: implicit_locals)
6767
end
6868

6969
def setup
@@ -200,6 +200,21 @@ def test_extra_locals_raises_error
200200
assert_match(/unknown local: :foo/, error.message)
201201
end
202202

203+
def test_rails_injected_locals_does_not_raise_error_if_not_passed
204+
@template = new_template("<%# locals: (message:) -%>")
205+
render(message: "Hi", message_counter: 1, message_iteration: 1, implicit_locals: %i[message_counter message_iteration])
206+
end
207+
208+
def test_rails_injected_locals_can_be_specified
209+
@template = new_template("<%# locals: (message: 'Hello') -%>\n<%= message %>")
210+
assert_equal "Hello", render(message: "Hello", implicit_locals: %i[message])
211+
end
212+
213+
def test_rails_injected_locals_can_be_specified_as_kwargs
214+
@template = new_template("<%# locals: (message: 'Hello', **kwargs) -%>\n<%= kwargs[:message_counter] %>-<%= kwargs[:message_iteration] %>")
215+
assert_equal "1-2", render(message: "Hello", message_counter: 1, message_iteration: 2, implicit_locals: %i[message_counter message_iteration])
216+
end
217+
203218
# TODO: This is currently handled inside ERB. The case of explicitly
204219
# lying about encodings via the normal Rails API should be handled
205220
# inside Rails.

0 commit comments

Comments
 (0)