parser 2.7.1.1 → 2.7.2.0

Sign up to get free protection for your applications and to get access to all the features.
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?