Skip to content

Fix collection counter and iteration detection when forwarded through anonymous arguments - #2722

Merged
joelhawksley merged 1 commit into
ViewComponent:mainfrom
bensheldon:collection-initialize-chain
Oct 14, 2026
Merged

joelhawksley merged 1 commit into
ViewComponent:mainfrom
bensheldon:collection-initialize-chain

Conversation

@bensheldon

Copy link
Copy Markdown
Contributor

The collection introspection checks currently read instance_method(:initialize).parameters, which only sees the outermost initialize. When that method forwards its arguments, ViewComponent can't see the signature that actually declares the keywords. This happens with a prepended module.

Collection proxy introspection was a barrier to using a prepend-module strategy when I was working on #1106 because I couldn't simply prepend an anonymous-argument-forwarding method on top of a View Component class's initialize without regressing the collection behavior.

I have 3 PRs in mind for fully landing this improvement:

  1. This PR, which improves the existing introspection behavior
  2. A separate PR which extends with_collection_parameter to explicitly declare "send _counter / _iteration parameters" in addition to the introspection
  3. (if the explicit API is accepted) Deprecate the introspection behavior in favor of explicit declaration. I was a bit surprised to discover introspection because View Component otherwise makes a point of trying to be non-magic and explicit, so I think removing the introspection would make it more in line with its values and remove complexity, and performance/cache-invalidation-shaped problems.

Example:

module Tracing
  def initialize(...) = super
end

class ItemComponent < ViewComponent::Base
  prepend Tracing
  def initialize(item:, item_counter:, item_iteration:) = ...
end

render(ItemComponent.with_collection(items))
# => ArgumentError: missing keywords: :item_counter, :item_iteration

and with a subclass that forwards to super:

class SpecialItemComponent < ItemComponent
  def initialize(**)
    super
  end
end

The forwarding method's ** also causes the missing-collection-parameter and reserved-content checks to be skipped for prepended modules.

What it does

Collect parameters from each initialize up the super_method chain while the current method takes a keyword splat (** or ...), and stop at the first one that only accepts the keywords it names. Forwarding prepended modules ((...), (*, **), (*, foo:, **)) and subclasses that call super are then looked through to the signature that declares _counter/_iteration.

A prepended module that takes the counter itself (def initialize(*, item_counter:, **) = super(*, **)) is also detected. Subclasses that narrow their parent's signature (e.g. def initialize(item:) = super(item:, item_counter: 0)) aren't affected, because the walk stops there.

When deciding whether a component accepts any keyword, an anonymous **/... on a prepended module is ignored, because it can only pass keywords along. A named **options on a prepended module, or a keyword splat on the component itself, still counts, as before.

Other details

  • With a forwarding prepend, the collection-parameter and reserved-content checks now run against the component's real signature. A component that declares content: behind one will now raise ReservedParameterError.
  • Results are still cached per class, so a prepend that runs after the first collection render or compile isn't picked up.

@bensheldon
bensheldon force-pushed the collection-initialize-chain branch 4 times, most recently from bea8e06 to 81204e9 Compare October 12, 2026 14:40
@bensheldon
bensheldon force-pushed the collection-initialize-chain branch from 81204e9 to 9de81a8 Compare October 12, 2026 14:45
@bensheldon

Copy link
Copy Markdown
Contributor Author

Ooh, the ractorization linting is fun 👍🏻 That led me to move stuff around a little more than I expected to, but it's a good direction!

@joelhawksley
joelhawksley merged commit 6b6c2c3 into ViewComponent:main Oct 14, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants