parser 2.7.1.1 → 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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/lib/parser.rb +1 -0
  3. data/lib/parser/all.rb +1 -0
  4. data/lib/parser/ast/processor.rb +2 -0
  5. data/lib/parser/base.rb +6 -5
  6. data/lib/parser/builders/default.rb +146 -19
  7. data/lib/parser/context.rb +1 -0
  8. data/lib/parser/current.rb +10 -1
  9. data/lib/parser/diagnostic.rb +1 -1
  10. data/lib/parser/diagnostic/engine.rb +1 -2
  11. data/lib/parser/lexer.rb +23770 -0
  12. data/lib/parser/macruby.rb +6149 -0
  13. data/lib/parser/max_numparam_stack.rb +1 -1
  14. data/lib/parser/messages.rb +17 -0
  15. data/lib/parser/meta.rb +5 -5
  16. data/lib/parser/ruby18.rb +5663 -0
  17. data/lib/parser/ruby19.rb +6092 -0
  18. data/lib/parser/ruby20.rb +6527 -0
  19. data/lib/parser/ruby21.rb +6578 -0
  20. data/lib/parser/ruby22.rb +6613 -0
  21. data/lib/parser/ruby23.rb +6624 -0
  22. data/lib/parser/ruby24.rb +6694 -0
  23. data/lib/parser/ruby25.rb +6662 -0
  24. data/lib/parser/ruby26.rb +6676 -0
  25. data/lib/parser/ruby27.rb +7803 -0
  26. data/lib/parser/ruby28.rb +8047 -0
  27. data/lib/parser/ruby30.rb +8052 -0
  28. data/lib/parser/rubymotion.rb +6086 -0
  29. data/lib/parser/runner.rb +26 -2
  30. data/lib/parser/runner/ruby_rewrite.rb +2 -2
  31. data/lib/parser/source/buffer.rb +3 -1
  32. data/lib/parser/source/comment.rb +1 -1
  33. data/lib/parser/source/comment/associator.rb +14 -4
  34. data/lib/parser/source/map/method_definition.rb +25 -0
  35. data/lib/parser/source/range.rb +10 -3
  36. data/lib/parser/source/tree_rewriter.rb +73 -10
  37. data/lib/parser/source/tree_rewriter/action.rb +114 -21
  38. data/lib/parser/tree_rewriter.rb +1 -2
  39. data/lib/parser/version.rb +1 -1
  40. data/parser.gemspec +3 -18
  41. metadata +16 -99
  42. data/.gitignore +0 -33
  43. data/.travis.yml +0 -42
  44. data/.yardopts +0 -21
  45. data/CHANGELOG.md +0 -1075
  46. data/CONTRIBUTING.md +0 -17
  47. data/Gemfile +0 -10
  48. data/LICENSE.txt +0 -25
  49. data/README.md +0 -309
  50. data/Rakefile +0 -166
  51. data/ci/run_rubocop_specs +0 -14
  52. data/doc/AST_FORMAT.md +0 -2180
  53. data/doc/CUSTOMIZATION.md +0 -37
  54. data/doc/INTERNALS.md +0 -21
  55. data/doc/css/.gitkeep +0 -0
  56. data/doc/css/common.css +0 -68
  57. data/lib/parser/lexer.rl +0 -2536
  58. data/lib/parser/macruby.y +0 -2198
  59. data/lib/parser/ruby18.y +0 -1934
  60. data/lib/parser/ruby19.y +0 -2175
  61. data/lib/parser/ruby20.y +0 -2353
  62. data/lib/parser/ruby21.y +0 -2357
  63. data/lib/parser/ruby22.y +0 -2364
  64. data/lib/parser/ruby23.y +0 -2370
  65. data/lib/parser/ruby24.y +0 -2408
  66. data/lib/parser/ruby25.y +0 -2405
  67. data/lib/parser/ruby26.y +0 -2413
  68. data/lib/parser/ruby27.y +0 -2941
  69. data/lib/parser/rubymotion.y +0 -2182
  70. data/test/bug_163/fixtures/input.rb +0 -5
  71. data/test/bug_163/fixtures/output.rb +0 -5
  72. data/test/bug_163/rewriter.rb +0 -20
  73. data/test/helper.rb +0 -60
  74. data/test/parse_helper.rb +0 -319
  75. data/test/racc_coverage_helper.rb +0 -133
  76. data/test/test_base.rb +0 -31
  77. data/test/test_current.rb +0 -29
  78. data/test/test_diagnostic.rb +0 -96
  79. data/test/test_diagnostic_engine.rb +0 -62
  80. data/test/test_encoding.rb +0 -99
  81. data/test/test_lexer.rb +0 -3608
  82. data/test/test_lexer_stack_state.rb +0 -78
  83. data/test/test_parse_helper.rb +0 -80
  84. data/test/test_parser.rb +0 -9430
  85. data/test/test_runner_parse.rb +0 -35
  86. data/test/test_runner_rewrite.rb +0 -47
  87. data/test/test_source_buffer.rb +0 -162
  88. data/test/test_source_comment.rb +0 -36
  89. data/test/test_source_comment_associator.rb +0 -367
  90. data/test/test_source_map.rb +0 -15
  91. data/test/test_source_range.rb +0 -187
  92. data/test/test_source_rewriter.rb +0 -541
  93. data/test/test_source_rewriter_action.rb +0 -46
  94. data/test/test_source_tree_rewriter.rb +0 -253
  95. data/test/test_static_environment.rb +0 -45
  96. data/test/using_tree_rewriter/fixtures/input.rb +0 -3
  97. data/test/using_tree_rewriter/fixtures/output.rb +0 -3
  98. 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.
