yarp 0.10.0 → 0.12.0

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.
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARP
4
+ class ParseResult
5
+ # When we've parsed the source, we have both the syntax tree and the list of
6
+ # comments that we found in the source. This class is responsible for
7
+ # walking the tree and finding the nearest location to attach each comment.
8
+ #
9
+ # It does this by first finding the nearest locations to each comment.
10
+ # Locations can either come from nodes directly or from location fields on
11
+ # nodes. For example, a `ClassNode` has an overall location encompassing the
12
+ # entire class, but it also has a location for the `class` keyword.
13
+ #
14
+ # Once the nearest locations are found, it determines which one to attach
15
+ # to. If it's a trailing comment (a comment on the same line as other source
16
+ # code), it will favor attaching to the nearest location that occurs before
17
+ # the comment. Otherwise it will favor attaching to the nearest location
18
+ # that is after the comment.
19
+ class Comments
20
+ # A target for attaching comments that is based on a specific node's
21
+ # location.
22
+ class NodeTarget
23
+ attr_reader :node
24
+
25
+ def initialize(node)
26
+ @node = node
27
+ end
28
+
29
+ def start_offset
30
+ node.location.start_offset
31
+ end
32
+
33
+ def end_offset
34
+ node.location.end_offset
35
+ end
36
+
37
+ def encloses?(comment)
38
+ start_offset <= comment.location.start_offset &&
39
+ comment.location.end_offset <= end_offset
40
+ end
41
+
42
+ def <<(comment)
43
+ node.location.comments << comment
44
+ end
45
+ end
46
+
47
+ # A target for attaching comments that is based on a location field on a
48
+ # node. For example, the `end` token of a ClassNode.
49
+ class LocationTarget
50
+ attr_reader :location
51
+
52
+ def initialize(location)
53
+ @location = location
54
+ end
55
+
56
+ def start_offset
57
+ location.start_offset
58
+ end
59
+
60
+ def end_offset
61
+ location.end_offset
62
+ end
63
+
64
+ def encloses?(comment)
65
+ false
66
+ end
67
+
68
+ def <<(comment)
69
+ location.comments << comment
70
+ end
71
+ end
72
+
73
+ attr_reader :parse_result
74
+
75
+ def initialize(parse_result)
76
+ @parse_result = parse_result
77
+ end
78
+
79
+ def attach!
80
+ parse_result.comments.each do |comment|
81
+ preceding, enclosing, following = nearest_targets(parse_result.value, comment)
82
+ target =
83
+ if comment.trailing?
84
+ preceding || following || enclosing || NodeTarget.new(parse_result.value)
85
+ else
86
+ # If a comment exists on its own line, prefer a leading comment.
87
+ following || preceding || enclosing || NodeTarget.new(parse_result.value)
88
+ end
89
+
90
+ target << comment
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # Responsible for finding the nearest targets to the given comment within
97
+ # the context of the given encapsulating node.
98
+ def nearest_targets(node, comment)
99
+ comment_start = comment.location.start_offset
100
+ comment_end = comment.location.end_offset
101
+
102
+ targets = []
103
+ node.comment_targets.map do |value|
104
+ case value
105
+ when StatementsNode
106
+ targets.concat(value.body.map { |node| NodeTarget.new(node) })
107
+ when Node
108
+ targets << NodeTarget.new(value)
109
+ when Location
110
+ targets << LocationTarget.new(value)
111
+ end
112
+ end
113
+
114
+ targets.sort_by!(&:start_offset)
115
+ preceding = nil
116
+ following = nil
117
+
118
+ left = 0
119
+ right = targets.length
120
+
121
+ # This is a custom binary search that finds the nearest nodes to the
122
+ # given comment. When it finds a node that completely encapsulates the
123
+ # comment, it recurses downward into the tree.
124
+ while left < right
125
+ middle = (left + right) / 2
126
+ target = targets[middle]
127
+
128
+ target_start = target.start_offset
129
+ target_end = target.end_offset
130
+
131
+ if target.encloses?(comment)
132
+ # The comment is completely contained by this target. Abandon the
133
+ # binary search at this level.
134
+ return nearest_targets(target.node, comment)
135
+ end
136
+
137
+ if target_end <= comment_start
138
+ # This target falls completely before the comment. Because we will
139
+ # never consider this target or any targets before it again, this
140
+ # target must be the closest preceding target we have encountered so
141
+ # far.
142
+ preceding = target
143
+ left = middle + 1
144
+ next
145
+ end
146
+
147
+ if comment_end <= target_start
148
+ # This target falls completely after the comment. Because we will
149
+ # never consider this target or any targets after it again, this
150
+ # target must be the closest following target we have encountered so
151
+ # far.
152
+ following = target
153
+ right = middle
154
+ next
155
+ end
156
+
157
+ # This should only happen if there is a bug in this parser.
158
+ raise "Comment location overlaps with a target location"
159
+ end
160
+
161
+ [preceding, NodeTarget.new(node), following]
162
+ end
163
+ end
164
+
165
+ private_constant :Comments
166
+
167
+ # Attach the list of comments to their respective locations in the tree.
168
+ def attach_comments!
169
+ Comments.new(self).attach!
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARP
4
+ class ParseResult
5
+ # The :line tracepoint event gets fired whenever the Ruby VM encounters an
6
+ # expression on a new line. The types of expressions that can trigger this
7
+ # event are:
8
+ #
9
+ # * if statements
10
+ # * unless statements
11
+ # * nodes that are children of statements lists
12
+ #
13
+ # In order to keep track of the newlines, we have a list of offsets that
14
+ # come back from the parser. We assign these offsets to the first nodes that
15
+ # we find in the tree that are on those lines.
16
+ #
17
+ # Note that the logic in this file should be kept in sync with the Java
18
+ # MarkNewlinesVisitor, since that visitor is responsible for marking the
19
+ # newlines for JRuby/TruffleRuby.
20
+ class Newlines < Visitor
21
+ def initialize(newline_marked)
22
+ @newline_marked = newline_marked
23
+ end
24
+
25
+ def visit_block_node(node)
26
+ old_newline_marked = @newline_marked
27
+ @newline_marked = Array.new(old_newline_marked.size, false)
28
+
29
+ begin
30
+ super(node)
31
+ ensure
32
+ @newline_marked = old_newline_marked
33
+ end
34
+ end
35
+
36
+ alias_method :visit_lambda_node, :visit_block_node
37
+
38
+ def visit_if_node(node)
39
+ node.set_newline_flag(@newline_marked)
40
+ super(node)
41
+ end
42
+
43
+ alias_method :visit_unless_node, :visit_if_node
44
+
45
+ def visit_statements_node(node)
46
+ node.body.each do |child|
47
+ child.set_newline_flag(@newline_marked)
48
+ end
49
+ super(node)
50
+ end
51
+ end
52
+
53
+ private_constant :Newlines
54
+
55
+ # Walk the tree and mark nodes that are on a new line.
56
+ def mark_newlines!
57
+ value.accept(Newlines.new(Array.new(1 + source.offsets.size, false)))
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARP
4
+ # A pattern is an object that wraps a Ruby pattern matching expression. The
5
+ # expression would normally be passed to an `in` clause within a `case`
6
+ # expression or a rightward assignment expression. For example, in the
7
+ # following snippet:
8
+ #
9
+ # case node
10
+ # in ConstantPathNode[ConstantReadNode[name: :YARP], ConstantReadNode[name: :Pattern]]
11
+ # end
12
+ #
13
+ # the pattern is the `ConstantPathNode[...]` expression.
14
+ #
15
+ # The pattern gets compiled into an object that responds to #call by running
16
+ # the #compile method. This method itself will run back through YARP to
17
+ # parse the expression into a tree, then walk the tree to generate the
18
+ # necessary callable objects. For example, if you wanted to compile the
19
+ # expression above into a callable, you would:
20
+ #
21
+ # callable = YARP::Pattern.new("ConstantPathNode[ConstantReadNode[name: :YARP], ConstantReadNode[name: :Pattern]]").compile
22
+ # callable.call(node)
23
+ #
24
+ # The callable object returned by #compile is guaranteed to respond to #call
25
+ # with a single argument, which is the node to match against. It also is
26
+ # guaranteed to respond to #===, which means it itself can be used in a `case`
27
+ # expression, as in:
28
+ #
29
+ # case node
30
+ # when callable
31
+ # end
32
+ #
33
+ # If the query given to the initializer cannot be compiled into a valid
34
+ # matcher (either because of a syntax error or because it is using syntax we
35
+ # do not yet support) then a YARP::Pattern::CompilationError will be
36
+ # raised.
37
+ class Pattern
38
+ # Raised when the query given to a pattern is either invalid Ruby syntax or
39
+ # is using syntax that we don't yet support.
40
+ class CompilationError < StandardError
41
+ def initialize(repr)
42
+ super(<<~ERROR)
43
+ YARP was unable to compile the pattern you provided into a usable
44
+ expression. It failed on to understand the node represented by:
45
+
46
+ #{repr}
47
+
48
+ Note that not all syntax supported by Ruby's pattern matching syntax
49
+ is also supported by YARP's patterns. If you're using some syntax
50
+ that you believe should be supported, please open an issue on
51
+ GitHub at https://github.com/ruby/yarp/issues/new.
52
+ ERROR
53
+ end
54
+ end
55
+
56
+ attr_reader :query
57
+
58
+ def initialize(query)
59
+ @query = query
60
+ @compiled = nil
61
+ end
62
+
63
+ def compile
64
+ result = YARP.parse("case nil\nin #{query}\nend")
65
+ compile_node(result.value.statements.body.last.conditions.last.pattern)
66
+ end
67
+
68
+ def scan(root)
69
+ return to_enum(__method__, root) unless block_given?
70
+
71
+ @compiled ||= compile
72
+ queue = [root]
73
+
74
+ while (node = queue.shift)
75
+ yield node if @compiled.call(node)
76
+ queue.concat(node.child_nodes.compact)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # Shortcut for combining two procs into one that returns true if both return
83
+ # true.
84
+ def combine_and(left, right)
85
+ ->(other) { left.call(other) && right.call(other) }
86
+ end
87
+
88
+ # Shortcut for combining two procs into one that returns true if either
89
+ # returns true.
90
+ def combine_or(left, right)
91
+ ->(other) { left.call(other) || right.call(other) }
92
+ end
93
+
94
+ # Raise an error because the given node is not supported.
95
+ def compile_error(node)
96
+ raise CompilationError, node.inspect
97
+ end
98
+
99
+ # in [foo, bar, baz]
100
+ def compile_array_pattern_node(node)
101
+ compile_error(node) if !node.rest.nil? || node.posts.any?
102
+
103
+ constant = node.constant
104
+ compiled_constant = compile_node(constant) if constant
105
+
106
+ preprocessed = node.requireds.map { |required| compile_node(required) }
107
+
108
+ compiled_requireds = ->(other) do
109
+ deconstructed = other.deconstruct
110
+
111
+ deconstructed.length == preprocessed.length &&
112
+ preprocessed
113
+ .zip(deconstructed)
114
+ .all? { |(matcher, value)| matcher.call(value) }
115
+ end
116
+
117
+ if compiled_constant
118
+ combine_and(compiled_constant, compiled_requireds)
119
+ else
120
+ compiled_requireds
121
+ end
122
+ end
123
+
124
+ # in foo | bar
125
+ def compile_alternation_pattern_node(node)
126
+ combine_or(compile_node(node.left), compile_node(node.right))
127
+ end
128
+
129
+ # in YARP::ConstantReadNode
130
+ def compile_constant_path_node(node)
131
+ parent = node.parent
132
+
133
+ if parent.is_a?(ConstantReadNode) && parent.slice == "YARP"
134
+ compile_node(node.child)
135
+ else
136
+ compile_error(node)
137
+ end
138
+ end
139
+
140
+ # in ConstantReadNode
141
+ # in String
142
+ def compile_constant_read_node(node)
143
+ value = node.slice
144
+
145
+ if YARP.const_defined?(value, false)
146
+ clazz = YARP.const_get(value)
147
+
148
+ ->(other) { clazz === other }
149
+ elsif Object.const_defined?(value, false)
150
+ clazz = Object.const_get(value)
151
+
152
+ ->(other) { clazz === other }
153
+ else
154
+ compile_error(node)
155
+ end
156
+ end
157
+
158
+ # in InstanceVariableReadNode[name: Symbol]
159
+ # in { name: Symbol }
160
+ def compile_hash_pattern_node(node)
161
+ compile_error(node) unless node.kwrest.nil?
162
+ compiled_constant = compile_node(node.constant) if node.constant
163
+
164
+ preprocessed =
165
+ node.assocs.to_h do |assoc|
166
+ [assoc.key.unescaped.to_sym, compile_node(assoc.value)]
167
+ end
168
+
169
+ compiled_keywords = ->(other) do
170
+ deconstructed = other.deconstruct_keys(preprocessed.keys)
171
+
172
+ preprocessed.all? do |keyword, matcher|
173
+ deconstructed.key?(keyword) && matcher.call(deconstructed[keyword])
174
+ end
175
+ end
176
+
177
+ if compiled_constant
178
+ combine_and(compiled_constant, compiled_keywords)
179
+ else
180
+ compiled_keywords
181
+ end
182
+ end
183
+
184
+ # in nil
185
+ def compile_nil_node(node)
186
+ ->(attribute) { attribute.nil? }
187
+ end
188
+
189
+ # in /foo/
190
+ def compile_regular_expression_node(node)
191
+ regexp = Regexp.new(node.unescaped, node.closing[1..])
192
+
193
+ ->(attribute) { regexp === attribute }
194
+ end
195
+
196
+ # in ""
197
+ # in "foo"
198
+ def compile_string_node(node)
199
+ string = node.unescaped
200
+
201
+ ->(attribute) { string === attribute }
202
+ end
203
+
204
+ # in :+
205
+ # in :foo
206
+ def compile_symbol_node(node)
207
+ symbol = node.unescaped.to_sym
208
+
209
+ ->(attribute) { symbol === attribute }
210
+ end
211
+
212
+ # Compile any kind of node. Dispatch out to the individual compilation
213
+ # methods based on the type of node.
214
+ def compile_node(node)
215
+ case node
216
+ when AlternationPatternNode
217
+ compile_alternation_pattern_node(node)
218
+ when ArrayPatternNode
219
+ compile_array_pattern_node(node)
220
+ when ConstantPathNode
221
+ compile_constant_path_node(node)
222
+ when ConstantReadNode
223
+ compile_constant_read_node(node)
224
+ when HashPatternNode
225
+ compile_hash_pattern_node(node)
226
+ when NilNode
227
+ compile_nil_node(node)
228
+ when RegularExpressionNode
229
+ compile_regular_expression_node(node)
230
+ when StringNode
231
+ compile_string_node(node)
232
+ when SymbolNode
233
+ compile_symbol_node(node)
234
+ else
235
+ compile_error(node)
236
+ end
237
+ end
238
+ end
239
+ end