parser 2.6.0.0 → 3.1.0.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 +4 -4
- data/lib/parser/all.rb +3 -0
- data/lib/parser/ast/processor.rb +48 -1
- data/lib/parser/base.rb +30 -6
- data/lib/parser/builders/default.rb +670 -38
- data/lib/parser/context.rb +24 -26
- data/lib/parser/current.rb +36 -9
- data/lib/parser/current_arg_stack.rb +46 -0
- data/lib/parser/diagnostic/engine.rb +1 -2
- data/lib/parser/diagnostic.rb +1 -1
- data/lib/parser/lexer/dedenter.rb +58 -49
- data/lib/parser/lexer/explanation.rb +1 -1
- data/lib/parser/lexer.rb +13837 -11893
- data/lib/parser/macruby.rb +2544 -2489
- data/lib/parser/max_numparam_stack.rb +56 -0
- data/lib/parser/messages.rb +78 -44
- data/lib/parser/meta.rb +13 -3
- data/lib/parser/ruby18.rb +2313 -2259
- data/lib/parser/ruby19.rb +2537 -2488
- data/lib/parser/ruby20.rb +2724 -2673
- data/lib/parser/ruby21.rb +2766 -2727
- data/lib/parser/ruby22.rb +2683 -2628
- data/lib/parser/ruby23.rb +2796 -2755
- data/lib/parser/ruby24.rb +2812 -2771
- data/lib/parser/ruby25.rb +2703 -2670
- data/lib/parser/ruby26.rb +2794 -2747
- data/lib/parser/ruby27.rb +7914 -0
- data/lib/parser/ruby28.rb +8047 -0
- data/lib/parser/ruby30.rb +8096 -0
- data/lib/parser/ruby31.rb +8354 -0
- data/lib/parser/rubymotion.rb +2527 -2485
- data/lib/parser/runner/ruby_parse.rb +2 -2
- data/lib/parser/runner/ruby_rewrite.rb +2 -2
- data/lib/parser/runner.rb +36 -2
- data/lib/parser/source/buffer.rb +53 -28
- data/lib/parser/source/comment/associator.rb +31 -8
- data/lib/parser/source/comment.rb +14 -1
- data/lib/parser/source/map/method_definition.rb +25 -0
- data/lib/parser/source/range.rb +19 -3
- data/lib/parser/source/tree_rewriter/action.rb +137 -28
- data/lib/parser/source/tree_rewriter.rb +144 -14
- data/lib/parser/static_environment.rb +23 -0
- data/lib/parser/tree_rewriter.rb +3 -3
- data/lib/parser/variables_stack.rb +36 -0
- data/lib/parser/version.rb +1 -1
- data/lib/parser.rb +4 -0
- data/parser.gemspec +12 -19
- metadata +34 -99
- data/.gitignore +0 -32
- data/.travis.yml +0 -45
- data/.yardopts +0 -21
- data/CHANGELOG.md +0 -943
- data/CONTRIBUTING.md +0 -17
- data/Gemfile +0 -10
- data/README.md +0 -301
- data/Rakefile +0 -165
- data/ci/run_rubocop_specs +0 -14
- data/doc/AST_FORMAT.md +0 -1735
- data/doc/CUSTOMIZATION.md +0 -37
- data/doc/INTERNALS.md +0 -21
- data/doc/css/.gitkeep +0 -0
- data/doc/css/common.css +0 -68
- data/lib/parser/lexer.rl +0 -2383
- data/lib/parser/macruby.y +0 -2198
- data/lib/parser/ruby18.y +0 -1934
- data/lib/parser/ruby19.y +0 -2175
- data/lib/parser/ruby20.y +0 -2353
- data/lib/parser/ruby21.y +0 -2357
- data/lib/parser/ruby22.y +0 -2364
- data/lib/parser/ruby23.y +0 -2370
- data/lib/parser/ruby24.y +0 -2408
- data/lib/parser/ruby25.y +0 -2405
- data/lib/parser/ruby26.y +0 -2413
- data/lib/parser/rubymotion.y +0 -2182
- data/test/bug_163/fixtures/input.rb +0 -5
- data/test/bug_163/fixtures/output.rb +0 -5
- data/test/bug_163/rewriter.rb +0 -20
- data/test/helper.rb +0 -52
- data/test/parse_helper.rb +0 -315
- data/test/racc_coverage_helper.rb +0 -133
- data/test/test_base.rb +0 -31
- data/test/test_current.rb +0 -27
- data/test/test_diagnostic.rb +0 -96
- data/test/test_diagnostic_engine.rb +0 -62
- data/test/test_encoding.rb +0 -99
- data/test/test_lexer.rb +0 -3543
- data/test/test_lexer_stack_state.rb +0 -78
- data/test/test_parse_helper.rb +0 -80
- data/test/test_parser.rb +0 -7087
- data/test/test_runner_rewrite.rb +0 -47
- data/test/test_source_buffer.rb +0 -162
- data/test/test_source_comment.rb +0 -36
- data/test/test_source_comment_associator.rb +0 -367
- data/test/test_source_map.rb +0 -15
- data/test/test_source_range.rb +0 -172
- data/test/test_source_rewriter.rb +0 -541
- data/test/test_source_rewriter_action.rb +0 -46
- data/test/test_source_tree_rewriter.rb +0 -173
- data/test/test_static_environment.rb +0 -45
- data/test/using_tree_rewriter/fixtures/input.rb +0 -3
- data/test/using_tree_rewriter/fixtures/output.rb +0 -3
- data/test/using_tree_rewriter/using_tree_rewriter.rb +0 -9
@@ -123,7 +123,7 @@ module Parser
|
|
123
123
|
opts.on '--emit-ruby', 'Emit S-expressions as valid Ruby code' do
|
124
124
|
@emit_ruby = true
|
125
125
|
end
|
126
|
-
|
126
|
+
|
127
127
|
opts.on '--emit-json', 'Emit S-expressions as valid JSON' do
|
128
128
|
@emit_json = true
|
129
129
|
end
|
@@ -146,7 +146,7 @@ module Parser
|
|
146
146
|
if @emit_ruby
|
147
147
|
puts ast.inspect
|
148
148
|
elsif @emit_json
|
149
|
-
puts JSON.generate(ast.to_sexp_array)
|
149
|
+
puts(ast ? JSON.generate(ast.to_sexp_array) : nil)
|
150
150
|
else
|
151
151
|
puts ast.to_s
|
152
152
|
end
|
@@ -55,8 +55,8 @@ module Parser
|
|
55
55
|
new_source = rewriter.rewrite(buffer, ast)
|
56
56
|
|
57
57
|
new_buffer = Source::Buffer.new(initial_buffer.name +
|
58
|
-
'|after ' + rewriter_class.name
|
59
|
-
|
58
|
+
'|after ' + rewriter_class.name,
|
59
|
+
source: new_source)
|
60
60
|
|
61
61
|
@parser.reset
|
62
62
|
new_ast = @parser.parse(new_buffer)
|
data/lib/parser/runner.rb
CHANGED
@@ -14,9 +14,8 @@ module Parser
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def initialize
|
17
|
-
Parser::Builders::Default.modernize
|
18
|
-
|
19
17
|
@option_parser = OptionParser.new { |opts| setup_option_parsing(opts) }
|
18
|
+
@legacy = {}
|
20
19
|
@parser_class = nil
|
21
20
|
@parser = nil
|
22
21
|
@files = []
|
@@ -30,6 +29,7 @@ module Parser
|
|
30
29
|
|
31
30
|
def execute(options)
|
32
31
|
parse_options(options)
|
32
|
+
setup_builder_default
|
33
33
|
prepare_parser
|
34
34
|
|
35
35
|
process_all_input
|
@@ -37,6 +37,8 @@ module Parser
|
|
37
37
|
|
38
38
|
private
|
39
39
|
|
40
|
+
LEGACY_MODES = %i[lambda procarg0 encoding index arg_inside_procarg0 forward_arg kwargs match_pattern].freeze
|
41
|
+
|
40
42
|
def runner_name
|
41
43
|
raise NotImplementedError, "implement #{self.class}##{__callee__}"
|
42
44
|
end
|
@@ -106,6 +108,21 @@ module Parser
|
|
106
108
|
@parser_class = Parser::Ruby26
|
107
109
|
end
|
108
110
|
|
111
|
+
opts.on '--27', 'Parse as Ruby 2.7 would' do
|
112
|
+
require 'parser/ruby27'
|
113
|
+
@parser_class = Parser::Ruby27
|
114
|
+
end
|
115
|
+
|
116
|
+
opts.on '--30', 'Parse as Ruby 3.0 would' do
|
117
|
+
require 'parser/ruby30'
|
118
|
+
@parser_class = Parser::Ruby30
|
119
|
+
end
|
120
|
+
|
121
|
+
opts.on '--31', 'Parse as Ruby 3.1 would' do
|
122
|
+
require 'parser/ruby31'
|
123
|
+
@parser_class = Parser::Ruby31
|
124
|
+
end
|
125
|
+
|
109
126
|
opts.on '--mac', 'Parse as MacRuby 0.12 would' do
|
110
127
|
require 'parser/macruby'
|
111
128
|
@parser_class = Parser::MacRuby
|
@@ -116,6 +133,17 @@ module Parser
|
|
116
133
|
@parser_class = Parser::RubyMotion
|
117
134
|
end
|
118
135
|
|
136
|
+
opts.on '--legacy', "Parse with all legacy modes" do
|
137
|
+
@legacy = Hash.new(true)
|
138
|
+
end
|
139
|
+
|
140
|
+
LEGACY_MODES.each do |mode|
|
141
|
+
opt_name = "--legacy-#{mode.to_s.gsub('_', '-')}"
|
142
|
+
opts.on opt_name, "Parse with legacy mode for emit_#{mode}" do
|
143
|
+
@legacy[mode] = true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
119
147
|
opts.on '-w', '--warnings', 'Enable warnings' do |w|
|
120
148
|
@warnings = w
|
121
149
|
end
|
@@ -154,6 +182,12 @@ module Parser
|
|
154
182
|
end
|
155
183
|
end
|
156
184
|
|
185
|
+
def setup_builder_default
|
186
|
+
LEGACY_MODES.each do |mode|
|
187
|
+
Parser::Builders::Default.send(:"emit_#{mode}=", !@legacy[mode])
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
157
191
|
def prepare_parser
|
158
192
|
@parser = @parser_class.new
|
159
193
|
|
data/lib/parser/source/buffer.rb
CHANGED
@@ -102,7 +102,7 @@ module Parser
|
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
|
-
def initialize(name, first_line = 1)
|
105
|
+
def initialize(name, first_line = 1, source: nil)
|
106
106
|
@name = name.to_s
|
107
107
|
@source = nil
|
108
108
|
@first_line = first_line
|
@@ -114,8 +114,9 @@ module Parser
|
|
114
114
|
@slice_source = nil
|
115
115
|
|
116
116
|
# Cache for fast lookup
|
117
|
-
@
|
118
|
-
|
117
|
+
@line_index_for_position = {}
|
118
|
+
|
119
|
+
self.source = source if source
|
119
120
|
end
|
120
121
|
|
121
122
|
##
|
@@ -205,9 +206,10 @@ module Parser
|
|
205
206
|
# @return [[Integer, Integer]] `[line, column]`
|
206
207
|
#
|
207
208
|
def decompose_position(position)
|
208
|
-
|
209
|
+
line_index = line_index_for_position(position)
|
210
|
+
line_begin = line_begins[line_index]
|
209
211
|
|
210
|
-
[ @first_line +
|
212
|
+
[ @first_line + line_index , position - line_begin ]
|
211
213
|
end
|
212
214
|
|
213
215
|
##
|
@@ -218,10 +220,7 @@ module Parser
|
|
218
220
|
# @api private
|
219
221
|
#
|
220
222
|
def line_for_position(position)
|
221
|
-
|
222
|
-
line_no, _ = line_for(position)
|
223
|
-
@first_line + line_no
|
224
|
-
end
|
223
|
+
line_index_for_position(position) + @first_line
|
225
224
|
end
|
226
225
|
|
227
226
|
##
|
@@ -232,10 +231,8 @@ module Parser
|
|
232
231
|
# @api private
|
233
232
|
#
|
234
233
|
def column_for_position(position)
|
235
|
-
|
236
|
-
|
237
|
-
position - line_begin
|
238
|
-
end
|
234
|
+
line_index = line_index_for_position(position)
|
235
|
+
position - line_begins[line_index]
|
239
236
|
end
|
240
237
|
|
241
238
|
##
|
@@ -276,15 +273,13 @@ module Parser
|
|
276
273
|
# @raise [IndexError] if `lineno` is out of bounds
|
277
274
|
#
|
278
275
|
def line_range(lineno)
|
279
|
-
index = lineno - @first_line
|
280
|
-
if index
|
276
|
+
index = lineno - @first_line
|
277
|
+
if index < 0 || index + 1 >= line_begins.size
|
281
278
|
raise IndexError, 'Parser::Source::Buffer: range for line ' \
|
282
279
|
"#{lineno} requested, valid line numbers are #{@first_line}.." \
|
283
|
-
"#{@first_line + line_begins.size -
|
284
|
-
elsif index == line_begins.size
|
285
|
-
Range.new(self, line_begins[-index][1], @source.size)
|
280
|
+
"#{@first_line + line_begins.size - 2}"
|
286
281
|
else
|
287
|
-
Range.new(self, line_begins[
|
282
|
+
Range.new(self, line_begins[index], line_begins[index + 1] - 1)
|
288
283
|
end
|
289
284
|
end
|
290
285
|
|
@@ -301,27 +296,57 @@ module Parser
|
|
301
296
|
# @return [Integer]
|
302
297
|
#
|
303
298
|
def last_line
|
304
|
-
line_begins.size + @first_line -
|
299
|
+
line_begins.size + @first_line - 2
|
300
|
+
end
|
301
|
+
|
302
|
+
# :nodoc:
|
303
|
+
def freeze
|
304
|
+
source_lines; line_begins; source_range # build cache
|
305
|
+
super
|
306
|
+
end
|
307
|
+
|
308
|
+
# :nodoc:
|
309
|
+
def inspect
|
310
|
+
"#<#{self.class} #{name}>"
|
305
311
|
end
|
306
312
|
|
307
313
|
private
|
308
314
|
|
315
|
+
# @returns [0, line_begin_of_line_1, ..., source.size + 1]
|
309
316
|
def line_begins
|
310
|
-
|
311
|
-
|
312
|
-
|
317
|
+
@line_begins ||= begin
|
318
|
+
begins = [0]
|
319
|
+
index = 0
|
313
320
|
while index = @source.index("\n".freeze, index)
|
314
321
|
index += 1
|
315
|
-
|
322
|
+
begins << index
|
316
323
|
end
|
324
|
+
begins << @source.size + 1
|
325
|
+
begins
|
317
326
|
end
|
327
|
+
end
|
318
328
|
|
319
|
-
|
329
|
+
# @returns 0-based line index of position
|
330
|
+
def line_index_for_position(position)
|
331
|
+
@line_index_for_position[position] || begin
|
332
|
+
index = bsearch(line_begins, position) - 1
|
333
|
+
@line_index_for_position[position] = index unless @line_index_for_position.frozen?
|
334
|
+
index
|
335
|
+
end
|
320
336
|
end
|
321
337
|
|
322
|
-
|
323
|
-
line_begins
|
324
|
-
|
338
|
+
if Array.method_defined?(:bsearch_index) # RUBY_VERSION >= 2.3
|
339
|
+
def bsearch(line_begins, position)
|
340
|
+
line_begins.bsearch_index do |line_begin|
|
341
|
+
position < line_begin
|
342
|
+
end || line_begins.size - 1 # || only for out of bound values
|
343
|
+
end
|
344
|
+
else
|
345
|
+
def bsearch(line_begins, position)
|
346
|
+
@line_range ||= 0...line_begins.size
|
347
|
+
@line_range.bsearch do |i|
|
348
|
+
position < line_begins[i]
|
349
|
+
end || line_begins.size - 1 # || only for out of bound values
|
325
350
|
end
|
326
351
|
end
|
327
352
|
end
|
@@ -84,12 +84,24 @@ module Parser
|
|
84
84
|
#
|
85
85
|
# Note that {associate} produces unexpected result for nodes which are
|
86
86
|
# equal but have distinct locations; comments for these nodes are merged.
|
87
|
+
# You may prefer using {associate_by_identity} or {associate_locations}.
|
87
88
|
#
|
88
89
|
# @return [Hash<Parser::AST::Node, Array<Parser::Source::Comment>>]
|
89
90
|
# @deprecated Use {associate_locations}.
|
90
91
|
#
|
91
92
|
def associate
|
92
|
-
@
|
93
|
+
@map_using = :eql
|
94
|
+
do_associate
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Same as {associate}, but compares by identity, thus producing an unambiguous
|
99
|
+
# result even in presence of equal nodes.
|
100
|
+
#
|
101
|
+
# @return [Hash<Parser::Source::Node, Array<Parser::Source::Comment>>]
|
102
|
+
#
|
103
|
+
def associate_locations
|
104
|
+
@map_using = :location
|
93
105
|
do_associate
|
94
106
|
end
|
95
107
|
|
@@ -100,15 +112,29 @@ module Parser
|
|
100
112
|
#
|
101
113
|
# @return [Hash<Parser::Source::Map, Array<Parser::Source::Comment>>]
|
102
114
|
#
|
103
|
-
def
|
104
|
-
@
|
115
|
+
def associate_by_identity
|
116
|
+
@map_using = :identity
|
105
117
|
do_associate
|
106
118
|
end
|
107
119
|
|
108
120
|
private
|
109
121
|
|
122
|
+
POSTFIX_TYPES = Set[:if, :while, :while_post, :until, :until_post, :masgn].freeze
|
123
|
+
def children_in_source_order(node)
|
124
|
+
if POSTFIX_TYPES.include?(node.type)
|
125
|
+
# All these types have either nodes with expressions, or `nil`
|
126
|
+
# so a compact will do, but they need to be sorted.
|
127
|
+
node.children.compact.sort_by { |child| child.loc.expression.begin_pos }
|
128
|
+
else
|
129
|
+
node.children.select do |child|
|
130
|
+
child.is_a?(AST::Node) && child.loc && child.loc.expression
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
110
135
|
def do_associate
|
111
136
|
@mapping = Hash.new { |h, k| h[k] = [] }
|
137
|
+
@mapping.compare_by_identity if @map_using == :identity
|
112
138
|
@comment_num = -1
|
113
139
|
advance_comment
|
114
140
|
|
@@ -131,10 +157,7 @@ module Parser
|
|
131
157
|
node_loc = node.location
|
132
158
|
if @current_comment.location.line <= node_loc.last_line ||
|
133
159
|
node_loc.is_a?(Map::Heredoc)
|
134
|
-
node.
|
135
|
-
next unless child.is_a?(AST::Node) && child.loc && child.loc.expression
|
136
|
-
visit(child)
|
137
|
-
end
|
160
|
+
children_in_source_order(node).each { |child| visit(child) }
|
138
161
|
|
139
162
|
process_trailing_comments(node)
|
140
163
|
end
|
@@ -181,7 +204,7 @@ module Parser
|
|
181
204
|
end
|
182
205
|
|
183
206
|
def associate_and_advance_comment(node)
|
184
|
-
key = @
|
207
|
+
key = @map_using == :location ? node.location : node
|
185
208
|
@mapping[key] << @current_comment
|
186
209
|
advance_comment
|
187
210
|
end
|
@@ -10,7 +10,7 @@ module Parser
|
|
10
10
|
# @return [String]
|
11
11
|
#
|
12
12
|
# @!attribute [r] location
|
13
|
-
# @return [Parser::Source::
|
13
|
+
# @return [Parser::Source::Range]
|
14
14
|
#
|
15
15
|
# @api public
|
16
16
|
#
|
@@ -48,6 +48,19 @@ module Parser
|
|
48
48
|
associator.associate_locations
|
49
49
|
end
|
50
50
|
|
51
|
+
##
|
52
|
+
# Associate `comments` with `ast` nodes using identity.
|
53
|
+
#
|
54
|
+
# @param [Parser::AST::Node] ast
|
55
|
+
# @param [Array<Comment>] comments
|
56
|
+
# @return [Hash<Parser::Source::Node, Array<Comment>>]
|
57
|
+
# @see Parser::Source::Comment::Associator#associate_by_identity
|
58
|
+
#
|
59
|
+
def self.associate_by_identity(ast, comments)
|
60
|
+
associator = Associator.new(ast, comments)
|
61
|
+
associator.associate_by_identity
|
62
|
+
end
|
63
|
+
|
51
64
|
##
|
52
65
|
# @param [Parser::Source::Range] range
|
53
66
|
#
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parser
|
4
|
+
module Source
|
5
|
+
|
6
|
+
class Map::MethodDefinition < Map
|
7
|
+
attr_reader :keyword
|
8
|
+
attr_reader :operator
|
9
|
+
attr_reader :name
|
10
|
+
attr_reader :end
|
11
|
+
attr_reader :assignment
|
12
|
+
|
13
|
+
def initialize(keyword_l, operator_l, name_l, end_l, assignment_l, body_l)
|
14
|
+
@keyword = keyword_l
|
15
|
+
@operator = operator_l
|
16
|
+
@name = name_l
|
17
|
+
@end = end_l
|
18
|
+
@assignment = assignment_l
|
19
|
+
|
20
|
+
super(@keyword.join(end_l || body_l))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/parser/source/range.rb
CHANGED
@@ -13,7 +13,7 @@ module Parser
|
|
13
13
|
# ^^
|
14
14
|
#
|
15
15
|
# @!attribute [r] source_buffer
|
16
|
-
# @return [Parser::
|
16
|
+
# @return [Parser::Source::Buffer]
|
17
17
|
#
|
18
18
|
# @!attribute [r] begin_pos
|
19
19
|
# @return [Integer] index of the first character in the range
|
@@ -112,11 +112,11 @@ module Parser
|
|
112
112
|
# @raise RangeError
|
113
113
|
#
|
114
114
|
def column_range
|
115
|
-
if
|
115
|
+
if line != last_line
|
116
116
|
raise RangeError, "#{self.inspect} spans more than one line"
|
117
117
|
end
|
118
118
|
|
119
|
-
|
119
|
+
column...last_column
|
120
120
|
end
|
121
121
|
|
122
122
|
##
|
@@ -149,6 +149,13 @@ module Parser
|
|
149
149
|
(@begin_pos...@end_pos).to_a
|
150
150
|
end
|
151
151
|
|
152
|
+
##
|
153
|
+
# @return [Range] a Ruby range with the same `begin_pos` and `end_pos`
|
154
|
+
#
|
155
|
+
def to_range
|
156
|
+
self.begin_pos...self.end_pos
|
157
|
+
end
|
158
|
+
|
152
159
|
##
|
153
160
|
# Composes a GNU/Clang-style string representation of the beginning of this
|
154
161
|
# range.
|
@@ -298,6 +305,15 @@ module Parser
|
|
298
305
|
(@end_pos <=> other.end_pos)
|
299
306
|
end
|
300
307
|
|
308
|
+
alias_method :eql?, :==
|
309
|
+
|
310
|
+
##
|
311
|
+
# Support for Ranges be used in as Hash indices and in Sets.
|
312
|
+
#
|
313
|
+
def hash
|
314
|
+
[@source_buffer, @begin_pos, @end_pos].hash
|
315
|
+
end
|
316
|
+
|
301
317
|
##
|
302
318
|
# @return [String] a human-readable representation of this range.
|
303
319
|
#
|
@@ -7,7 +7,7 @@ module Parser
|
|
7
7
|
#
|
8
8
|
# Actions are arranged in a tree and get combined so that:
|
9
9
|
# children are strictly contained by their parent
|
10
|
-
# sibblings all disjoint from one another
|
10
|
+
# sibblings all disjoint from one another and ordered
|
11
11
|
# only actions with replacement==nil may have children
|
12
12
|
#
|
13
13
|
class TreeRewriter::Action
|
@@ -25,30 +25,77 @@ module Parser
|
|
25
25
|
freeze
|
26
26
|
end
|
27
27
|
|
28
|
-
# Assumes action.children.empty?
|
29
28
|
def combine(action)
|
30
|
-
return self
|
29
|
+
return self if action.empty? # Ignore empty action
|
31
30
|
do_combine(action)
|
32
31
|
end
|
33
32
|
|
33
|
+
def empty?
|
34
|
+
@insert_before.empty? &&
|
35
|
+
@insert_after.empty? &&
|
36
|
+
@children.empty? &&
|
37
|
+
(@replacement == nil || (@replacement.empty? && @range.empty?))
|
38
|
+
end
|
39
|
+
|
34
40
|
def ordered_replacements
|
35
41
|
reps = []
|
36
42
|
reps << [@range.begin, @insert_before] unless @insert_before.empty?
|
37
43
|
reps << [@range, @replacement] if @replacement
|
38
|
-
reps.concat(@children.
|
44
|
+
reps.concat(@children.flat_map(&:ordered_replacements))
|
39
45
|
reps << [@range.end, @insert_after] unless @insert_after.empty?
|
40
46
|
reps
|
41
47
|
end
|
42
48
|
|
49
|
+
def nested_actions
|
50
|
+
actions = []
|
51
|
+
actions << [:wrap, @range, @insert_before, @insert_after] if !@insert_before.empty? ||
|
52
|
+
!@insert_after.empty?
|
53
|
+
actions << [:replace, @range, @replacement] if @replacement
|
54
|
+
actions.concat(@children.flat_map(&:nested_actions))
|
55
|
+
end
|
56
|
+
|
43
57
|
def insertion?
|
44
58
|
!insert_before.empty? || !insert_after.empty? || (replacement && !replacement.empty?)
|
45
59
|
end
|
46
60
|
|
61
|
+
##
|
62
|
+
# A root action has its range set to the whole source range, even
|
63
|
+
# though it typically do not act on that range.
|
64
|
+
# This method returns the action as if it was a child action with
|
65
|
+
# its range contracted.
|
66
|
+
# @return [Action]
|
67
|
+
def contract
|
68
|
+
raise 'Empty actions can not be contracted' if empty?
|
69
|
+
return self if insertion?
|
70
|
+
range = @range.with(
|
71
|
+
begin_pos: children.first.range.begin_pos,
|
72
|
+
end_pos: children.last.range.end_pos,
|
73
|
+
)
|
74
|
+
with(range: range)
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# @return [Action] that has been moved to the given source_buffer and with the given offset
|
79
|
+
# No check is done on validity of resulting range.
|
80
|
+
def moved(source_buffer, offset)
|
81
|
+
moved_range = ::Parser::Source::Range.new(
|
82
|
+
source_buffer,
|
83
|
+
@range.begin_pos + offset,
|
84
|
+
@range.end_pos + offset
|
85
|
+
)
|
86
|
+
with(
|
87
|
+
range: moved_range,
|
88
|
+
children: children.map { |child| child.moved(source_buffer, offset) }
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
47
92
|
protected
|
48
93
|
|
49
|
-
|
94
|
+
attr_reader :children
|
95
|
+
|
96
|
+
def with(range: @range, enforcer: @enforcer, children: @children, insert_before: @insert_before, replacement: @replacement, insert_after: @insert_after)
|
50
97
|
children = swallow(children) if replacement
|
51
|
-
self.class.new(range,
|
98
|
+
self.class.new(range, enforcer, children: children, insert_before: insert_before, replacement: replacement, insert_after: insert_after)
|
52
99
|
end
|
53
100
|
|
54
101
|
# Assumes range.contains?(action.range) && action.children.empty?
|
@@ -56,24 +103,32 @@ module Parser
|
|
56
103
|
if action.range == @range
|
57
104
|
merge(action)
|
58
105
|
else
|
59
|
-
|
106
|
+
place_in_hierarchy(action)
|
60
107
|
end
|
61
108
|
end
|
62
109
|
|
63
|
-
def
|
64
|
-
family =
|
110
|
+
def place_in_hierarchy(action)
|
111
|
+
family = analyse_hierarchy(action)
|
65
112
|
|
66
113
|
if family[:fusible]
|
67
|
-
fuse_deletions(action, family[:fusible], [*family[:
|
114
|
+
fuse_deletions(action, family[:fusible], [*family[:sibbling_left], *family[:child], *family[:sibbling_right]])
|
68
115
|
else
|
69
116
|
extra_sibbling = if family[:parent] # action should be a descendant of one of the children
|
70
|
-
family[:parent]
|
117
|
+
family[:parent].do_combine(action)
|
71
118
|
elsif family[:child] # or it should become the parent of some of the children,
|
72
|
-
action.with(children: family[:child])
|
119
|
+
action.with(children: family[:child], enforcer: @enforcer)
|
120
|
+
.combine_children(action.children)
|
73
121
|
else # or else it should become an additional child
|
74
122
|
action
|
75
123
|
end
|
76
|
-
with(children: [*family[:
|
124
|
+
with(children: [*family[:sibbling_left], extra_sibbling, *family[:sibbling_right]])
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Assumes `more_children` all contained within `@range`
|
129
|
+
def combine_children(more_children)
|
130
|
+
more_children.inject(self) do |parent, new_child|
|
131
|
+
parent.place_in_hierarchy(new_child)
|
77
132
|
end
|
78
133
|
end
|
79
134
|
|
@@ -84,22 +139,76 @@ module Parser
|
|
84
139
|
without_fusible.do_combine(fused_deletion)
|
85
140
|
end
|
86
141
|
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
142
|
+
# Similar to @children.bsearch_index || size
|
143
|
+
# except allows for a starting point
|
144
|
+
# and `bsearch_index` is only Ruby 2.3+
|
145
|
+
def bsearch_child_index(from = 0)
|
146
|
+
size = @children.size
|
147
|
+
(from...size).bsearch { |i| yield @children[i] } || size
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns the children in a hierarchy with respect to `action`:
|
151
|
+
# :sibbling_left, sibbling_right (for those that are disjoint from `action`)
|
152
|
+
# :parent (in case one of our children contains `action`)
|
153
|
+
# :child (in case `action` strictly contains some of our children)
|
154
|
+
# :fusible (in case `action` overlaps some children but they can be fused in one deletion)
|
155
|
+
# or raises a `CloberingError`
|
156
|
+
# In case a child has equal range to `action`, it is returned as `:parent`
|
157
|
+
# Reminder: an empty range 1...1 is considered disjoint from 1...10
|
158
|
+
def analyse_hierarchy(action)
|
159
|
+
r = action.range
|
160
|
+
# left_index is the index of the first child that isn't completely to the left of action
|
161
|
+
left_index = bsearch_child_index { |child| child.range.end_pos > r.begin_pos }
|
162
|
+
# right_index is the index of the first child that is completely on the right of action
|
163
|
+
start = left_index == 0 ? 0 : left_index - 1 # See "corner case" below for reason of -1
|
164
|
+
right_index = bsearch_child_index(start) { |child| child.range.begin_pos >= r.end_pos }
|
165
|
+
center = right_index - left_index
|
166
|
+
case center
|
167
|
+
when 0
|
168
|
+
# All children are disjoint from action, nothing else to do
|
169
|
+
when -1
|
170
|
+
# Corner case: if a child has empty range == action's range
|
171
|
+
# then it will appear to be both disjoint and to the left of action,
|
172
|
+
# as well as disjoint and to the right of action.
|
173
|
+
# Since ranges are equal, we return it as parent
|
174
|
+
left_index -= 1 # Fix indices, as otherwise this child would be
|
175
|
+
right_index += 1 # considered as a sibbling (both left and right!)
|
176
|
+
parent = @children[left_index]
|
100
177
|
else
|
101
|
-
@
|
178
|
+
overlap_left = @children[left_index].range.begin_pos <=> r.begin_pos
|
179
|
+
overlap_right = @children[right_index-1].range.end_pos <=> r.end_pos
|
180
|
+
|
181
|
+
# For one child to be the parent of action, we must have:
|
182
|
+
if center == 1 && overlap_left <= 0 && overlap_right >= 0
|
183
|
+
parent = @children[left_index]
|
184
|
+
else
|
185
|
+
# Otherwise consider all non disjoint elements (center) to be contained...
|
186
|
+
contained = @children[left_index...right_index]
|
187
|
+
fusible = check_fusible(action,
|
188
|
+
(contained.shift if overlap_left < 0), # ... but check first and last one
|
189
|
+
(contained.pop if overlap_right > 0) # ... for overlaps
|
190
|
+
)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
{
|
195
|
+
parent: parent,
|
196
|
+
sibbling_left: @children[0...left_index],
|
197
|
+
sibbling_right: @children[right_index...@children.size],
|
198
|
+
fusible: fusible,
|
199
|
+
child: contained,
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
# @param [Array(Action | nil)] fusible
|
204
|
+
def check_fusible(action, *fusible)
|
205
|
+
fusible.compact!
|
206
|
+
return if fusible.empty?
|
207
|
+
fusible.each do |child|
|
208
|
+
kind = action.insertion? || child.insertion? ? :crossing_insertions : :crossing_deletions
|
209
|
+
@enforcer.call(kind) { {range: action.range, conflict: child.range} }
|
102
210
|
end
|
211
|
+
fusible
|
103
212
|
end
|
104
213
|
|
105
214
|
# Assumes action.range == range && action.children.empty?
|
@@ -109,7 +218,7 @@ module Parser
|
|
109
218
|
insert_before: "#{action.insert_before}#{insert_before}",
|
110
219
|
replacement: action.replacement || @replacement,
|
111
220
|
insert_after: "#{insert_after}#{action.insert_after}",
|
112
|
-
)
|
221
|
+
).combine_children(action.children)
|
113
222
|
end
|
114
223
|
|
115
224
|
def call_enforcer_for_merge(action)
|