actionview 7.1.5.1 → 7.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -416
  3. data/README.rdoc +1 -1
  4. data/lib/action_view/base.rb +24 -9
  5. data/lib/action_view/cache_expiry.rb +9 -3
  6. data/lib/action_view/dependency_tracker/{ripper_tracker.rb → ruby_tracker.rb} +4 -3
  7. data/lib/action_view/dependency_tracker.rb +1 -1
  8. data/lib/action_view/digestor.rb +6 -2
  9. data/lib/action_view/gem_version.rb +3 -3
  10. data/lib/action_view/helpers/asset_tag_helper.rb +19 -7
  11. data/lib/action_view/helpers/atom_feed_helper.rb +1 -1
  12. data/lib/action_view/helpers/cache_helper.rb +2 -2
  13. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  14. data/lib/action_view/helpers/date_helper.rb +8 -1
  15. data/lib/action_view/helpers/form_helper.rb +222 -217
  16. data/lib/action_view/helpers/form_options_helper.rb +6 -3
  17. data/lib/action_view/helpers/form_tag_helper.rb +80 -47
  18. data/lib/action_view/helpers/output_safety_helper.rb +5 -6
  19. data/lib/action_view/helpers/tag_helper.rb +208 -18
  20. data/lib/action_view/helpers/tags/collection_helpers.rb +2 -1
  21. data/lib/action_view/helpers/text_helper.rb +11 -4
  22. data/lib/action_view/helpers/url_helper.rb +3 -77
  23. data/lib/action_view/layouts.rb +8 -10
  24. data/lib/action_view/log_subscriber.rb +8 -4
  25. data/lib/action_view/railtie.rb +0 -1
  26. data/lib/action_view/render_parser/prism_render_parser.rb +127 -0
  27. data/lib/action_view/{ripper_ast_parser.rb → render_parser/ripper_render_parser.rb} +152 -9
  28. data/lib/action_view/render_parser.rb +21 -169
  29. data/lib/action_view/renderer/abstract_renderer.rb +1 -1
  30. data/lib/action_view/renderer/partial_renderer.rb +2 -2
  31. data/lib/action_view/renderer/renderer.rb +32 -38
  32. data/lib/action_view/renderer/template_renderer.rb +3 -3
  33. data/lib/action_view/rendering.rb +4 -4
  34. data/lib/action_view/template/error.rb +11 -0
  35. data/lib/action_view/template/handlers/erb.rb +45 -37
  36. data/lib/action_view/template/renderable.rb +7 -1
  37. data/lib/action_view/template/resolver.rb +0 -2
  38. data/lib/action_view/template.rb +36 -8
  39. data/lib/action_view/test_case.rb +7 -10
  40. data/lib/action_view.rb +1 -0
  41. metadata +30 -18
@@ -1,176 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_view/ripper_ast_parser"
4
-
5
3
  module ActionView
6
- class RenderParser # :nodoc:
7
- def initialize(name, code)
8
- @name = name
9
- @code = code
10
- @parser = RipperASTParser
11
- end
12
-
13
- def render_calls
14
- render_nodes = @parser.parse_render_nodes(@code)
15
-
16
- render_nodes.map do |method, nodes|
17
- nodes.map { |n| send(:parse_render, n) }
18
- end.flatten.compact
19
- end
4
+ module RenderParser # :nodoc:
5
+ ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
6
+ RENDER_TYPE_KEYS = [:partial, :template, :layout]
20
7
 
