parser 2.6.5.0 → 2.7.2.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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/lib/parser.rb +4 -1
  3. data/lib/parser/all.rb +1 -0
  4. data/lib/parser/ast/processor.rb +21 -0
  5. data/lib/parser/base.rb +25 -5
  6. data/lib/parser/builders/default.rb +394 -24
  7. data/lib/parser/context.rb +5 -0
  8. data/lib/parser/current.rb +16 -7
  9. data/lib/parser/current_arg_stack.rb +43 -0
  10. data/lib/parser/diagnostic.rb +1 -1
  11. data/lib/parser/diagnostic/engine.rb +1 -2
  12. data/lib/parser/lexer.rb +23770 -0
  13. data/lib/parser/lexer/dedenter.rb +52 -49
  14. data/lib/parser/macruby.rb +6149 -0
  15. data/lib/parser/{lexer/max_numparam_stack.rb → max_numparam_stack.rb} +10 -4
  16. data/lib/parser/messages.rb +52 -29
  17. data/lib/parser/meta.rb +10 -5
  18. data/lib/parser/ruby18.rb +5663 -0
  19. data/lib/parser/ruby19.rb +6092 -0
  20. data/lib/parser/ruby20.rb +6527 -0
  21. data/lib/parser/ruby21.rb +6578 -0
  22. data/lib/parser/ruby22.rb +6613 -0
  23. data/lib/parser/ruby23.rb +6624 -0
  24. data/lib/parser/ruby24.rb +6694 -0
  25. data/lib/parser/ruby25.rb +6662 -0
  26. data/lib/parser/ruby26.rb +6676 -0
  27. data/lib/parser/ruby27.rb +7803 -0
  28. data/lib/parser/ruby28.rb +8047 -0
  29. data/lib/parser/ruby30.rb +8052 -0
  30. data/lib/parser/rubymotion.rb +6086 -0
  31. data/lib/parser/runner.rb +26 -2
  32. data/lib/parser/runner/ruby_rewrite.rb +2 -2
  33. data/lib/parser/source/buffer.rb +3 -1
  34. data/lib/parser/source/comment.rb +1 -1
  35. data/lib/parser/source/comment/associator.rb +14 -4
  36. data/lib/parser/source/map/method_definition.rb +25 -0
  37. data/lib/parser/source/range.rb +19 -3
  38. data/lib/parser/source/tree_rewriter.rb +115 -12
  39. data/lib/parser/source/tree_rewriter/action.rb +137 -28
  40. data/lib/parser/static_environment.rb +10 -0
  41. data/lib/parser/tree_rewriter.rb +1 -2
  42. data/lib/parser/variables_stack.rb +32 -0
  43. data/lib/parser/version.rb +1 -1
  44. data/parser.gemspec +10 -18
  45. metadata +22 -99
  46. data/.gitignore +0 -33
  47. data/.travis.yml +0 -45
  48. data/.yardopts +0 -21
  49. data/CHANGELOG.md +0 -997
  50. data/CONTRIBUTING.md +0 -17
  51. data/Gemfile +0 -10
  52. data/LICENSE.txt +0 -25
  53. data/README.md +0 -301
  54. data/Rakefile +0 -166
  55. data/ci/run_rubocop_specs +0 -14
  56. data/doc/AST_FORMAT.md +0 -1816
  57. data/doc/CUSTOMIZATION.md +0 -37
  58. data/doc/INTERNALS.md +0 -21
  59. data/doc/css/.gitkeep +0 -0
  60. data/doc/css/common.css +0 -68
  61. data/lib/parser/lexer.rl +0 -2533
  62. data/lib/parser/macruby.y +0 -2198
  63. data/lib/parser/ruby18.y +0 -1934
  64. data/lib/parser/ruby19.y +0 -2175
  65. data/lib/parser/ruby20.y +0 -2353
  66. data/lib/parser/ruby21.y +0 -2357
  67. data/lib/parser/ruby22.y +0 -2364
  68. data/lib/parser/ruby23.y +0 -2370
  69. data/lib/parser/ruby24.y +0 -2408
  70. data/lib/parser/ruby25.y +0 -2405
  71. data/lib/parser/ruby26.y +0 -2413
  72. data/lib/parser/ruby27.y +0 -2470
  73. data/lib/parser/rubymotion.y +0 -2182
  74. data/test/bug_163/fixtures/input.rb +0 -5
  75. data/test/bug_163/fixtures/output.rb +0 -5
  76. data/test/bug_163/rewriter.rb +0 -20
  77. data/test/helper.rb +0 -59
  78. data/test/parse_helper.rb +0 -316
  79. data/test/racc_coverage_helper.rb +0 -133
  80. data/test/test_base.rb +0 -31
  81. data/test/test_current.rb +0 -29
  82. data/test/test_diagnostic.rb +0 -96
  83. data/test/test_diagnostic_engine.rb +0 -62
  84. data/test/test_encoding.rb +0 -99
  85. data/test/test_lexer.rb +0 -3667
  86. data/test/test_lexer_stack_state.rb +0 -78
  87. data/test/test_parse_helper.rb +0 -80
  88. data/test/test_parser.rb +0 -7644
  89. data/test/test_runner_parse.rb +0 -35
  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
