parser 2.6.0.0 → 3.1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/lib/parser/all.rb +3 -0
  3. data/lib/parser/ast/processor.rb +48 -1
  4. data/lib/parser/base.rb +30 -6
  5. data/lib/parser/builders/default.rb +670 -38
  6. data/lib/parser/context.rb +24 -26
  7. data/lib/parser/current.rb +36 -9
  8. data/lib/parser/current_arg_stack.rb +46 -0
  9. data/lib/parser/diagnostic/engine.rb +1 -2
  10. data/lib/parser/diagnostic.rb +1 -1
  11. data/lib/parser/lexer/dedenter.rb +58 -49
  12. data/lib/parser/lexer/explanation.rb +1 -1
  13. data/lib/parser/lexer.rb +13837 -11893
  14. data/lib/parser/macruby.rb +2544 -2489
  15. data/lib/parser/max_numparam_stack.rb +56 -0
  16. data/lib/parser/messages.rb +78 -44
  17. data/lib/parser/meta.rb +13 -3
  18. data/lib/parser/ruby18.rb +2313 -2259
  19. data/lib/parser/ruby19.rb +2537 -2488
  20. data/lib/parser/ruby20.rb +2724 -2673
  21. data/lib/parser/ruby21.rb +2766 -2727
  22. data/lib/parser/ruby22.rb +2683 -2628
  23. data/lib/parser/ruby23.rb +2796 -2755
  24. data/lib/parser/ruby24.rb +2812 -2771
  25. data/lib/parser/ruby25.rb +2703 -2670
  26. data/lib/parser/ruby26.rb +2794 -2747
  27. data/lib/parser/ruby27.rb +7914 -0
  28. data/lib/parser/ruby28.rb +8047 -0
  29. data/lib/parser/ruby30.rb +8096 -0
  30. data/lib/parser/ruby31.rb +8354 -0
  31. data/lib/parser/rubymotion.rb +2527 -2485
  32. data/lib/parser/runner/ruby_parse.rb +2 -2
  33. data/lib/parser/runner/ruby_rewrite.rb +2 -2
  34. data/lib/parser/runner.rb +36 -2
  35. data/lib/parser/source/buffer.rb +53 -28
  36. data/lib/parser/source/comment/associator.rb +31 -8
  37. data/lib/parser/source/comment.rb +14 -1
  38. data/lib/parser/source/map/method_definition.rb +25 -0
  39. data/lib/parser/source/range.rb +19 -3
  40. data/lib/parser/source/tree_rewriter/action.rb +137 -28
  41. data/lib/parser/source/tree_rewriter.rb +144 -14
  42. data/lib/parser/static_environment.rb +23 -0
  43. data/lib/parser/tree_rewriter.rb +3 -3
  44. data/lib/parser/variables_stack.rb +36 -0
  45. data/lib/parser/version.rb +1 -1
  46. data/lib/parser.rb +4 -0
  47. data/parser.gemspec +12 -19
  48. metadata +34 -99
  49. data/.gitignore +0 -32
  50. data/.travis.yml +0 -45
  51. data/.yardopts +0 -21
  52. data/CHANGELOG.md +0 -943
  53. data/CONTRIBUTING.md +0 -17
  54. data/Gemfile +0 -10
  55. data/README.md +0 -301
  56. data/Rakefile +0 -165
  57. data/ci/run_rubocop_specs +0 -14
  58. data/doc/AST_FORMAT.md +0 -1735
  59. data/doc/CUSTOMIZATION.md +0 -37
  60. data/doc/INTERNALS.md +0 -21
  61. data/doc/css/.gitkeep +0 -0
  62. data/doc/css/common.css +0 -68
  63. data/lib/parser/lexer.rl +0 -2383
  64. data/lib/parser/macruby.y +0 -2198
  65. data/lib/parser/ruby18.y +0 -1934
  66. data/lib/parser/ruby19.y +0 -2175
  67. data/lib/parser/ruby20.y +0 -2353
  68. data/lib/parser/ruby21.y +0 -2357
  69. data/lib/parser/ruby22.y +0 -2364
  70. data/lib/parser/ruby23.y +0 -2370
  71. data/lib/parser/ruby24.y +0 -2408
  72. data/lib/parser/ruby25.y +0 -2405
  73. data/lib/parser/ruby26.y +0 -2413
  74. data/lib/parser/rubymotion.y +0 -2182
  75. data/test/bug_163/fixtures/input.rb +0 -5
  76. data/test/bug_163/fixtures/output.rb +0 -5
  77. data/test/bug_163/rewriter.rb +0 -20
  78. data/test/helper.rb +0 -52
  79. data/test/parse_helper.rb +0 -315
  80. data/test/racc_coverage_helper.rb +0 -133
  81. data/test/test_base.rb +0 -31
  82. data/test/test_current.rb +0 -27
  83. data/test/test_diagnostic.rb +0 -96
  84. data/test/test_diagnostic_engine.rb +0 -62
  85. data/test/test_encoding.rb +0 -99
  86. data/test/test_lexer.rb +0 -3543
  87. data/test/test_lexer_stack_state.rb +0 -78
  88. data/test/test_parse_helper.rb +0 -80
  89. data/test/test_parser.rb +0 -7087
  90. data/test/test_runner_rewrite.rb +0 -47
  91. data/test/test_source_buffer.rb +0 -162
  92. data/test/test_source_comment.rb +0 -36
  93. data/test/test_source_comment_associator.rb +0 -367
  94. data/test/test_source_map.rb +0 -15
  95. data/test/test_source_range.rb +0 -172
  96. data/test/test_source_rewriter.rb +0 -541
  97. data/test/test_source_rewriter_action.rb +0 -46
  98. data/test/test_source_tree_rewriter.rb +0 -173
  99. data/test/test_static_environment.rb +0 -45
  100. data/test/using_tree_rewriter/fixtures/input.rb +0 -3
  101. data/test/using_tree_rewriter/fixtures/output.rb +0 -3
  102. 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