21
- private
22
- def directory
23
- File.dirname(@name)
24
- end
25
-
26
- def resolve_path_directory(path)
27
- if path.include?("/")
28
- path
29
- else
30
- "#{directory}/#{path}"
31
- end
32
- end
33
-
34
- # Convert
35
- # render("foo", ...)
36
- # into either
37
- # render(template: "foo", ...)
38
- # or
39
- # render(partial: "foo", ...)
40
- def normalize_args(string, options_hash)
41
- if options_hash
42
- { partial: string, locals: options_hash }
43
- else
44
- { partial: string }
45
- end
46
- end
47
-
48
- def parse_render(node)
49
- node = node.argument_nodes
50
-
51
- if (node.length == 1 || node.length == 2) && !node[0].hash?
52
- if node.length == 1
53
- options = normalize_args(node[0], nil)
54
- elsif node.length == 2
55
- options = normalize_args(node[0], node[1])
56
- end
57
-
58
- return nil unless options
59
-
60
- parse_render_from_options(options)
61
- elsif node.length == 1 && node[0].hash?
62
- options = parse_hash_to_symbols(node[0])
63
-
64
- return nil unless options
65
-
66
- parse_render_from_options(options)
67
- else
68
- nil
69
- end
70
- end
71
-
72
- def parse_hash(node)
73
- node.hash? && node.to_hash
74
- end
75
-
76
- def parse_hash_to_symbols(node)
77
- hash = parse_hash(node)
78
-
79
- return unless hash
80
-
81
- hash.transform_keys do |key_node|
82
- key = parse_sym(key_node)
83
-
84
- return unless key
85
-
86
- key
87
- end
88
- end
89
-
90
- ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
91
-
92
- RENDER_TYPE_KEYS =
93
- [:partial, :template, :layout]
94
-
95
- def parse_render_from_options(options_hash)
96
- renders = []
97
- keys = options_hash.keys
98
-
99
- if (keys & RENDER_TYPE_KEYS).size < 1
100
- # Must have at least one of render keys
101
- return nil
102
- end
103
-
104
- if (keys - ALL_KNOWN_KEYS).any?
105
- # de-opt in case of unknown option
106
- return nil
107
- end
108
-
109
- render_type = (keys & RENDER_TYPE_KEYS)[0]
110
-
111
- node = options_hash[render_type]
112
-
113
- if node.string?
114
- template = resolve_path_directory(node.to_string)
115
- else
116
- if node.variable_reference?
117
- dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
118
- elsif node.vcall?
119
- dependency = node.variable_name
120
- elsif node.call?
121
- dependency = node.call_method_name
122
- else
123
- return
124
- end
125
-
126
- object_template = true
127
- template = "#{dependency.pluralize}/#{dependency.singularize}"
128
- end
129
-
130
- return unless template
131
-
132
- if spacer_template = render_template_with_spacer?(options_hash)
133
- virtual_path = partial_to_virtual_path(:partial, spacer_template)
134
- renders << virtual_path
135
- end
136
-
137
- if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
138
- return nil if options_hash.key?(:object) && options_hash.key?(:collection)
139
- return nil unless options_hash.key?(:partial)
140
- end
141
-
142
- virtual_path = partial_to_virtual_path(render_type, template)
143
- renders << virtual_path
144
-
145
- # Support for rendering multiple templates (i.e. a partial with a layout)
146
- if layout_template = render_template_with_layout?(render_type, options_hash)
147
- virtual_path = partial_to_virtual_path(:layout, layout_template)
148
-
149
- renders << virtual_path
150
- end
151
-
152
- renders
153
- end
154
-
155
- def parse_str(node)
156
- node.string? && node.to_string
157
- end
158
-
159
- def parse_sym(node)
160
- node.symbol? && node.to_symbol
8
+ class Base # :nodoc:
9
+ def initialize(name, code)
10
+ @name = name
11
+ @code = code
161
12
  end
162
13
 
163
14
  private
164
- def render_template_with_layout?(render_type, options_hash)
165
- if render_type != :layout && options_hash.key?(:layout)
166
- parse_str(options_hash[:layout])
167
- end
168
- end
169
-
170
- def render_template_with_spacer?(options_hash)
171
- if options_hash.key?(:spacer_template)
172
- parse_str(options_hash[:spacer_template])
173
- end
15
+ def directory
16
+ File.dirname(@name)
174
17
  end
175
18
 
176
19
  def partial_to_virtual_path(render_type, partial_path)
@@ -180,9 +23,18 @@ module ActionView
180
23
  partial_path
181
24
  end
182
25
  end
26
+ end
183
27
 
184
- def layout_to_virtual_path(layout_path)
185
- "layouts/#{layout_path}"
186
- end
28
+ # Check if prism is available. If it is, use it. Otherwise, use ripper.
29
+ begin
30
+ require "prism"
31
+ rescue LoadError
32
+ require "ripper"
33
+ require_relative "render_parser/ripper_render_parser"
34
+ Default = RipperRenderParser
35
+ else
36
+ require_relative "render_parser/prism_render_parser"
37
+ Default = PrismRenderParser
38
+ end
187
39
  end
188
40
  end
@@ -79,7 +79,7 @@ module ActionView
79
79
  path = if object.respond_to?(:to_partial_path)
80
80
  object.to_partial_path
81
81
  else
82
- raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
82
+ raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement #to_partial_path.")
83
83
  end
84
84
 
85
85
  if view.prefix_partial_path_with_controller_namespace
@@ -94,7 +94,7 @@ module ActionView
94
94
  # # <%= render partial: "accounts/account", locals: { account: @account} %>
95
95
  # <%= render partial: @account %>
96
96
  #
97
- # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
97
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on #to_partial_path,
98
98
  # # that's why we can replace:
99
99
  # # <%= render partial: "posts/post", collection: @posts %>
100
100
  # <%= render partial: @posts %>
@@ -114,7 +114,7 @@ module ActionView
114
114
  # # <%= render partial: "accounts/account", locals: { account: @account} %>
115
115
  # <%= render @account %>
116
116
  #
117
- # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
117
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on #to_partial_path,
118
118
  # # that's why we can replace:
119
119
  # # <%= render partial: "posts/post", collection: @posts %>
120
120
  # <%= render @posts %>
@@ -45,12 +45,6 @@ module ActionView
45
45
  end
46
46
  end
47
47
 
48
- # Direct access to template rendering.
49
- def render_template(context, options) # :nodoc:
50
- render_template_to_object(context, options).body
51
- end
52
-
53
- # Direct access to partial rendering.
54
48
  def render_partial(context, options, &block) # :nodoc:
55
49
  render_partial_to_object(context, options, &block).body
56
50
  end
@@ -59,46 +53,46 @@ module ActionView
59
53
  @cache_hits ||= {}
60
54
  end
61
55
 
62
- def render_template_to_object(context, options) # :nodoc:
63
- TemplateRenderer.new(@lookup_context).render(context, options)
64
- end
56
+ private
57
+ def render_template_to_object(context, options)
58
+ TemplateRenderer.new(@lookup_context).render(context, options)
59
+ end
65
60
 
66
- def render_partial_to_object(context, options, &block) # :nodoc:
67
- partial = options[:partial]
68
- if String === partial
69
- collection = collection_from_options(options)
61
+ def render_partial_to_object(context, options, &block)
62
+ partial = options[:partial]
63
+ if String === partial
64
+ collection = collection_from_options(options)
70
65
 
71
- if collection
72
- # Collection + Partial
73
- renderer = CollectionRenderer.new(@lookup_context, options)
74
- renderer.render_collection_with_partial(collection, partial, context, block)
75
- else
76
- if options.key?(:object)
77
- # Object + Partial
78
- renderer = ObjectRenderer.new(@lookup_context, options)
79
- renderer.render_object_with_partial(options[:object], partial, context, block)
66
+ if collection
67
+ # Collection + Partial
68
+ renderer = CollectionRenderer.new(@lookup_context, options)
69
+ renderer.render_collection_with_partial(collection, partial, context, block)
80
70
  else
81
- # Partial
82
- renderer = PartialRenderer.new(@lookup_context, options)
83
- renderer.render(partial, context, block)
71
+ if options.key?(:object)
72
+ # Object + Partial
73
+ renderer = ObjectRenderer.new(@lookup_context, options)
74
+ renderer.render_object_with_partial(options[:object], partial, context, block)
75
+ else
76
+ # Partial
77
+ renderer = PartialRenderer.new(@lookup_context, options)
78
+ renderer.render(partial, context, block)
79
+ end
84
80
  end
85
- end
86
- else
87
- collection = collection_from_object(partial) || collection_from_options(options)
88
-
89
- if collection
90
- # Collection + Derived Partial
91
- renderer = CollectionRenderer.new(@lookup_context, options)
92
- renderer.render_collection_derive_partial(collection, context, block)
93
81
  else
94
- # Object + Derived Partial
95
- renderer = ObjectRenderer.new(@lookup_context, options)
96
- renderer.render_object_derive_partial(partial, context, block)
82
+ collection = collection_from_object(partial) || collection_from_options(options)
83
+
84
+ if collection
85
+ # Collection + Derived Partial
86
+ renderer = CollectionRenderer.new(@lookup_context, options)
87
+ renderer.render_collection_derive_partial(collection, context, block)
88
+ else
89
+ # Object + Derived Partial
90
+ renderer = ObjectRenderer.new(@lookup_context, options)
91
+ renderer.render_object_derive_partial(partial, context, block)
92
+ end
97
93
  end
98
94
  end
99
- end
100
95
 
101
- private
102
96
  def collection_from_options(options)
103
97
  if options.key?(:collection)
104
98
  collection = options[:collection]
@@ -99,14 +99,14 @@ module ActionView
99
99
  if layout.start_with?("/")
100
100
  raise ArgumentError, "Rendering layouts from an absolute path is not supported."
101
101
  else
102
- @lookup_context.find_template(layout, nil, false, [], details)
102
+ @lookup_context.find_template(layout, nil, false, keys, details)
103
103
  end
104
104
  rescue ActionView::MissingTemplate
105
105
  all_details = @details.merge(formats: @lookup_context.default_formats)
106
- raise unless template_exists?(layout, nil, false, [], **all_details)
106
+ raise unless template_exists?(layout, nil, false, keys, **all_details)
107
107
  end
108
108
  when Proc
109
- resolve_layout(layout.call(@lookup_context, formats), keys, formats)
109
+ resolve_layout(layout.call(@lookup_context, formats, keys), keys, formats)
110
110
  else
111
111
  layout
112
112
  end
@@ -27,10 +27,10 @@ module ActionView
27
27
  extend ActiveSupport::Concern
28
28
  include ActionView::ViewPaths
29
29
 
30
- attr_reader :rendered_format
30
+ attr_internal_reader :rendered_format
31
31
 
32
32
  def initialize
33
- @rendered_format = nil
33
+ @_rendered_format = nil
34
34
  super
35
35
  end
36
36
 
@@ -136,7 +136,7 @@ module ActionView
136
136
  end
137
137
 
138
138
  rendered_format = rendered_template.format || lookup_context.formats.first
139
- @rendered_format = Template::Types[rendered_format]
139
+ @_rendered_format = Template::Types[rendered_format]
140
140
 
141
141
  rendered_template.body
142
142
  end
@@ -179,7 +179,7 @@ module ActionView
179
179
  options[:partial] = action_name
180
180
  end
181
181
 
182
- if (options.keys & [:partial, :file, :template]).empty?
182
+ if !options.keys.intersect?([:partial, :file, :template])
183
183
  options[:prefixes] ||= _prefixes
184
184
  end
185
185
 
@@ -27,6 +27,17 @@ module ActionView
27
27
  end
28
28
  end
29
29
 
30
+ class StrictLocalsError < ArgumentError # :nodoc:
31
+ def initialize(argument_error, template)
32
+ message = argument_error.message.
33
+ gsub("unknown keyword:", "unknown local:").
34
+ gsub("missing keyword:", "missing local:").
35
+ gsub("no keywords accepted", "no locals accepted").
36
+ concat(" for #{template.short_identifier}")
37
+ super(message)
38
+ end
39
+ end
40
+
30
41
  class MissingTemplate < ActionViewError # :nodoc:
31
42
  attr_reader :path, :paths, :prefixes, :partial
32
43
 
@@ -42,7 +42,9 @@ module ActionView
42
42
  # source location inside the template.
43
43
  def translate_location(spot, backtrace_location, source)
44
44
  # Tokenize the source line
45
- tokens = ::ERB::Util.tokenize(source.lines[backtrace_location.lineno - 1])
45
+ source_lines = source.lines
46
+ return nil if source_lines.size < backtrace_location.lineno
47
+ tokens = ::ERB::Util.tokenize(source_lines[backtrace_location.lineno - 1])
46
48
  new_first_column = find_offset(spot[:snippet], tokens, spot[:first_column])
47
49
  lineno_delta = spot[:first_lineno] - backtrace_location.lineno
48
50
  spot[:first_lineno] -= lineno_delta
@@ -51,7 +53,7 @@ module ActionView
51
53
  column_delta = spot[:first_column] - new_first_column
52
54
  spot[:first_column] -= column_delta
53
55
  spot[:last_column] -= column_delta
54
- spot[:script_lines] = source.lines
56
+ spot[:script_lines] = source_lines
55
57
 
56
58
  spot
57
59
  rescue NotImplementedError, LocationParsingError
