Skip to content

Dynamically register events to dispatch#3490

Merged
kddnewton merged 1 commit intoruby:mainfrom
sambostock:dynamic-dispatch
Mar 20, 2025
Merged

Dynamically register events to dispatch#3490
kddnewton merged 1 commit intoruby:mainfrom
sambostock:dynamic-dispatch

Conversation

@sambostock
Copy link
Contributor

@sambostock sambostock commented Mar 11, 2025

Note

This draft PR is a companion to Shopify/ruby-lsp#3291, which proposes this approach. Tests and other things have been omitted for now in the interest of getting feedback on the approach first, and will be added if we decide to move forward.

Instead of requiring the consumer to provide a list of all events which they wish to handle, we can give them to option of dynamically detecting them, by scanning the listener's public methods.

This approach is similar to that used by Minitest (scanning for test_ methods) and Rails generators (running all public methods in the order they are defined).

While this is slower than specifying a hard coded list, the penalty is only during registration. There is no change the the behaviour of dispatching the events. It is also a non-breaking change, so consumers can continue specifying each event if desired.

Copy link
Collaborator

@kddnewton kddnewton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather have a separate method for this, as this behavior gets magically triggered by having no events in the list currently. Something like:

def register(listener, *events)
  events.each { |event| (listeners[event] ||= []) << listener }
end

def register_public_methods(listener)
  register(listener, *listener.public_methods(false).grep(/^on_.*_(enter|leave)$/))
end

The other difference I would like to see I have in the example here, which is that I think we should not include inherited methods, (so using public_methods(false)).

Instead of requiring the consumer to provide a list of all events which
they wish to handle, we can give them to option of dynamically detecting
them, by scanning the listener's public methods.

This approach is similar to that used by Minitest (scanning for `test_`
methods) and Rails generators (running all public methods in the order
they are defined).

While this is slower than specifying a hard coded list, the penalty is
only during registration. There is no change the the behaviour of
dispatching the events.
@kddnewton kddnewton marked this pull request as ready for review March 20, 2025 17:05
@kddnewton
Copy link
Collaborator

I like this approach so I'm marking it ready for review. I also force-pushed to your branch the changes I suggested, so it should be good to go. Thanks for doing this!

@kddnewton kddnewton merged commit 640b1e7 into ruby:main Mar 20, 2025
57 checks passed
@sambostock sambostock deleted the dynamic-dispatch branch March 20, 2025 17:59
@sambostock
Copy link
Contributor Author

Thanks @kddnewton!

#
# def register_public_methods: (Listener) -> void
def register_public_methods(listener)
register_events(listener, listener.public_methods(false).grep(/\Aon_.+_(?:enter|leave)\z/))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, @kddnewton under what circumstances would we not want to register inherited handlers? I can't think of a scenario in which that would be either introducing a bug or making things harder for the consumer, but maybe I'm missing something.

For example, if a consumer had some common functionality extracted into a parent class or module, this could break registration.

module HandleClassAndModuleNodeEnter
  def on_class_node_enter(node)
    on_class_or_module_enter(node)
  end

  def on_module_node_enter(node)
    on_class_or_module_enter(node)
  end
end

class MyListener
  include HandleClassAndModuleNodeEnter
  
  def initialize(dispatcher)
    dispatcher.register_public_methods(self)
  end

  def on_class_or_module_enter(node)
    # ...
  end
end

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it was more a way to avoid mishandling things. On the off chance something snuck in through some unwitting module, it wouldn't get accidentally triggered.

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