- new_buffer.source = new_source
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
 
@@ -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
- @line_for_position = {}
118
- @column_for_position = {}
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
- line_no, line_begin = line_for(position)
209
+ line_index = line_index_for_position(position)
210
+ line_begin = line_begins[line_index]
209
211
 
210
- [ @first_line + line_no, position - line_begin ]
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
- @line_for_position[position] ||= begin
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
- @column_for_position[position] ||= begin
236
- _, line_begin = line_for(position)
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 + 1
280
- if index <= 0 || index > line_begins.size
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 - 1}"
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[-index][1], line_begins[-index - 1][1] - 1)
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 - 1
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
- unless @line_begins
311
- @line_begins, index = [ [ 0, 0 ] ], 0
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
- @line_begins.unshift [ @line_begins.length, index ]
322
+ begins << index
316
323
  end
324
+ begins << @source.size + 1
325
+ begins
317
326
  end
327
+ end
318
328
 
319
- @line_begins
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
- def line_for(position)
323
- line_begins.bsearch do |line, line_begin|
324
- line_begin <= position
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
- @map_using_locations = false
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 associate_locations
104
- @map_using_locations = true
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.children.each do |child|
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 = @map_using_locations ? node.location : node
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::Map]
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
@@ -13,7 +13,7 @@ module Parser
13
13
  # ^^
14
14
  #
15
15
  # @!attribute [r] source_buffer
16
- # @return [Parser::Diagnostic::Engine]
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 self.begin.line != self.end.line
115
+ if line != last_line
116
116
  raise RangeError, "#{self.inspect} spans more than one line"
117
117
  end
118
118
 
119
- self.begin.column...self.end.column
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 unless action.insertion? || action.replacement # Ignore empty action
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.sort_by(&:range).flat_map(&:ordered_replacements))
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
- def with(range: @range, children: @children, insert_before: @insert_before, replacement: @replacement, insert_after: @insert_after)
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, @enforcer, children: children, insert_before: insert_before, replacement: replacement, insert_after: insert_after)
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
- place_in_hierachy(action)
106
+ place_in_hierarchy(action)
60
107
  end
61
108
  end
62
109
 
63
- def place_in_hierachy(action)
64
- family = @children.group_by { |child| child.relationship_with(action) }
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[:sibbling], *family[:child]])
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][0].do_combine(action)
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[:sibbling], extra_sibbling])
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
- # Returns what relationship self should have with `action`; either of
88
- # :sibbling, :parent, :child, :fusible or raises a CloberingError
89
- # In case of equal range, returns :parent
90
- def relationship_with(action)
91
- if action.range == @range || @range.contains?(action.range)
92
- :parent
93
- elsif @range.contained?(action.range)
94
- :child
95
- elsif @range.disjoint?(action.range)
96
- :sibbling
97
- elsif !action.insertion? && !insertion?
98
- @enforcer.call(:crossing_deletions) { {range: action.range, conflict: @range} }
99
- :fusible
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
- @enforcer.call(:crossing_insertions) { {range: action.range, conflict: @range} }
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)