@@ -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].freeze
41
+
40
42
  def runner_name
41
43
  raise NotImplementedError, "implement #{self.class}##{__callee__}"
42
44
  end
@@ -111,6 +113,11 @@ module Parser
111
113
  @parser_class = Parser::Ruby27
112
114
  end
113
115
 
116
+ opts.on '--30', 'Parse as Ruby 3.0 would' do
117
+ require 'parser/ruby30'
118
+ @parser_class = Parser::Ruby30
119
+ end
120
+
114
121
  opts.on '--mac', 'Parse as MacRuby 0.12 would' do
115
122
  require 'parser/macruby'
116
123
  @parser_class = Parser::MacRuby
@@ -121,6 +128,17 @@ module Parser
121
128
  @parser_class = Parser::RubyMotion
122
129
  end
123
130
 
131
+ opts.on '--legacy', "Parse with all legacy modes" do
132
+ @legacy = Hash.new(true)
133
+ end
134
+
135
+ LEGACY_MODES.each do |mode|
136
+ opt_name = "--legacy-#{mode.to_s.gsub('_', '-')}"
137
+ opts.on opt_name, "Parse with legacy mode for emit_#{mode}" do
138
+ @legacy[mode] = true
139
+ end
140
+ end
141
+
124
142
  opts.on '-w', '--warnings', 'Enable warnings' do |w|
125
143
  @warnings = w
126
144
  end
@@ -159,6 +177,12 @@ module Parser
159
177
  end
160
178
  end
161
179
 
180
+ def setup_builder_default
181
+ LEGACY_MODES.each do |mode|
182
+ Parser::Builders::Default.send(:"emit_#{mode}=", !@legacy[mode])
183
+ end
184
+ end
185
+
162
186
  def prepare_parser
163
187
  @parser = @parser_class.new
164
188
 
@@ -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)
@@ -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
@@ -116,6 +116,8 @@ module Parser
116
116
  # Cache for fast lookup
117
117
  @line_for_position = {}
118
118
  @column_for_position = {}
119
+
120
+ self.source = source if source
119
121
  end
120
122
 
121
123
  ##
@@ -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
  #
@@ -107,6 +107,19 @@ module Parser
107
107
 
108
108
  private
109
109
 
110
+ POSTFIX_TYPES = Set[:if, :while, :while_post, :until, :until_post, :masgn].freeze
111
+ def children_in_source_order(node)
112
+ if POSTFIX_TYPES.include?(node.type)
113
+ # All these types have either nodes with expressions, or `nil`
114
+ # so a compact will do, but they need to be sorted.
115
+ node.children.compact.sort_by { |child| child.loc.expression.begin_pos }
116
+ else
117
+ node.children.select do |child|
118
+ child.is_a?(AST::Node) && child.loc && child.loc.expression
119
+ end
120
+ end
121
+ end
122
+
110
123
  def do_associate
111
124
  @mapping = Hash.new { |h, k| h[k] = [] }
112
125
  @comment_num = -1
@@ -131,10 +144,7 @@ module Parser
131
144
  node_loc = node.location
132
145
  if @current_comment.location.line <= node_loc.last_line ||
133
146
  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
147
+ children_in_source_order(node).each { |child| visit(child) }
138
148
 
139
149
  process_trailing_comments(node)
140
150
  end
@@ -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
  #
@@ -117,6 +117,71 @@ module Parser
117
117
  @action_root = TreeRewriter::Action.new(all_encompassing_range, @enforcer)
118
118
  end
119
119
 