@@ -105,51 +107,57 @@ module ActionView
105
107
  raise WrongEncodingError.new(string, string.encoding)
106
108
  end
107
109
 
110
+ # Find which token in the source template spans the byte range that
111
+ # contains the error_column, then return the offset compared to the
112
+ # original source template.
113
+ #
114
+ # Iterate consecutive pairs of CODE or TEXT tokens, requiring
115
+ # a match of the first token before matching either token.
116
+ #
117
+ # For example, if we want to find tokens A, B, C, we do the following:
118
+ # 1. Find a match for A: test error_column or advance scanner.
119
+ # 2. Find a match for B or A:
120
+ # a. If B: start over with next token set (B, C).
121
+ # b. If A: test error_column or advance scanner.
122
+ # c. Otherwise: Advance 1 byte
123
+ #
124
+ # Prioritize matching the next token over the current token once
125
+ # a match for the current token has been found. This is to prevent
126
+ # the current token from looping past the next token if they both
127
+ # match (i.e. if the current token is a single space character).
108
128
  def find_offset(compiled, source_tokens, error_column)
109
129
  compiled = StringScanner.new(compiled)
130
+ offset_source_tokens(source_tokens).each_cons(2) do |(name, str, offset), (_, next_str, _)|
131
+ matched_str = false
110
132
 
111
- passed_tokens = []
133
+ until compiled.eos?
134
+ if matched_str && next_str && compiled.match?(next_str)
135
+ break
136
+ elsif compiled.match?(str)
137
+ matched_str = true
112
138
 
113
- while tok = source_tokens.shift
114
- tok_name, str = *tok
115
- case tok_name
116
- when :TEXT
117
- loop do
118
- break if compiled.match?(str)
119
- compiled.getch
120
- end
121
- raise LocationParsingError unless compiled.scan(str)
122
- when :CODE
123
- if compiled.pos > error_column
124
- raise LocationParsingError, "We went too far"
125
- end
139
+ if name == :CODE && compiled.pos <= error_column && compiled.pos + str.bytesize >= error_column
140
+ return error_column - compiled.pos + offset
141
+ end
126
142
 
127
- if compiled.pos + str.bytesize >= error_column
128
- offset = error_column - compiled.pos
129
- return passed_tokens.map(&:last).join.bytesize + offset
143
+ compiled.pos += str.bytesize
130
144
  else
131
- unless compiled.scan(str)
132
- raise LocationParsingError, "Couldn't find code snippet"
133
- end
134
- end
135
- when :OPEN
136
- next_tok = source_tokens.first.last
137
- loop do
138
- break if compiled.match?(next_tok)
139
- compiled.getch
145
+ compiled.pos += 1
140
146
  end
141
- when :CLOSE
142
- next_tok = source_tokens.first.last
143
- loop do
144
- break if compiled.match?(next_tok)
145
- compiled.getch
146
- end
147
- else
148
- raise LocationParsingError, "Not implemented: #{tok.first}"
149
147
  end
148
+ end
149
+
150
+ raise LocationParsingError, "Couldn't find code snippet"
151
+ end
150
152
 
151
- passed_tokens << tok
153
+ def offset_source_tokens(source_tokens)
154
+ source_offset = 0
155
+ with_offset = source_tokens.filter_map do |(name, str)|
156
+ result = [name, str, source_offset] if name == :CODE || name == :TEXT
157
+ source_offset += str.bytesize
158
+ result
152
159
  end
160
+ with_offset << [:EOS, nil, source_offset]
153
161
  end
154
162
  end
155
163
  end
@@ -14,10 +14,16 @@ module ActionView
14
14
 
15
15
  def render(context, *args)
16
16
  @renderable.render_in(context)
17
+ rescue NoMethodError
18
+ if !@renderable.respond_to?(:render_in)
19
+ raise ArgumentError, "'#{@renderable.inspect}' is not a renderable object. It must implement #render_in."
20
+ else
21
+ raise
22
+ end
17
23
  end
18
24
 
19
25
  def format
20
- @renderable.format
26
+ @renderable.try(:format)
21
27
  end
22
28
  end
23
29
  end
@@ -10,8 +10,6 @@ require "concurrent/map"
10
10
  module ActionView
11
11
  # = Action View Resolver
