Class: Parser::Source::Rewriter

Inherits:
Object
  • Object
show all
Defined in:
lib/parser/source/rewriter.rb

Overview

Rewriter performs the heavy lifting in the source rewriting process. It schedules code updates to be performed in the correct order and verifies that no two updates clobber each other, that is, attempt to modify the same section of code. (However, if two updates modify the same section in exactly the same way, they are merged.)

If it is detected that one update clobbers another one, an :error and a :note diagnostics describing both updates are generated and passed to the diagnostic engine. After that, an exception is raised.

The default diagnostic engine consumer simply prints the diagnostics to stderr.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source_buffer) ⇒ Rewriter

Returns a new instance of Rewriter.

Parameters:



32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/parser/source/rewriter.rb', line 32

def initialize(source_buffer)
  @diagnostics = Diagnostic::Engine.new
  @diagnostics.consumer = lambda do |diag|
    $stderr.puts diag.render
  end

  @source_buffer = source_buffer
  @queue         = []
  @clobber       = 0
  @insertions    = 0 # clobbered zero-length positions; index 0 is the far left

  @insert_before_multi_order = 0
  @insert_after_multi_order = 0
end

Instance Attribute Details

#diagnosticsDiagnostic::Engine (readonly)

Returns:



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/parser/source/rewriter.rb', line 25

