Skip to content

Commit 96ac9a8

Browse files
authored
Merge pull request #2724 from bensheldon/collection-arguments-ractor-safety
Fix stale and inherited collection parameter detection; build it at compile time
2 parents 6b6c2c3 + 4398910 commit 96ac9a8

5 files changed

Lines changed: 131 additions & 19 deletions

File tree

‎.audition-baseline.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"class-level-state|lib/view_component/base.rb": 12,
2+
"class-level-state|lib/view_component/base.rb": 7,
33
"class-level-state|lib/view_component/cache_digest.rb": 1,
44
"class-level-state|lib/view_component/configurable.rb": 1,
55
"class-level-state|lib/view_component/engine.rb": 1,

‎docs/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ nav_order: 6
1010

1111
## main
1212

13+
* Fix subclasses defined after their parent renders a collection using the parent's default collection parameter name instead of their own. Fix recompiling a component not refreshing its collection parameter, counter, and iteration detection. Compute that detection when a component compiles, and store the results frozen so they're shareable across Ractors.
14+
15+
*Ben Sheldon*
16+
1317
* Fix collection counter and iteration detection, and collection and reserved parameter validation, when `initialize` arguments are forwarded through anonymous arguments (`...` or `**`), such as by prepended modules and subclasses calling `super`.
1418

1519
*Ben Sheldon*

‎lib/view_component/base.rb‎

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ def strip_trailing_whitespace?
747747
# @private
748748
def __vc_validate_collection_parameter!(validate_default: false)
749749
if validate_default
750-
return if __vc_collection_arguments[:parameter]
750+
return if __vc_collection_arguments[:parameter_accepted]
751751

752752
raise MissingCollectionArgumentError.new(name, __vc_collection_parameter)
753753
end
@@ -770,12 +770,12 @@ def __vc_validate_initialization_parameters!
770770

771771
# @private
772772
def __vc_collection_parameter
773-
@__vc_provided_collection_parameter ||= name && name.demodulize.underscore.chomp("_component").to_sym
773+
__vc_collection_arguments[:parameter]
774774
end
775775

776776
# @private
777777
def __vc_collection_counter_parameter
778-
@__vc_collection_counter_parameter ||= :"#{__vc_collection_parameter}_counter"
778+
__vc_collection_arguments[:counter_parameter]
779779
end
780780

781781
# @private
@@ -785,22 +785,43 @@ def __vc_counter_argument_present?
785785

786786
# @private
787787
def __vc_collection_iteration_parameter
788-
@__vc_collection_iteration_parameter ||= :"#{__vc_collection_parameter}_iteration"
788+
__vc_collection_arguments[:iteration_parameter]
789789
end
790790

791791
# @private
792792
def __vc_iteration_argument_present?
793793
__vc_collection_arguments[:iteration]
794794
end
795795

796+
# @private
797+
def __vc_build_collection_arguments
798+
names =
799+
if respond_to?(:attribute_names)
800+
attribute_names.map(&:to_sym)
801+
else
802+
__vc_initialize_signatures.flat_map { |_owner, parameters| parameters.map(&:last) }
803+
end
804+
parameter = __vc_provided_collection_parameter || (name && name.demodulize.underscore.chomp("_component").to_sym)
805+
counter_parameter = :"#{parameter}_counter"
806+
iteration_parameter = :"#{parameter}_iteration"
807+
808+
@__vc_collection_arguments = Ractor.make_shareable(
809+
{
810+
initialize_parameter_names: names,
811+
parameter: parameter,
812+
parameter_accepted: parameter.nil? || names.include?(parameter) || __vc_splatted_keyword_argument_present?,
813+
counter_parameter: counter_parameter,
814+
counter: names.include?(counter_parameter),
815+
iteration_parameter: iteration_parameter,
816+
iteration: names.include?(iteration_parameter)
817+
}
818+
)
819+
end
820+
796821
private
797822

798823
def __vc_collection_arguments
799-
@__vc_collection_arguments ||= {
800-
parameter: __vc_collection_parameter_accepted?(__vc_collection_parameter),
801-
counter: __vc_initialize_parameter_names.include?(__vc_collection_counter_parameter),
802-
iteration: __vc_initialize_parameter_names.include?(__vc_collection_iteration_parameter)
803-
}.freeze
824+
@__vc_collection_arguments || __vc_build_collection_arguments
804825
end
805826

806827
def __vc_collection_parameter_accepted?(parameter)
@@ -818,12 +839,7 @@ def __vc_splatted_keyword_argument_present?
818839
end
819840

