Skip to content

Commit 03a45f8

Browse files
vinistockkddnewton
andcommitted
Add node event dispatcher
This commit changes the node template to create a dispatcher class, which can be used to walk an AST an emit events to all registered listeners Co-authored-by: Kevin Newton <kddnewton@users.noreply.github.com>
1 parent deef3aa commit 03a45f8

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

templates/lib/yarp/node.rb.erb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ module YARP
4848
}.compact.join(", ") %>]
4949
end
5050

51+
# def compact_child_nodes: () -> Array[Node]
52+
def compact_child_nodes
53+
<%- if node.fields.any? { |field| field.is_a?(YARP::OptionalNodeField) } -%>
54+
compact = []
55+
<%- node.fields.each do |field| -%>
56+
<%- case field -%>
57+
<%- when YARP::NodeField -%>
58+
compact << <%= field.name %>
59+
<%- when YARP::OptionalNodeField -%>
60+
compact << <%= field.name %> if <%= field.name %>
61+
<%- when YARP::NodeListField -%>
62+
compact.concat(<%= field.name %>)
63+
<%- end -%>
64+
<%- end -%>
65+
compact
66+
<%- else -%>
67+
[<%= node.fields.map { |field|
68+
case field
69+
when YARP::NodeField then field.name
70+
when YARP::NodeListField then "*#{field.name}"
71+
end
72+
}.compact.join(", ") %>]
73+
<%- end -%>
74+
end
75+
5176
# def comment_targets: () -> Array[Node | Location]
5277
def comment_targets
5378
[<%= node.fields.map { |field|
@@ -136,6 +161,13 @@ module YARP
136161
<%- end -%>
137162
inspector.to_str
138163
end
164+
165+
# Returns a symbol representation of the type of node.
166+
#
167+
# def human: () -> Symbol
168+
def human
169+
:<%= node.human %>
170+
end
139171
end
140172

141173
<%- end -%>
@@ -157,6 +189,55 @@ module YARP
157189
<%- end -%>
158190
end
159191

192+
# The dispatcher class fires events for nodes that are found while walking an AST to all registered listeners. It's
193+
# useful for performing different types of analysis on the AST without having to repeat the same visits multiple times
194+
class Dispatcher
195+
# attr_reader listeners: Hash[Symbol, Array[Listener]]
196+
attr_reader :listeners
197+
198+
def initialize
199+
@listeners = {}
200+
end
201+
202+
# Register a listener for one or more events
203+
#
204+
# def register: (Listener, *Symbol) -> void
205+
def register(listener, *events)
206+
events.each { |event| (listeners[event] ||= []) << listener }
207+
end
208+
209+
# Walks `root` dispatching events to all registered listeners
210+
#
211+
# def dispatch: (Node) -> void
212+
def dispatch(root)
213+
queue = [root]
214+
215+
while (node = queue.shift)
216+
case node.human
217+
<%- nodes.each do |node| -%>
218+
when :<%= node.human %>
219+
listeners[:<%= node.human %>_enter]&.each { |listener| listener.<%= node.human %>_enter(node) }
220+
queue = node.compact_child_nodes.concat(queue)
221+
listeners[:<%= node.human %>_leave]&.each { |listener| listener.<%= node.human %>_leave(node) }
222+
<%- end -%>
223+
end
224+
end
225+
end
226+
227+
# Dispatches a single event for `node` to all registered listeners
228+
#
229+
# def dispatch_once: (Node) -> void
230+
def dispatch_once(node)
231+
case node.human
232+
<%- nodes.each do |node| -%>
233+
when :<%= node.human %>
234+
listeners[:<%= node.human %>_enter]&.each { |listener| listener.<%= node.human %>_enter(node) }
235+
listeners[:<%= node.human %>_leave]&.each { |listener| listener.<%= node.human %>_leave(node) }
236+
<%- end -%>
237+
end
238+
end
239+
end
240+
160241
module DSL
161242
private
162243

test/yarp/dispatcher_test.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "test_helper"
4+
5+
module YARP
6+
class DispatcherTest < TestCase
7+
def setup
8+
@listener = Class.new do
9+
attr_reader :events_received
10+
11+
def initialize
12+
@events_received = []
13+
end
14+
15+
def call_node_enter(node)
16+
@events_received << :call_node_enter
17+
end
18+
19+
def call_node_leave(node)
20+
@events_received << :call_node_leave
21+
end
22+
end.new
23+
end
24+
25+
def test_dispatching_events
26+
dispatcher = Dispatcher.new
27+
dispatcher.register(@listener, :call_node_enter, :call_node_leave)
28+
29+
root = YARP.parse(<<~RUBY).value
30+
def foo
31+
something(1, 2, 3)
32+
end
33+
RUBY
34+
35+
dispatcher.dispatch(root)
36+
37+
assert_equal([:call_node_enter, :call_node_leave], @listener.events_received)
38+
end
39+
end
40+
end

0 commit comments

Comments
 (0)