120
+ ##
121
+ # Returns true iff no (non trivial) update has been recorded
122
+ #
123
+ # @return [Boolean]
124
+ #
125
+ def empty?
126
+ @action_root.empty?
127
+ end
128
+
129
+ ##
130
+ # Merges the updates of argument with the receiver.
131
+ # Policies of the receiver are used.
132
+ # This action is atomic in that it won't change the receiver
133
+ # unless it succeeds.
134
+ #
135
+ # @param [Rewriter] with
136
+ # @return [Rewriter] self
137
+ # @raise [ClobberingError] when clobbering is detected
138
+ #
139
+ def merge!(with)
140
+ raise 'TreeRewriter are not for the same source_buffer' unless
141
+ source_buffer == with.source_buffer
142
+
143
+ @action_root = @action_root.combine(with.action_root)
144
+ self
145
+ end
146
+
147
+ ##
148
+ # Returns a new rewriter that consists of the updates of the received
149
+ # and the given argument. Policies of the receiver are used.
150
+ #
151
+ # @param [Rewriter] with
152
+ # @return [Rewriter] merge of receiver and argument
153
+ # @raise [ClobberingError] when clobbering is detected
154
+ #
155
+ def merge(with)
156
+ dup.merge!(with)
157
+ end
158
+
159
+ ##
160
+ # For special cases where one needs to merge a rewriter attached to a different source_buffer
161
+ # or that needs to be offset. Policies of the receiver are used.
162
+ #
163
+ # @param [TreeRewriter] rewriter from different source_buffer
164
+ # @param [Integer] offset
165
+ # @return [Rewriter] self
166
+ # @raise [IndexError] if action ranges (once offset) don't fit the current buffer
167
+ #
168
+ def import!(foreign_rewriter, offset: 0)
169
+ return self if foreign_rewriter.empty?
170
+
171
+ contracted = foreign_rewriter.action_root.contract
172
+ merge_effective_range = ::Parser::Source::Range.new(
173
+ @source_buffer,
174
+ contracted.range.begin_pos + offset,
175
+ contracted.range.end_pos + offset,
176
+ )
177
+ check_range_validity(merge_effective_range)
178
+
179
+ merge_with = contracted.moved(@source_buffer, offset)
180
+
181
+ @action_root = @action_root.combine(merge_with)
182
+ self
183
+ end
184
+
120
185
  ##
121
186
  # Replaces the code of the source range `range` with `content`.
122
187
  #
@@ -185,28 +250,62 @@ module Parser
185
250
  # @return [String]
186
251
  #
187
252
  def process
188
- source = @source_buffer.source.dup
189
- adjustment = 0
253
+ source = @source_buffer.source
190
254
 
255
+ chunks = []
256
+ last_end = 0
191
257
  @action_root.ordered_replacements.each do |range, replacement|
192
- begin_pos = range.begin_pos + adjustment
193
- end_pos = begin_pos + range.length
194
-
195
- source[begin_pos...end_pos] = replacement
196
-
197
- adjustment += replacement.length - range.length
258
+ chunks << source[last_end...range.begin_pos] << replacement
259
+ last_end = range.end_pos
198
260
  end
261
+ chunks << source[last_end...source.length]
262
+ chunks.join
263
+ end
199
264
 
200
- source
265
+ ##
266
+ # Returns a representation of the rewriter as an ordered list of replacements.
267
+ #
268
+ # rewriter.as_replacements # => [ [1...1, '('],
269
+ # [2...4, 'foo'],
270
+ # [5...6, ''],
271
+ # [6...6, '!'],
272
+ # [10...10, ')'],
273
+ # ]
274
+ #
275
+ # This representation is sufficient to recreate the result of `process` but it is
276
+ # not sufficient to recreate completely the rewriter for further merging/actions.
277
+ # See `as_nested_actions`
278
+ #
279
+ # @return [Array<Range, String>] an ordered list of pairs of range & replacement
280
+ #
281
+ def as_replacements
282
+ @action_root.ordered_replacements
283
+ end
284
+
285
+ ##
286
+ # Returns a representation of the rewriter as nested insertions (:wrap) and replacements.
287
+ #
288
+ # rewriter.as_actions # =>[ [:wrap, 1...10, '(', ')'],
289
+ # [:wrap, 2...6, '', '!'], # aka "insert_after"
290
+ # [:replace, 2...4, 'foo'],
291
+ # [:replace, 5...6, ''], # aka "removal"
292
+ # ],
293
+ #
294
+ # Contrary to `as_replacements`, this representation is sufficient to recreate exactly
295
+ # the rewriter.
296
+ #
297
+ # @return [Array<(Symbol, Range, String{, String})>]
298
+ #
299
+ def as_nested_actions
300
+ @action_root.nested_actions
201
301
  end
202
302
 
203
303
  ##
204
304
  # Provides a protected block where a sequence of multiple rewrite actions
205
305
  # are handled atomically. If any of the actions failed by clobbering,
206
- # all the actions are rolled back.
306
+ # all the actions are rolled back. Transactions can be nested.
207
307
  #
208
308
  # @raise [RuntimeError] when no block is passed
209
- # @raise [RuntimeError] when already in a transaction
210
309
  #
211
310
  def transaction
212
311
  unless block_given?
@@ -256,6 +355,10 @@ module Parser
256
355
 
257
356
  extend Deprecation
258
357
 
358
+ protected
359
+
360
+ attr_reader :action_root
361
+
259
362
  private
260
363
 
261
364
  ACTIONS = %i[accept warn raise].freeze
@@ -273,7 +376,7 @@ module Parser
273
376
 
274
377
  def check_range_validity(range)
275
378
  if range.begin_pos < 0 || range.end_pos > @source_buffer.source.size
276
- raise IndexError, "The range #{range} is outside the bounds of the source"
379
+ raise IndexError, "The range #{range.to_range} is outside the bounds of the source"
277
380
  end
278
381
  range
279
382
  end
@@ -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)