820841
def __vc_initialize_parameter_names
821-
@__vc_initialize_parameter_names ||=
822-
if respond_to?(:attribute_names)
823-
attribute_names.map(&:to_sym)
824-
else
825-
__vc_initialize_signatures.flat_map { |_owner, parameters| parameters.map(&:last) }
826-
end
842+
__vc_collection_arguments[:initialize_parameter_names]
827843
end
828844

829845
def __vc_initialize_signatures
@@ -838,9 +854,7 @@ def __vc_initialize_signatures
838854
signatures
839855
end
840856

841-
def __vc_provided_collection_parameter
842-
@__vc_provided_collection_parameter ||= nil
843-
end
857+
attr_reader :__vc_provided_collection_parameter
844858
end
845859

846860
ActiveSupport.run_load_hooks(:view_component, self)

‎lib/view_component/compiler.rb‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def compile(raise_errors: false, force: false)
4141
return false
4242
end
4343

44+
@component.__vc_build_collection_arguments
45+
4446
if raise_errors
4547
@component.__vc_validate_initialization_parameters!
4648
@component.__vc_validate_collection_parameter!

‎test/sandbox/test/collection_test.rb‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,5 +224,97 @@ def test_prepended_named_keyword_splat_accepts_collection_parameter
224224
assert_selector("p", text: "Radio clock")
225225
assert_selector("p", text: "Mints")
226226
end
227+
228+
def test_collection_arguments_are_ractor_shareable
229+
ForwardedCounterComponent.__vc_compile
230+
231+
assert Ractor.shareable?(ForwardedCounterComponent.send(:__vc_collection_arguments))
232+
end
233+
234+
class RecompiledCounterComponent < ViewComponent::Base
235+
with_collection_parameter :item
236+
237+
def initialize(item:)
238+
@item = item
239+
end
240+
241+
def call
242+
"<p>#{@item.name} #{@counter}</p>".html_safe
243+
end
244+
end
245+
246+
def test_recompiling_refreshes_collection_arguments
247+
render_inline(RecompiledCounterComponent.with_collection(@products))
248+
RecompiledCounterComponent.prepend(ConsumeCounter)
249+
RecompiledCounterComponent.__vc_compile(force: true)
250+
251+
render_inline(RecompiledCounterComponent.with_collection(@products))
252+
253+
assert_selector("p", text: "Radio clock 0")
254+
assert_selector("p", text: "Mints 1")
255+
end
256+
257+
class DefaultParameterParentComponent < ViewComponent::Base
258+
def initialize(**)
259+
end
260+
261+
def call
262+
""
263+
end
264+
end
265+
266+
def test_subclass_defined_after_parent_renders_uses_its_own_collection_parameter
267+
render_inline(DefaultParameterParentComponent.with_collection(@products))
268+
269+
self.class.class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
270+
class DefaultParameterChildComponent < DefaultParameterParentComponent
271+
def initialize(default_parameter_child:)
272+
@product = default_parameter_child
273+
end
274+
275+
def call
276+
"<p>\#{@product.name}</p>".html_safe
277+
end
278+
end
279+
RUBY
280+
281+
assert_equal :default_parameter_child, DefaultParameterChildComponent.__vc_collection_parameter
282+
283+
render_inline(DefaultParameterChildComponent.with_collection(@products))
284+
285+
assert_selector("p", text: "Radio clock")
286+
assert_selector("p", text: "Mints")
287+
end
288+
289+
class ReservedParameterParentComponent < ViewComponent::Base
290+
def initialize(title:, content: nil)
291+
end
292+
293+
def call
294+
""
295+
end
296+
end
297+
298+
class NamedKeywordSplatChildComponent < ReservedParameterParentComponent
299+
def initialize(**options)
300+
super(title: options[:title])
301+
end
302+
end
303+
304+
class AnonymousKeywordSplatChildComponent < ReservedParameterParentComponent
305+
def initialize(**)
306+
super
307+
end
308+
end
309+
310+
def test_keyword_splat_subclass_raises_for_reserved_parameter_in_parent
311+
assert_raises(ViewComponent::ReservedParameterError) do
312+
NamedKeywordSplatChildComponent.__vc_validate_initialization_parameters!
313+
end
314+
315+
assert_raises(ViewComponent::ReservedParameterError) do
316+
AnonymousKeywordSplatChildComponent.__vc_validate_initialization_parameters!
317+
end
318+
end
227319
end
228320
end

0 commit comments

Comments
 (0)