class Rewriter
  attr_reader :source_buffer
  attr_reader :diagnostics

  ##
  # @param [Source::Buffer] source_buffer
  #
  def initialize(source_buffer)
    @diagnostics = Diagnostic::Engine.new
    @diagnostics.consumer = lambda do |diag|
      $stderr.puts diag.render
    end

    @source_buffer = source_buffer
    @queue         = []
    @clobber       = 0
    @insertions    = 0 # clobbered zero-length positions; index 0 is the far left

    @insert_before_multi_order = 0
    @insert_after_multi_order = 0
  end

  ##
  # Removes the source range.
  #
  # @param [Range] range
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def remove(range)
    append Rewriter::Action.new(range, ''.freeze)
  end

  ##
  # Inserts new code before the given source range.
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def insert_before(range, content)
    append Rewriter::Action.new(range.begin, content)
  end

  ##
  # Inserts new code before the given source range by allowing other
  # insertions at the same position.
  # Note that an insertion with latter invocation comes _before_ earlier
  # insertion at the same position in the rewritten source.
  #
  # @example Inserting '[('
  #   rewriter.
  #     insert_before_multi(range, '(').
  #     insert_before_multi(range, '[').
  #     process
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def insert_before_multi(range, content)
    @insert_before_multi_order -= 1
    append Rewriter::Action.new(range.begin, content, true, @insert_before_multi_order)
  end

  ##
  # Inserts new code after the given source range.
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def insert_after(range, content)
    append Rewriter::Action.new(range.end, content)
  end

  ##
  # Inserts new code after the given source range by allowing other
  # insertions at the same position.
  # Note that an insertion with latter invocation comes _after_ earlier
  # insertion at the same position in the rewritten source.
  #
  # @example Inserting ')]'
  #   rewriter.
  #     insert_after_multi(range, ')').
  #     insert_after_multi(range, ']').
  #     process
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def insert_after_multi(range, content)
    @insert_after_multi_order += 1
    append Rewriter::Action.new(range.end, content, true, @insert_after_multi_order)
  end

  ##
  # Replaces the code of the source range `range` with `content`.
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def replace(range, content)
    append Rewriter::Action.new(range, content)
  end

  ##
  # Applies all scheduled changes to the `source_buffer` and returns
  # modified source as a new string.
  #
  # @return [String]
  #
  def process
    if in_transaction?
      raise "Do not call #{self.class}##{__method__} inside a transaction"
    end

    adjustment = 0
    source     = @source_buffer.source.dup

    @queue.sort.each do |action|
      begin_pos = action.range.begin_pos + adjustment
      end_pos   = begin_pos + action.range.length

      source[begin_pos...end_pos] = action.replacement

      adjustment += (action.replacement.length - action.range.length)
    end

    source
  end

  ##
  # Provides a protected block where a sequence of multiple rewrite actions
  # are handled atomically. If any of the actions failed by clobbering,
  # all the actions are rolled back.
  #
  # @example
  #  begin
  #    rewriter.transaction do
  #      rewriter.insert_before(range_of_something, '(')
  #      rewriter.insert_after(range_of_something, ')')
  #    end
  #  rescue Parser::ClobberingError
  #  end
  #
  # @raise [RuntimeError] when no block is passed
  # @raise [RuntimeError] when already in a transaction
  #
  def transaction
    unless block_given?
      raise "#{self.class}##{__method__} requires block"
    end

    if in_transaction?
      raise 'Nested transaction is not supported'
    end

    @pending_queue = @queue.dup
    @pending_clobber = @clobber
    @pending_insertions = @insertions

    yield

    @queue = @pending_queue
    @clobber = @pending_clobber
    @insertions = @pending_insertions

    self
  ensure
    @pending_queue = nil
    @pending_clobber = nil
    @pending_insertions = nil
  end

  private

  # Schedule a code update. If it overlaps with another update, check
  # whether they conflict, and raise a clobbering error if they do.
  # (As a special case, zero-length ranges at the same position are
  # considered to "overlap".) Otherwise, merge them.
  #
  # Updates which are adjacent to each other, but do not overlap, are also
  # merged.
  #
  # RULES:
  #
  # - Insertion ("replacing" a zero-length range):
  #   - Two insertions at the same point conflict. This is true even
  #     if the earlier insertion has already been merged with an adjacent
  #     update, and even if they are both inserting the same text.
  #   - An insertion never conflicts with a replace or remove operation
  #     on its right or left side, which does not overlap it (in other
  #     words, which does not update BOTH its right and left sides).
  #   - An insertion always conflicts with a remove operation which spans
  #     both its sides.
  #   - An insertion conflicts with a replace operation which spans both its
  #     sides, unless the replacement text is longer than the replaced text
  #     by the size of the insertion (or more), and the portion of
  #     replacement text immediately after the insertion position is
  #     identical to the inserted text.
  #
  # - Removal operations never conflict with each other.
  #
  # - Replacement operations:
  #   - Take the portion of each replacement text which falls within:
  #     - The other operation's replaced region
  #     - The other operation's replacement text, if it extends past the
  #       end of its own replaced region (in other words, if the replacement
  #       text is longer than the text it replaces)
  #   - If and only if the taken texts are identical for both operations,
  #     they do not conflict.
  #
  def append(action)
    range = action.range

    # Is this an insertion?
    if range.empty?
      # Replacing nothing with... nothing?
      return self if action.replacement.empty?

      if !action.allow_multiple_insertions? && (conflicting = clobbered_insertion?(range))
        raise_clobber_error(action, [conflicting])
      end

      record_insertion(range)

      if (adjacent = adjacent_updates?(range))
        conflicting = adjacent.find do |a|
          a.range.overlaps?(range) &&
            !replace_compatible_with_insertion?(a, action)
        end
        raise_clobber_error(action, [conflicting]) if conflicting

        merge_actions!(action, adjacent)
      else
        active_queue << action
      end
    else
      # It's a replace or remove operation.
      if (insertions = adjacent_insertions?(range))
        insertions.each do |insertion|
          if range.overlaps?(insertion.range) &&
             !replace_compatible_with_insertion?(action, insertion)
            raise_clobber_error(action, [insertion])
          else
            action = merge_actions(action, [insertion])
            active_queue.delete(insertion)
          end
        end
      end

      if (adjacent = adjacent_updates?(range))
        if can_merge?(action, adjacent)
          record_replace(range)
          merge_actions!(action, adjacent)
        else
          raise_clobber_error(action, adjacent)
        end
      else
        record_replace(range)
        active_queue << action
      end
    end

    self
  end

  def record_insertion(range)
    self.active_insertions = active_insertions | (1 << range.begin_pos)
  end

  def record_replace(range)
    self.active_clobber = active_clobber | clobbered_position_mask(range)
  end

  def clobbered_position_mask(range)
    ((1 << range.size) - 1) << range.begin_pos
  end

  def adjacent_position_mask(range)
    ((1 << (range.size + 2)) - 1) << (range.begin_pos - 1)
  end

  def adjacent_insertion_mask(range)
    ((1 << (range.size + 1)) - 1) << range.begin_pos
  end

  def clobbered_insertion?(insertion)
    insertion_pos = insertion.begin_pos
    if active_insertions & (1 << insertion_pos) != 0
      # The clobbered insertion may have already been merged with other
      # updates, so it won't necessarily have the same begin_pos.
      active_queue.find do |a|
        a.range.begin_pos <= insertion_pos && insertion_pos <= a.range.end_pos
      end
    end
  end

  def adjacent_insertions?(range)
    # Just retrieve insertions which have not been merged with an adjacent
    # remove or replace.
    if active_insertions & adjacent_insertion_mask(range) != 0
      result = active_queue.select do |a|
        a.range.empty? && adjacent?(range, a.range)
      end
      result.empty? ? nil : result
    end
  end

  def adjacent_updates?(range)
    if active_clobber & adjacent_position_mask(range) != 0
      active_queue.select { |a| adjacent?(range, a.range) }
    end
  end

  def replace_compatible_with_insertion?(replace, insertion)
    (replace.replacement.length - replace.range.size) >= insertion.range.size &&
      (offset = insertion.range.begin_pos - replace.range.begin_pos) &&
      replace.replacement[offset, insertion.replacement.length] == insertion.replacement
  end

  def can_merge?(action, existing)
    # Compare 2 replace/remove operations (neither is an insertion)
    range = action.range

    existing.all? do |other|
      overlap = range.intersect(other.range)
      next true if overlap.nil? # adjacent, not overlapping

      repl1_offset = overlap.begin_pos - range.begin_pos
      repl2_offset = overlap.begin_pos - other.range.begin_pos
      repl1_length = [other.range.length - repl2_offset,
                      other.replacement.length  - repl2_offset].max
      repl2_length = [range.length - repl1_offset,
                      action.replacement.length - repl1_offset].max

      replacement1 = action.replacement[repl1_offset, repl1_length] || ''.freeze
      replacement2 = other.replacement[repl2_offset, repl2_length] || ''.freeze
      replacement1 == replacement2
    end
  end

  def merge_actions(action, existing)
    actions = existing.push(action).sort_by do |a|
      [a.range.begin_pos, a.range.end_pos]
    end
    range = actions.first.range.join(actions.max_by { |a| a.range.end_pos }.range)

    Rewriter::Action.new(range, merge_replacements(actions))
  end

  def merge_actions!(action, existing)
    new_action = merge_actions(action, existing)
    active_queue.delete(action)
    replace_actions(existing, new_action)
  end

  def merge_replacements(actions)
    # `actions` must be sorted by beginning position
    begin_pos = actions.first.range.begin_pos
    result    = ''
    prev_act  = nil

    actions.each do |act|
      if !prev_act || act.range.disjoint?(prev_act.range)
        result << act.replacement
      else
        prev_end = [prev_act.range.begin_pos + prev_act.replacement.length,
                    prev_act.range.end_pos].max
        offset   = prev_end - act.range.begin_pos
        result << act.replacement[offset..-1] if offset < act.replacement.size
      end

      prev_act = act
    end

    result
  end

  def replace_actions(old, updated)
    old.each { |act| active_queue.delete(act) }
    active_queue << updated
  end

  def raise_clobber_error(action, existing)
    # cannot replace 3 characters with "foobar"
    diagnostic = Diagnostic.new(:error,
                                :invalid_action,
                                { :action => action },
                                action.range)
    @diagnostics.process(diagnostic)

    # clobbered by: remove 3 characters
    diagnostic = Diagnostic.new(:note,
                                :clobbered,
                                { :action => existing[0] },
                                existing[0].range)
    @diagnostics.process(diagnostic)

    raise ClobberingError, "Parser::Source::Rewriter detected clobbering"
  end

  def in_transaction?
    !@pending_queue.nil?
  end

  def active_queue
    @pending_queue || @queue
  end

  def active_clobber
    @pending_clobber || @clobber
  end

  def active_insertions
    @pending_insertions || @insertions
  end

  def active_clobber=(value)
    if @pending_clobber
      @pending_clobber = value
    else
      @clobber = value
    end
  end

  def active_insertions=(value)
    if @pending_insertions
      @pending_insertions = value
    else
      @insertions = value
    end
  end

  def adjacent?(range1, range2)
    range1.begin_pos <= range2.end_pos && range2.begin_pos <= range1.end_pos
  end
end

#source_bufferSource::Buffer (readonly)

Returns:



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/parser/source/rewriter.rb', line 25

class Rewriter
  attr_reader :source_buffer
  attr_reader :diagnostics

  ##
  # @param [Source::Buffer] source_buffer
  #
  def initialize(source_buffer)
    @diagnostics = Diagnostic::Engine.new
    @diagnostics.consumer = lambda do |diag|
      $stderr.puts diag.render
    end

    @source_buffer = source_buffer
    @queue         = []
    @clobber       = 0
    @insertions    = 0 # clobbered zero-length positions; index 0 is the far left

    @insert_before_multi_order = 0
    @insert_after_multi_order = 0
  end

  ##
  # Removes the source range.
  #
  # @param [Range] range
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def remove(range)
    append Rewriter::Action.new(range, ''.freeze)
  end

  ##
  # Inserts new code before the given source range.
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def insert_before(range, content)
    append Rewriter::Action.new(range.begin, content)
  end

  ##
  # Inserts new code before the given source range by allowing other
  # insertions at the same position.
  # Note that an insertion with latter invocation comes _before_ earlier
  # insertion at the same position in the rewritten source.
  #
  # @example Inserting '[('
  #   rewriter.
  #     insert_before_multi(range, '(').
  #     insert_before_multi(range, '[').
  #     process
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def insert_before_multi(range, content)
    @insert_before_multi_order -= 1
    append Rewriter::Action.new(range.begin, content, true, @insert_before_multi_order)
  end

  ##
  # Inserts new code after the given source range.
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def insert_after(range, content)
    append Rewriter::Action.new(range.end, content)
  end

  ##
  # Inserts new code after the given source range by allowing other
  # insertions at the same position.
  # Note that an insertion with latter invocation comes _after_ earlier
  # insertion at the same position in the rewritten source.
  #
  # @example Inserting ')]'
  #   rewriter.
  #     insert_after_multi(range, ')').
  #     insert_after_multi(range, ']').
  #     process
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def insert_after_multi(range, content)
    @insert_after_multi_order += 1
    append Rewriter::Action.new(range.end, content, true, @insert_after_multi_order)
  end

  ##
  # Replaces the code of the source range `range` with `content`.
  #
  # @param [Range] range
  # @param [String] content
  # @return [Rewriter] self
  # @raise [ClobberingError] when clobbering is detected
  #
  def replace(range, content)
    append Rewriter::Action.new(range, content)
  end

  ##
  # Applies all scheduled changes to the `source_buffer` and returns
  # modified source as a new string.
  #
  # @return [String]
  #
  def process
    if in_transaction?
      raise "Do not call #{self.class}##{__method__} inside a transaction"
    end

    adjustment = 0
    source     = @source_buffer.source.dup

    @queue.sort.each do |action|
      begin_pos = action.range.begin_pos + adjustment
      end_pos   = begin_pos + action.range.length

      source[begin_pos...end_pos] = action.replacement

      adjustment += (action.replacement.length - action.range.length)
    end

    source
  end

  ##
  # Provides a protected block where a sequence of multiple rewrite actions
  # are handled atomically. If any of the actions failed by clobbering,
  # all the actions are rolled back.
  #
  # @example
  #  begin
  #    rewriter.transaction do
  #      rewriter.insert_before(range_of_something, '(')
  #      rewriter.insert_after(range_of_something, ')')
  #    end
  #  rescue Parser::ClobberingError
  #  end
  #
  # @raise [RuntimeError] when no block is passed
  # @raise [RuntimeError] when already in a transaction
  #
  def transaction
    unless block_given?
      raise "#{self.class}##{__method__} requires block"
    end

    if in_transaction?
      raise 'Nested transaction is not supported'
    end

    @pending_queue = @queue.dup
    @pending_clobber = @clobber
    @pending_insertions = @insertions

    yield

    @queue = @pending_queue
    @clobber = @pending_clobber
    @insertions = @pending_insertions

    self
  ensure
    @pending_queue = nil
    @pending_clobber = nil
    @pending_insertions = nil
  end

  private

  # Schedule a code update. If it overlaps with another update, check
  # whether they conflict, and raise a clobbering error if they do.
  # (As a special case, zero-length ranges at the same position are
  # considered to "overlap".) Otherwise, merge them.
  #
  # Updates which are adjacent to each other, but do not overlap, are also
  # merged.
  #
  # RULES:
  #
  # - Insertion ("replacing" a zero-length range):
  #   - Two insertions at the same point conflict. This is true even
  #     if the earlier insertion has already been merged with an adjacent
  #     update, and even if they are both inserting the same text.
  #   - An insertion never conflicts with a replace or remove operation
  #     on its right or left side, which does not overlap it (in other
  #     words, which does not update BOTH its right and left sides).
  #   - An insertion always conflicts with a remove operation which spans
  #     both its sides.
  #   - An insertion conflicts with a replace operation which spans both its
  #     sides, unless the replacement text is longer than the replaced text
  #     by the size of the insertion (or more), and the portion of
  #     replacement text immediately after the insertion position is
  #     identical to the inserted text.
  #
  # - Removal operations never conflict with each other.
  #
  # - Replacement operations:
  #   - Take the portion of each replacement text which falls within:
  #     - The other operation's replaced region
  #     - The other operation's replacement text, if it extends past the
  #       end of its own replaced region (in other words, if the replacement
  #       text is longer than the text it replaces)
  #   - If and only if the taken texts are identical for both operations,
  #     they do not conflict.
  #
  def append(action)
    range = action.range

    # Is this an insertion?
    if range.empty?
      # Replacing nothing with... nothing?
      return self if action.replacement.empty?

      if !action.allow_multiple_insertions? && (conflicting = clobbered_insertion?(range))
        raise_clobber_error(action, [conflicting])
      end

      record_insertion(range)

      if (adjacent = adjacent_updates?(range))
        conflicting = adjacent.find do |a|
          a.range.overlaps?(range) &&
            !replace_compatible_with_insertion?(a, action)
        end
        raise_clobber_error(action, [conflicting]) if conflicting

        merge_actions!(action, adjacent)
      else
        active_queue << action
      end
    else
      # It's a replace or remove operation.
      if (insertions = adjacent_insertions?(range))
        insertions.each do |insertion|
          if range.overlaps?(insertion.range) &&
             !replace_compatible_with_insertion?(action, insertion)
            raise_clobber_error(action, [insertion])
          else
            action = merge_actions(action, [insertion])
            active_queue.delete(insertion)
          end
        end
      end

      if (adjacent = adjacent_updates?(range))
        if can_merge?(action, adjacent)
          record_replace(range)
          merge_actions!(action, adjacent)
        else
          raise_clobber_error(action, adjacent)
        end
      else
        record_replace(range)
        active_queue << action
      end
    end

    self
  end

  def record_insertion(range)
    self.active_insertions = active_insertions | (1 << range.begin_pos)
  end

  def record_replace(range)
    self.active_clobber = active_clobber | clobbered_position_mask(range)
  end

  def clobbered_position_mask(range)
    ((1 << range.size) - 1) << range.begin_pos
  end

  def adjacent_position_mask(range)
    ((1 << (range.size + 2)) - 1) << (range.begin_pos - 1)
  end

  def adjacent_insertion_mask(range)
    ((1 << (range.size + 1)) - 1) << range.begin_pos
  end

  def clobbered_insertion?(insertion)
    insertion_pos = insertion.begin_pos
    if active_insertions & (1 << insertion_pos) != 0
      # The clobbered insertion may have already been merged with other
      # updates, so it won't necessarily have the same begin_pos.
      active_queue.find do |a|
        a.range.begin_pos <= insertion_pos && insertion_pos <= a.range.end_pos
      end
    end
  end

  def adjacent_insertions?(range)
    # Just retrieve insertions which have not been merged with an adjacent
    # remove or replace.
    if active_insertions & adjacent_insertion_mask(range) != 0
      result = active_queue.select do |a|
        a.range.empty? && adjacent?(range, a.range)
      end
      result.empty? ? nil : result
    end
  end

  def adjacent_updates?(range)
    if active_clobber & adjacent_position_mask(range) != 0
      active_queue.select { |a| adjacent?(range, a.range) }
    end
  end

  def replace_compatible_with_insertion?(replace, insertion)
    (replace.replacement.length - replace.range.size) >= insertion.range.size &&
      (offset = insertion.range.begin_pos - replace.range.begin_pos) &&
      replace.replacement[offset, insertion.replacement.length] == insertion.replacement
  end

  def can_merge?(action, existing)
    # Compare 2 replace/remove operations (neither is an insertion)
    range = action.range

    existing.all? do |other|
      overlap = range.intersect(other.range)
      next true if overlap.nil? # adjacent, not overlapping

      repl1_offset = overlap.begin_pos - range.begin_pos
      repl2_offset = overlap.begin_pos - other.range.begin_pos
      repl1_length = [other.range.length - repl2_offset,
                      other.replacement.length  - repl2_offset].max
      repl2_length = [range.length - repl1_offset,
                      action.replacement.length - repl1_offset].max

      replacement1 = action.replacement[repl1_offset, repl1_length] || ''.freeze
      replacement2 = other.replacement[repl2_offset, repl2_length] || ''.freeze
      replacement1 == replacement2
    end
  end

  def merge_actions(action, existing)
    actions = existing.push(action).sort_by do |a|
      [a.range.begin_pos, a.range.end_pos]
    end
    range = actions.first.range.join(actions.max_by { |a| a.range.end_pos }.range)

    Rewriter::Action.new(range, merge_replacements(actions))
  end

  def merge_actions!(action, existing)
    new_action = merge_actions(action, existing)
    active_queue.delete(action)
    replace_actions(existing, new_action)
  end

  def merge_replacements(actions)
    # `actions` must be sorted by beginning position
    begin_pos = actions.first.range.begin_pos
    result    = ''
    prev_act  = nil

    actions.each do |act|
      if !prev_act || act.range.disjoint?(prev_act.range)
        result << act.replacement
      else
        prev_end = [prev_act.range.begin_pos + prev_act.replacement.length,
                    prev_act.range.end_pos].max
        offset   = prev_end - act.range.begin_pos
        result << act.replacement[offset..-1] if offset < act.replacement.size
      end

      prev_act = act
    end

    result
  end

  def replace_actions(old, updated)
    old.each { |act| active_queue.delete(act) }
    active_queue << updated
  end

  def raise_clobber_error(action, existing)
    # cannot replace 3 characters with "foobar"
    diagnostic = Diagnostic.new(:error,
                                :invalid_action,
                                { :action => action },
                                action.range)
    @diagnostics.process(diagnostic)

    # clobbered by: remove 3 characters
    diagnostic = Diagnostic.new(:note,
                                :clobbered,
                                { :action => existing[0] },
                                existing[0].range)
    @diagnostics.process(diagnostic)

    raise ClobberingError, "Parser::Source::Rewriter detected clobbering"
  end

  def in_transaction?
    !@pending_queue.nil?
  end

  def active_queue
    @pending_queue || @queue
  end

  def active_clobber
    @pending_clobber || @clobber
  end

  def active_insertions
    @pending_insertions || @insertions
  end

  def active_clobber=(value)
    if @pending_clobber
      @pending_clobber = value
    else
      @clobber = value
    end
  end

  def active_insertions=(value)
    if @pending_insertions
      @pending_insertions = value
    else
      @insertions = value
    end
  end

  def adjacent?(range1, range2)
    range1.begin_pos <= range2.end_pos && range2.begin_pos <= range1.end_pos
  end
end

Instance Method Details

#insert_after(range, content) ⇒ Rewriter

Inserts new code after the given source range.

Parameters:

Returns:

Raises:



100
101
102
# File 'lib/parser/source/rewriter.rb', line 100

def insert_after(range, content)
  append Rewriter::Action.new(range.end, content)
end

#insert_after_multi(range, content) ⇒ Rewriter

Inserts new code after the given source range by allowing other insertions at the same position. Note that an insertion with latter invocation comes after earlier insertion at the same position in the rewritten source.

Examples:

Inserting ')]'

rewriter.
  insert_after_multi(range, ')').
  insert_after_multi(range, ']').
  process

Parameters:

Returns:

Raises:



121
122
123
124
# File 'lib/parser/source/rewriter.rb', line 121

def insert_after_multi(range, content)
  @insert_after_multi_order += 1
  append Rewriter::Action.new(range.end, content, true, @insert_after_multi_order)
end

#insert_before(range, content) ⇒ Rewriter

Inserts new code before the given source range.

Parameters:

Returns:

Raises:



66
67
68
# File 'lib/parser/source/rewriter.rb', line 66

def insert_before(range, content)
  append Rewriter::Action.new(range.begin, content)
end

#insert_before_multi(range, content) ⇒ Rewriter

Inserts new code before the given source range by allowing other insertions at the same position. Note that an insertion with latter invocation comes before earlier insertion at the same position in the rewritten source.

