Skip to content

Commit aa94960

Browse files
authored
Merge pull request rails#49782 from Shopify/fix-strict-locals
Ignore implicit locals if not declared by templates with strict locals
2 parents 02fa5c2 + 110206a commit aa94960

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)