12
12
  class Resolver
13
- include ActiveSupport::Deprecation::DeprecatedConstantAccessor
14
-
15
13
  class PathParser # :nodoc:
16
14
  ParsedPath = Struct.new(:path, :details)
17
15
 
@@ -148,6 +148,20 @@ module ActionView
148
148
  # <p><%= alert %></p>
149
149
  # <% end %>
150
150
  #
151
+ # By default, templates will accept any <tt>locals</tt> as keyword arguments
152
+ # and make them available to <tt>local_assigns</tt>. To restrict what
153
+ # <tt>local_assigns</tt> a template will accept, add a <tt>locals:</tt> magic comment:
154
+ #
155
+ # <%# locals: (headline:, alerts: []) %>
156
+ #
157
+ # <h1><%= headline %></h1>
158
+ #
159
+ # <% alerts.each do |alert| %>
160
+ # <p><%= alert %></p>
161
+ # <% end %>
162
+ #
163
+ # Read more about strict locals in {Action View Overview}[https://guides.rubyonrails.org/action_view_overview.html#strict-locals]
164
+ # in the guides.
151
165
 
152
166
  eager_autoload do
153
167
  autoload :Error
@@ -216,11 +230,21 @@ module ActionView
216
230
  end
217
231
 
218
232
  def spot(location) # :nodoc:
219
- ast = RubyVM::AbstractSyntaxTree.parse(compiled_source, keep_script_lines: true)
220
233
  node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
221
- node = find_node_by_id(ast, node_id)
234
+ found =
235
+ if RubyVM::InstructionSequence.compile("").to_a[4][:parser] == :prism
236
+ require "prism"
222
237
 
223
- ErrorHighlight.spot(node)
238
+ if Prism::VERSION >= "1.0.0"
239
+ result = Prism.parse(compiled_source).value
240
+ result.breadth_first_search { |node| node.node_id == node_id }
241
+ end
242
+ else
243
+ node = RubyVM::AbstractSyntaxTree.parse(compiled_source, keep_script_lines: true)
244
+ find_node_by_id(node, node_id)
245
+ end
246
+
247
+ ErrorHighlight.spot(found) if found
224
248
  end
225
249
 
226
250
  # Translate an error location returned by ErrorHighlight to the correct
@@ -425,9 +449,9 @@ module ActionView
425
449
  method_arguments =
426
450
  if set_strict_locals
427
451
  if set_strict_locals.include?("&")
428
- "output_buffer, #{set_strict_locals}"
452
+ "local_assigns, output_buffer, #{set_strict_locals}"
429
453
  else
430
- "output_buffer, #{set_strict_locals}, &_"
454
+ "local_assigns, output_buffer, #{set_strict_locals}, &_"
431
455
  end
432
456
  else
433
457
  "local_assigns, output_buffer, &_"
@@ -486,11 +510,12 @@ module ActionView
486
510
 
487
511
  return unless strict_locals?
488
512
 
489
- parameters = mod.instance_method(method_name).parameters - [[:req, :output_buffer]]
513
+ parameters = mod.instance_method(method_name).parameters
514
+ parameters -= [[:req, :local_assigns], [:req, :output_buffer]]
515
+
490
516
  # Check compiled method parameters to ensure that only kwargs
491
517
  # were provided as strict locals, preventing `locals: (foo, *foo)` etc
492
518
  # and allowing `locals: (foo:)`.
493
-
494
519
  non_kwarg_parameters = parameters.select do |parameter|
495
520
  ![:keyreq, :key, :keyrest, :nokey].include?(parameter[0])
496
521
  end
@@ -531,12 +556,15 @@ module ActionView
531
556
  end
532
557
  end
533
558
 
559
+ RUBY_RESERVED_KEYWORDS = ::ActiveSupport::Delegation::RUBY_RESERVED_KEYWORDS
560
+ private_constant :RUBY_RESERVED_KEYWORDS
561
+
534
562
  def locals_code
535
563
  return "" if strict_locals?
536
564
 
537
565
  # Only locals with valid variable names get set directly. Others will
538
566
  # still be available in local_assigns.
539
- locals = @locals - Module::RUBY_RESERVED_KEYWORDS
567
+ locals = @locals - RUBY_RESERVED_KEYWORDS
540
568
 
541
569
  locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
542
570