Examples:

Inserting '[('

rewriter.
  insert_before_multi(range, '(').
  insert_before_multi(range, '[').
  process

Parameters:

Returns:

Raises:



87
88
89
90
# File 'lib/parser/source/rewriter.rb', line 87

def insert_before_multi(range, content)
  @insert_before_multi_order -= 1
  append Rewriter::Action.new(range.begin, content, true, @insert_before_multi_order)
end

#processString

Applies all scheduled changes to the source_buffer and returns modified source as a new string.

Returns:



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/parser/source/rewriter.rb', line 144

def process
  if in_transaction?
    raise "Do not call #{self.class}##{__method__} inside a transaction"
  end

  adjustment = 0
  source     = @source_buffer.source.dup

  @queue.sort.each do |action|
    begin_pos = action.range.begin_pos + adjustment
    end_pos   = begin_pos + action.range.length

    source[begin_pos...end_pos] = action.replacement

    adjustment += (action.replacement.length - action.range.length)
  end

  source
end

#remove(range) ⇒ Rewriter

Removes the source range.

Parameters:

Returns:

Raises:



54
55
56
# File 'lib/parser/source/rewriter.rb', line 54

def remove(range)
  append Rewriter::Action.new(range, ''.freeze)
end

#replace(range, content) ⇒ Rewriter

Replaces the code of the source range range with content.

Parameters:

Returns:

Raises:



134
135
136
# File 'lib/parser/source/rewriter.rb', line 134

def replace(range, content)
  append Rewriter::Action.new(range, content)
end

#transactionObject

Provides a protected block where a sequence of multiple rewrite actions are handled atomically. If any of the actions failed by clobbering, all the actions are rolled back.

Examples:

begin
  rewriter.transaction do
    rewriter.insert_before(range_of_something, '(')
    rewriter.insert_after(range_of_something, ')')
  end
rescue Parser::ClobberingError
end

Raises:

  • (RuntimeError)

    when no block is passed

  • (RuntimeError)

    when already in a transaction



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/parser/source/rewriter.rb', line 181

def transaction
  unless block_given?
    raise "#{self.class}##{__method__} requires block"
  end

  if in_transaction?
    raise 'Nested transaction is not supported'
  end

  @pending_queue = @queue.dup
  @pending_clobber = @clobber
  @pending_insertions = @insertions

  yield

  @queue = @pending_queue
  @clobber = @pending_clobber
  @insertions = @pending_insertions

  self
ensure
  @pending_queue = nil
  @pending_clobber = nil
  @pending_insertions = nil
end