prism 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +172 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +62 -0
- data/LICENSE.md +7 -0
- data/Makefile +84 -0
- data/README.md +89 -0
- data/config.yml +2481 -0
- data/docs/build_system.md +74 -0
- data/docs/building.md +22 -0
- data/docs/configuration.md +60 -0
- data/docs/design.md +53 -0
- data/docs/encoding.md +117 -0
- data/docs/fuzzing.md +93 -0
- data/docs/heredocs.md +36 -0
- data/docs/mapping.md +117 -0
- data/docs/ripper.md +36 -0
- data/docs/ruby_api.md +25 -0
- data/docs/serialization.md +181 -0
- data/docs/testing.md +55 -0
- data/ext/prism/api_node.c +4725 -0
- data/ext/prism/api_pack.c +256 -0
- data/ext/prism/extconf.rb +136 -0
- data/ext/prism/extension.c +626 -0
- data/ext/prism/extension.h +18 -0
- data/include/prism/ast.h +1932 -0
- data/include/prism/defines.h +45 -0
- data/include/prism/diagnostic.h +231 -0
- data/include/prism/enc/pm_encoding.h +95 -0
- data/include/prism/node.h +41 -0
- data/include/prism/pack.h +141 -0
- data/include/prism/parser.h +418 -0
- data/include/prism/regexp.h +19 -0
- data/include/prism/unescape.h +48 -0
- data/include/prism/util/pm_buffer.h +51 -0
- data/include/prism/util/pm_char.h +91 -0
- data/include/prism/util/pm_constant_pool.h +78 -0
- data/include/prism/util/pm_list.h +67 -0
- data/include/prism/util/pm_memchr.h +14 -0
- data/include/prism/util/pm_newline_list.h +61 -0
- data/include/prism/util/pm_state_stack.h +24 -0
- data/include/prism/util/pm_string.h +61 -0
- data/include/prism/util/pm_string_list.h +25 -0
- data/include/prism/util/pm_strpbrk.h +29 -0
- data/include/prism/version.h +4 -0
- data/include/prism.h +82 -0
- data/lib/prism/compiler.rb +465 -0
- data/lib/prism/debug.rb +157 -0
- data/lib/prism/desugar_compiler.rb +206 -0
- data/lib/prism/dispatcher.rb +2051 -0
- data/lib/prism/dsl.rb +750 -0
- data/lib/prism/ffi.rb +251 -0
- data/lib/prism/lex_compat.rb +838 -0
- data/lib/prism/mutation_compiler.rb +718 -0
- data/lib/prism/node.rb +14540 -0
- data/lib/prism/node_ext.rb +55 -0
- data/lib/prism/node_inspector.rb +68 -0
- data/lib/prism/pack.rb +185 -0
- data/lib/prism/parse_result/comments.rb +172 -0
- data/lib/prism/parse_result/newlines.rb +60 -0
- data/lib/prism/parse_result.rb +266 -0
- data/lib/prism/pattern.rb +239 -0
- data/lib/prism/ripper_compat.rb +174 -0
- data/lib/prism/serialize.rb +662 -0
- data/lib/prism/visitor.rb +470 -0
- data/lib/prism.rb +64 -0
- data/prism.gemspec +113 -0
- data/src/diagnostic.c +287 -0
- data/src/enc/pm_big5.c +52 -0
- data/src/enc/pm_euc_jp.c +58 -0
- data/src/enc/pm_gbk.c +61 -0
- data/src/enc/pm_shift_jis.c +56 -0
- data/src/enc/pm_tables.c +507 -0
- data/src/enc/pm_unicode.c +2324 -0
- data/src/enc/pm_windows_31j.c +56 -0
- data/src/node.c +2633 -0
- data/src/pack.c +493 -0
- data/src/prettyprint.c +2136 -0
- data/src/prism.c +14587 -0
- data/src/regexp.c +580 -0
- data/src/serialize.c +1899 -0
- data/src/token_type.c +349 -0
- data/src/unescape.c +637 -0
- data/src/util/pm_buffer.c +103 -0
- data/src/util/pm_char.c +272 -0
- data/src/util/pm_constant_pool.c +252 -0
- data/src/util/pm_list.c +41 -0
- data/src/util/pm_memchr.c +33 -0
- data/src/util/pm_newline_list.c +134 -0
- data/src/util/pm_state_stack.c +19 -0
- data/src/util/pm_string.c +200 -0
- data/src/util/pm_string_list.c +29 -0
- data/src/util/pm_strncasecmp.c +17 -0
- data/src/util/pm_strpbrk.c +66 -0
- metadata +138 -0
@@ -0,0 +1,266 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prism
|
4
|
+
# This represents a source of Ruby code that has been parsed. It is used in
|
5
|
+
# conjunction with locations to allow them to resolve line numbers and source
|
6
|
+
# ranges.
|
7
|
+
class Source
|
8
|
+
attr_reader :source, :offsets
|
9
|
+
|
10
|
+
def initialize(source, offsets = compute_offsets(source))
|
11
|
+
@source = source
|
12
|
+
@offsets = offsets
|
13
|
+
end
|
14
|
+
|
15
|
+
def slice(offset, length)
|
16
|
+
source.byteslice(offset, length)
|
17
|
+
end
|
18
|
+
|
19
|
+
def line(value)
|
20
|
+
offsets.bsearch_index { |offset| offset > value } || offsets.length
|
21
|
+
end
|
22
|
+
|
23
|
+
def line_offset(value)
|
24
|
+
offsets[line(value) - 1]
|
25
|
+
end
|
26
|
+
|
27
|
+
def column(value)
|
28
|
+
value - offsets[line(value) - 1]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def compute_offsets(code)
|
34
|
+
offsets = [0]
|
35
|
+
code.b.scan("\n") { offsets << $~.end(0) }
|
36
|
+
offsets
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# This represents a location in the source.
|
41
|
+
class Location
|
42
|
+
# A Source object that is used to determine more information from the given
|
43
|
+
# offset and length.
|
44
|
+
protected attr_reader :source
|
45
|
+
|
46
|
+
# The byte offset from the beginning of the source where this location
|
47
|
+
# starts.
|
48
|
+
attr_reader :start_offset
|
49
|
+
|
50
|
+
# The length of this location in bytes.
|
51
|
+
attr_reader :length
|
52
|
+
|
53
|
+
# The list of comments attached to this location
|
54
|
+
attr_reader :comments
|
55
|
+
|
56
|
+
def initialize(source, start_offset, length)
|
57
|
+
@source = source
|
58
|
+
@start_offset = start_offset
|
59
|
+
@length = length
|
60
|
+
@comments = []
|
61
|
+
end
|
62
|
+
|
63
|
+
# Create a new location object with the given options.
|
64
|
+
def copy(**options)
|
65
|
+
Location.new(
|
66
|
+
options.fetch(:source) { source },
|
67
|
+
options.fetch(:start_offset) { start_offset },
|
68
|
+
options.fetch(:length) { length }
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a string representation of this location.
|
73
|
+
def inspect
|
74
|
+
"#<Prism::Location @start_offset=#{@start_offset} @length=#{@length} start_line=#{start_line}>"
|
75
|
+
end
|
76
|
+
|
77
|
+
# The source code that this location represents.
|
78
|
+
def slice
|
79
|
+
source.slice(start_offset, length)
|
80
|
+
end
|
81
|
+
|
82
|
+
# The byte offset from the beginning of the source where this location ends.
|
83
|
+
def end_offset
|
84
|
+
start_offset + length
|
85
|
+
end
|
86
|
+
|
87
|
+
# The line number where this location starts.
|
88
|
+
def start_line
|
89
|
+
source.line(start_offset)
|
90
|
+
end
|
91
|
+
|
92
|
+
# The content of the line where this location starts before this location.
|
93
|
+
def start_line_slice
|
94
|
+
offset = source.line_offset(start_offset)
|
95
|
+
source.slice(offset, start_offset - offset)
|
96
|
+
end
|
97
|
+
|
98
|
+
# The line number where this location ends.
|
99
|
+
def end_line
|
100
|
+
source.line(end_offset - 1)
|
101
|
+
end
|
102
|
+
|
103
|
+
# The column number in bytes where this location starts from the start of
|
104
|
+
# the line.
|
105
|
+
def start_column
|
106
|
+
source.column(start_offset)
|
107
|
+
end
|
108
|
+
|
109
|
+
# The column number in bytes where this location ends from the start of the
|
110
|
+
# line.
|
111
|
+
def end_column
|
112
|
+
source.column(end_offset)
|
113
|
+
end
|
114
|
+
|
115
|
+
def deconstruct_keys(keys)
|
116
|
+
{ start_offset: start_offset, end_offset: end_offset }
|
117
|
+
end
|
118
|
+
|
119
|
+
def pretty_print(q)
|
120
|
+
q.text("(#{start_line},#{start_column})-(#{end_line},#{end_column}))")
|
121
|
+
end
|
122
|
+
|
123
|
+
def ==(other)
|
124
|
+
other.is_a?(Location) &&
|
125
|
+
other.start_offset == start_offset &&
|
126
|
+
other.end_offset == end_offset
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns a new location that stretches from this location to the given
|
130
|
+
# other location. Raises an error if this location is not before the other
|
131
|
+
# location or if they don't share the same source.
|
132
|
+
def join(other)
|
133
|
+
raise "Incompatible sources" if source != other.source
|
134
|
+
raise "Incompatible locations" if start_offset > other.start_offset
|
135
|
+
|
136
|
+
Location.new(source, start_offset, other.end_offset - start_offset)
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.null
|
140
|
+
new(0, 0)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# This represents a comment that was encountered during parsing.
|
145
|
+
class Comment
|
146
|
+
TYPES = [:inline, :embdoc, :__END__]
|
147
|
+
|
148
|
+
attr_reader :type, :location
|
149
|
+
|
150
|
+
def initialize(type, location)
|
151
|
+
@type = type
|
152
|
+
@location = location
|
153
|
+
end
|
154
|
+
|
155
|
+
def deconstruct_keys(keys)
|
156
|
+
{ type: type, location: location }
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns true if the comment happens on the same line as other code and false if the comment is by itself
|
160
|
+
def trailing?
|
161
|
+
type == :inline && !location.start_line_slice.strip.empty?
|
162
|
+
end
|
163
|
+
|
164
|
+
def inspect
|
165
|
+
"#<Prism::Comment @type=#{@type.inspect} @location=#{@location.inspect}>"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# This represents an error that was encountered during parsing.
|
170
|
+
class ParseError
|
171
|
+
attr_reader :message, :location
|
172
|
+
|
173
|
+
def initialize(message, location)
|
174
|
+
@message = message
|
175
|
+
@location = location
|
176
|
+
end
|
177
|
+
|
178
|
+
def deconstruct_keys(keys)
|
179
|
+
{ message: message, location: location }
|
180
|
+
end
|
181
|
+
|
182
|
+
def inspect
|
183
|
+
"#<Prism::ParseError @message=#{@message.inspect} @location=#{@location.inspect}>"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# This represents a warning that was encountered during parsing.
|
188
|
+
class ParseWarning
|
189
|
+
attr_reader :message, :location
|
190
|
+
|
191
|
+
def initialize(message, location)
|
192
|
+
@message = message
|
193
|
+
@location = location
|
194
|
+
end
|
195
|
+
|
196
|
+
def deconstruct_keys(keys)
|
197
|
+
{ message: message, location: location }
|
198
|
+
end
|
199
|
+
|
200
|
+
def inspect
|
201
|
+
"#<Prism::ParseWarning @message=#{@message.inspect} @location=#{@location.inspect}>"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# This represents the result of a call to ::parse or ::parse_file. It contains
|
206
|
+
# the AST, any comments that were encounters, and any errors that were
|
207
|
+
# encountered.
|
208
|
+
class ParseResult
|
209
|
+
attr_reader :value, :comments, :errors, :warnings, :source
|
210
|
+
|
211
|
+
def initialize(value, comments, errors, warnings, source)
|
212
|
+
@value = value
|
213
|
+
@comments = comments
|
214
|
+
@errors = errors
|
215
|
+
@warnings = warnings
|
216
|
+
@source = source
|
217
|
+
end
|
218
|
+
|
219
|
+
def deconstruct_keys(keys)
|
220
|
+
{ value: value, comments: comments, errors: errors, warnings: warnings }
|
221
|
+
end
|
222
|
+
|
223
|
+
def success?
|
224
|
+
errors.empty?
|
225
|
+
end
|
226
|
+
|
227
|
+
def failure?
|
228
|
+
!success?
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# This represents a token from the Ruby source.
|
233
|
+
class Token
|
234
|
+
attr_reader :type, :value, :location
|
235
|
+
|
236
|
+
def initialize(type, value, location)
|
237
|
+
@type = type
|
238
|
+
@value = value
|
239
|
+
@location = location
|
240
|
+
end
|
241
|
+
|
242
|
+
def deconstruct_keys(keys)
|
243
|
+
{ type: type, value: value, location: location }
|
244
|
+
end
|
245
|
+
|
246
|
+
def pretty_print(q)
|
247
|
+
q.group do
|
248
|
+
q.text(type.to_s)
|
249
|
+
self.location.pretty_print(q)
|
250
|
+
q.text("(")
|
251
|
+
q.nest(2) do
|
252
|
+
q.breakable("")
|
253
|
+
q.pp(value)
|
254
|
+
end
|
255
|
+
q.breakable("")
|
256
|
+
q.text(")")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def ==(other)
|
261
|
+
other.is_a?(Token) &&
|
262
|
+
other.type == type &&
|
263
|
+
other.value == value
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prism
|
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: :Prism], 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 Prism 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 = Prism::Pattern.new("ConstantPathNode[ConstantReadNode[name: :Prism], 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 Prism::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
|
+
prism 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 prism'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/prism/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 = Prism.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.compact_child_nodes)
|
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 Prism::ConstantReadNode
|
130
|
+
def compile_constant_path_node(node)
|
131
|
+
parent = node.parent
|
132
|
+
|
133
|
+
if parent.is_a?(ConstantReadNode) && parent.slice == "Prism"
|
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 Prism.const_defined?(value, false)
|
146
|
+
clazz = Prism.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
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ripper"
|
4
|
+
|
5
|
+
module Prism
|
6
|
+
# This class is meant to provide a compatibility layer between prism and
|
7
|
+
# Ripper. It functions by parsing the entire tree first and then walking it
|
8
|
+
# and executing each of the Ripper callbacks as it goes.
|
9
|
+
#
|
10
|
+
# This class is going to necessarily be slower than the native Ripper API. It
|
11
|
+
# is meant as a stopgap until developers migrate to using prism. It is also
|
12
|
+
# meant as a test harness for the prism parser.
|
13
|
+
class RipperCompat
|
14
|
+
# This class mirrors the ::Ripper::SexpBuilder subclass of ::Ripper that
|
15
|
+
# returns the arrays of [type, *children].
|
16
|
+
class SexpBuilder < RipperCompat
|
17
|
+
private
|
18
|
+
|
19
|
+
Ripper::PARSER_EVENTS.each do |event|
|
20
|
+
define_method(:"on_#{event}") do |*args|
|
21
|
+
[event, *args]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Ripper::SCANNER_EVENTS.each do |event|
|
26
|
+
define_method(:"on_#{event}") do |value|
|
27
|
+
[:"@#{event}", value, [lineno, column]]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# This class mirrors the ::Ripper::SexpBuilderPP subclass of ::Ripper that
|
33
|
+
# returns the same values as ::Ripper::SexpBuilder except with a couple of
|
34
|
+
# niceties that flatten linked lists into arrays.
|
35
|
+
class SexpBuilderPP < SexpBuilder
|
36
|
+
private
|
37
|
+
|
38
|
+
def _dispatch_event_new
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
|
42
|
+
def _dispatch_event_push(list, item)
|
43
|
+
list << item
|
44
|
+
list
|
45
|
+
end
|
46
|
+
|
47
|
+
Ripper::PARSER_EVENT_TABLE.each do |event, arity|
|
48
|
+
case event
|
49
|
+
when /_new\z/
|
50
|
+
alias_method :"on_#{event}", :_dispatch_event_new if arity == 0
|
51
|
+
when /_add\z/
|
52
|
+
alias_method :"on_#{event}", :_dispatch_event_push
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :source, :lineno, :column
|
58
|
+
|
59
|
+
def initialize(source)
|
60
|
+
@source = source
|
61
|
+
@result = nil
|
62
|
+
@lineno = nil
|
63
|
+
@column = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
############################################################################
|
67
|
+
# Public interface
|
68
|
+
############################################################################
|
69
|
+
|
70
|
+
def error?
|
71
|
+
result.errors.any?
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse
|
75
|
+
result.value.accept(self) unless error?
|
76
|
+
end
|
77
|
+
|
78
|
+
############################################################################
|
79
|
+
# Visitor methods
|
80
|
+
############################################################################
|
81
|
+
|
82
|
+
def visit(node)
|
83
|
+
node&.accept(self)
|
84
|
+
end
|
85
|
+
|
86
|
+
def visit_call_node(node)
|
87
|
+
if !node.opening_loc && node.arguments.arguments.length == 1
|
88
|
+
bounds(node.receiver.location)
|
89
|
+
left = visit(node.receiver)
|
90
|
+
|
91
|
+
bounds(node.arguments.arguments.first.location)
|
92
|
+
right = visit(node.arguments.arguments.first)
|
93
|
+
|
94
|
+
on_binary(left, source[node.message_loc.start_offset...node.message_loc.end_offset].to_sym, right)
|
95
|
+
else
|
96
|
+
raise NotImplementedError
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def visit_integer_node(node)
|
101
|
+
bounds(node.location)
|
102
|
+
on_int(source[node.location.start_offset...node.location.end_offset])
|
103
|
+
end
|
104
|
+
|
105
|
+
def visit_statements_node(node)
|
106
|
+
bounds(node.location)
|
107
|
+
node.body.inject(on_stmts_new) do |stmts, stmt|
|
108
|
+
on_stmts_add(stmts, visit(stmt))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def visit_token(node)
|
113
|
+
bounds(node.location)
|
114
|
+
|
115
|
+
case node.type
|
116
|
+
when :MINUS
|
117
|
+
on_op(node.value)
|
118
|
+
when :PLUS
|
119
|
+
on_op(node.value)
|
120
|
+
else
|
121
|
+
raise NotImplementedError, "Unknown token: #{node.type}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def visit_program_node(node)
|
126
|
+
bounds(node.location)
|
127
|
+
on_program(visit(node.statements))
|
128
|
+
end
|
129
|
+
|
130
|
+
############################################################################
|
131
|
+
# Entrypoints for subclasses
|
132
|
+
############################################################################
|
133
|
+
|
134
|
+
# This is a convenience method that runs the SexpBuilder subclass parser.
|
135
|
+
def self.sexp_raw(source)
|
136
|
+
SexpBuilder.new(source).parse
|
137
|
+
end
|
138
|
+
|
139
|
+
# This is a convenience method that runs the SexpBuilderPP subclass parser.
|
140
|
+
def self.sexp(source)
|
141
|
+
SexpBuilderPP.new(source).parse
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# This method is responsible for updating lineno and column information
|
147
|
+
# to reflect the current node.
|
148
|
+
#
|
149
|
+
# This method could be drastically improved with some caching on the start
|
150
|
+
# of every line, but for now it's good enough.
|
151
|
+
def bounds(location)
|
152
|
+
start_offset = location.start_offset
|
153
|
+
|
154
|
+
@lineno = source[0..start_offset].count("\n") + 1
|
155
|
+
@column = start_offset - (source.rindex("\n", start_offset) || 0)
|
156
|
+
end
|
157
|
+
|
158
|
+
def result
|
159
|
+
@result ||= Prism.parse(source)
|
160
|
+
end
|
161
|
+
|
162
|
+
def _dispatch0; end
|
163
|
+
def _dispatch1(_); end
|
164
|
+
def _dispatch2(_, _); end
|
165
|
+
def _dispatch3(_, _, _); end
|
166
|
+
def _dispatch4(_, _, _, _); end
|
167
|
+
def _dispatch5(_, _, _, _, _); end
|
168
|
+
def _dispatch7(_, _, _, _, _, _, _); end
|
169
|
+
|
170
|
+
(Ripper::SCANNER_EVENT_TABLE.merge(Ripper::PARSER_EVENT_TABLE)).each do |event, arity|
|
171
|
+
alias_method :"on_#{event}", :"_dispatch#{arity}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|