@@ -129,6 +129,8 @@ module Parser
129
129
  ##
130
130
  # Merges the updates of argument with the receiver.
131
131
  # Policies of the receiver are used.
132
+ # This action is atomic in that it won't change the receiver
133
+ # unless it succeeds.
132
134
  #
133
135
  # @param [Rewriter] with
134
136
  # @return [Rewriter] self
@@ -154,6 +156,32 @@ module Parser
154
156
  dup.merge!(with)
155
157
  end
156
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
+
157
185
  ##
158
186
  # Replaces the code of the source range `range` with `content`.
159
187
  #
@@ -222,19 +250,54 @@ module Parser
222
250
  # @return [String]
223
251
  #
224
252
  def process
225
- source = @source_buffer.source.dup
226
- adjustment = 0
253
+ source = @source_buffer.source
227
254
 
255
+ chunks = []
256
+ last_end = 0
228
257
  @action_root.ordered_replacements.each do |range, replacement|
229
- begin_pos = range.begin_pos + adjustment
230
- end_pos = begin_pos + range.length
231
-
232
- source[begin_pos...end_pos] = replacement
233
-
234
- adjustment += replacement.length - range.length
258
+ chunks << source[last_end...range.begin_pos] << replacement
259
+ last_end = range.end_pos
235
260
  end
261
+ chunks << source[last_end...source.length]
262
+ chunks.join
263
+ end
236
264
 
237
- 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
238
301
  end
239
302
 
240
303
  ##
@@ -313,7 +376,7 @@ module Parser
313
376
 
314
377
  def check_range_validity(range)
315
378
  if range.begin_pos < 0 || range.end_pos > @source_buffer.source.size
316
- 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"
317
380
  end
318
381
  range
319
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
@@ -41,15 +41,54 @@ module Parser
41
41
  reps = []
42
42
  reps << [@range.begin, @insert_before] unless @insert_before.empty?
43
43
  reps << [@range, @replacement] if @replacement
44
- reps.concat(@children.sort_by(&:range).flat_map(&:ordered_replacements))
44
+ reps.concat(@children.flat_map(&:ordered_replacements))
45
45
  reps << [@range.end, @insert_after] unless @insert_after.empty?
46
46
  reps
47
47
  end
48
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
+
49
57
  def insertion?
50
58
  !insert_before.empty? || !insert_after.empty? || (replacement && !replacement.empty?)
51
59
  end
52
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
+
53
92
  protected
54
93
 
55
94
  attr_reader :children
@@ -69,24 +108,24 @@ module Parser
69
108
  end
70
109
 
71
110
  def place_in_hierarchy(action)
72
- family = @children.group_by { |child| child.relationship_with(action) }
111
+ family = analyse_hierarchy(action)
73
112
 
74
113
  if family[:fusible]
75
- fuse_deletions(action, family[:fusible], [*family[:sibbling], *family[:child]])
114
+ fuse_deletions(action, family[:fusible], [*family[:sibbling_left], *family[:child], *family[:sibbling_right]])
76
115
  else
77
116
  extra_sibbling = if family[:parent] # action should be a descendant of one of the children
78
- family[:parent][0].do_combine(action)
117
+ family[:parent].do_combine(action)
79
118
  elsif family[:child] # or it should become the parent of some of the children,
80
119
  action.with(children: family[:child], enforcer: @enforcer)
81
120
  .combine_children(action.children)
82
121
  else # or else it should become an additional child
83
122
  action
84
123
  end
85
- with(children: [*family[:sibbling], extra_sibbling])
124
+ with(children: [*family[:sibbling_left], extra_sibbling, *family[:sibbling_right]])
86
125
  end
87
126
  end
88
127
 
89
- # Assumes more_children all contained within @range
128
+ # Assumes `more_children` all contained within `@range`
90
129
  def combine_children(more_children)
91
130
  more_children.inject(self) do |parent, new_child|
92
131
  parent.place_in_hierarchy(new_child)
@@ -100,22 +139,76 @@ module Parser
100
139
  without_fusible.do_combine(fused_deletion)
101
140
  end
102
141
 
103
- # Returns what relationship self should have with `action`; either of
104
- # :sibbling, :parent, :child, :fusible or raises a CloberingError
105
- # In case of equal range, returns :parent
106
- def relationship_with(action)
107
- if action.range == @range || @range.contains?(action.range)
108
- :parent
109
- elsif @range.contained?(action.range)
110
- :child
111
- elsif @range.disjoint?(action.range)
112
- :sibbling
113
- elsif !action.insertion? && !insertion?
114
- @enforcer.call(:crossing_deletions) { {range: action.range, conflict: @range} }
115
- :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]
116
177
  else
117
- @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} }
118
210
  end
211
+ fusible
119
212
  end
120
213
 
121
214
  # Assumes action.range == range && action.children.empty?