prism 0.13.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.
- 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
|