class Parser::Source::TreeRewriter

{TreeRewriter} performs the heavy lifting in the source rewriting process. It schedules code updates to be performed in the correct order.

For simple cases, the resulting source will be obvious.

Examples for more complex cases follow. Assume these examples are acting on the source ‘’puts(:hello, :world)‘. The methods wrap, remove, etc. receive a Range as first argument; for clarity, examples below use english sentences and a string of raw code instead.

## Overlapping ranges:

Any two rewriting actions on overlapping ranges will fail and raise a ‘ClobberingError`, unless they are both deletions (covered next).

=> CloberringError

## Overlapping deletions:

The overlapping ranges are merged and ‘’:hello, :world’‘ will be removed. This policy can be changed. `:crossing_deletions` defaults to `:accept` but can be set to `:warn` or `:raise`.

## Multiple actions at the same end points:

Results will always be independent on the order they were given. Exception: rewriting actions done on exactly the same range (covered next).

Example:

The resulting string will be ‘’puts({:hello => [:everybody]})‘` and this result is independent on the order the instructions were given in.

Note that if the two “replace” were given as a single replacement of ‘, :world’ for ‘ => :everybody’, the result would be a ‘ClobberingError` because of the wrap in square brackets.

## Multiple wraps on same range:

The wraps are combined in order given and results would be ‘’puts(, :world)‘`.

## Multiple replacements on same range:

The replacements are made in the order given, so the latter replacement supersedes the former and ‘:hello’ will be replaced by ‘:hey’.

This policy can be changed. ‘:different_replacements` defaults to `:accept` but can be set to `:warn` or `:raise`.

## Swallowed insertions: wrap ‘world’ by ‘__’, ‘__’ replace ‘:hello, :world’ with ‘:hi’

A containing replacement will swallow the contained rewriting actions and ‘’:hello, :world’‘ will be replaced by `’:hi’‘.

This policy can be changed for swallowed insertions. ‘:swallowed_insertions` defaults to `:accept` but can be set to `:warn` or `:raise`

## Implementation The updates are organized in a tree, according to the ranges they act on (where children are strictly contained by their parent), hence the name.

@!attribute [r] source_buffer

@return [Source::Buffer]

@!attribute [r] diagnostics

@return [Diagnostic::Engine]

@api public

Attributes

diagnostics[R]
source_buffer[R]

Public Class Methods

new(source_buffer, crossing_deletions: :accept, different_replacements: :accept, swallowed_insertions: :accept) click to toggle source

@param [Source::Buffer] source_buffer

# File lib/parser/source/tree_rewriter.rb, line 98
def initialize(source_buffer,
               crossing_deletions: :accept,
               different_replacements: :accept,
               swallowed_insertions: :accept)
  @diagnostics = Diagnostic::Engine.new
  @diagnostics.consumer = -> diag { $stderr.puts diag.render }

  @source_buffer = source_buffer
  @in_transaction = false

  @policy = {crossing_deletions: crossing_deletions,
             different_replacements: different_replacements,
             swallowed_insertions: swallowed_insertions}.freeze
  check_policy_validity

  @enforcer = method(:enforce_policy)
  # We need a range that would be jugded as containing all other ranges,
  # including 0...0 and size...size:
  all_encompassing_range = @source_buffer.source_range.adjust(begin_pos: -1, end_pos: +1)
  @action_root = TreeRewriter::Action.new(all_encompassing_range, @enforcer)
end

Public Instance Methods

as_nested_actions() click to toggle source

Returns a representation of the rewriter as nested insertions (:wrap) and replacements.

rewriter.as_actions # =>[ [:wrap, 1...10, '(', ')'],
                          [:wrap, 2...6, '', '!'],  # aka "insert_after"
                          [:replace, 2...4, 'foo'],
                          [:replace, 5...6, ''],  # aka "removal"
                        ],

Contrary to ‘as_replacements`, this representation is sufficient to recreate exactly the rewriter.

@return [Array<(Symbol, Range, String{, String})>]

# File lib/parser/source/tree_rewriter.rb, line 299
def as_nested_actions
  @action_root.nested_actions
end
as_replacements() click to toggle source

Returns a representation of the rewriter as an ordered list of replacements.

rewriter.as_replacements # => [ [1...1, '('],
                                [2...4, 'foo'],
                                [5...6, ''],
                                [6...6, '!'],
                                [10...10, ')'],
                              ]

This representation is sufficient to recreate the result of ‘process` but it is not sufficient to recreate completely the rewriter for further merging/actions. See `as_nested_actions`

@return [Array<Range, String>] an ordered list of pairs of range & replacement

# File lib/parser/source/tree_rewriter.rb, line 281
def as_replacements
  @action_root.ordered_replacements
end
empty?() click to toggle source

Returns true iff no (non trivial) update has been recorded

@return [Boolean]

# File lib/parser/source/tree_rewriter.rb, line 125
def empty?
  @action_root.empty?
end
import!(foreign_rewriter, offset: 0) click to toggle source

For special cases where one needs to merge a rewriter attached to a different source_buffer or that needs to be offset. Policies of the receiver are used.

@param [TreeRewriter] rewriter from different source_buffer @param [Integer] offset @return [Rewriter] self @raise [IndexError] if action ranges (once offset) don’t fit the current buffer

# File lib/parser/source/tree_rewriter.rb, line 168
def import!(foreign_rewriter, offset: 0)
  return self if foreign_rewriter.empty?

  contracted = foreign_rewriter.action_root.contract
  merge_effective_range = ::Parser::Source::Range.new(
    @source_buffer,
    contracted.range.begin_pos + offset,
    contracted.range.end_pos + offset,
  )
  check_range_validity(merge_effective_range)

  merge_with = contracted.moved(@source_buffer, offset)

  @action_root = @action_root.combine(merge_with)
  self
end
in_transaction?() click to toggle source
# File lib/parser/source/tree_rewriter.rb, line 329
def in_transaction?
  @in_transaction
end
insert_after(range, content) click to toggle source

Shortcut for ‘wrap(range, nil, content)`

@param [Range] range @param [String] content @return [Rewriter] self @raise [ClobberingError] when clobbering is detected

# File lib/parser/source/tree_rewriter.rb, line 242
def insert_after(range, content)
  wrap(range, nil, content)
end
insert_before(range, content) click to toggle source

Shortcut for ‘wrap(range, content, nil)`

@param [Range] range @param [String] content @return [Rewriter] self @raise [ClobberingError] when clobbering is detected

# File lib/parser/source/tree_rewriter.rb, line 230
def insert_before(range, content)
  wrap(range, content, nil)
end
merge(with) click to toggle source

Returns a new rewriter that consists of the updates of the received and the given argument. Policies of the receiver are used.

@param [Rewriter] with @return [Rewriter] merge of receiver and argument @raise [ClobberingError] when clobbering is detected

# File lib/parser/source/tree_rewriter.rb, line 155
def merge(with)
  dup.merge!(with)
end
merge!(with) click to toggle source

Merges the updates of argument with the receiver. Policies of the receiver are used. This action is atomic in that it won’t change the receiver unless it succeeds.

@param [Rewriter] with @return [Rewriter] self @raise [ClobberingError] when clobbering is detected

# File lib/parser/source/tree_rewriter.rb, line 139
def merge!(with)
  raise 'TreeRewriter are not for the same source_buffer' unless
    source_buffer == with.source_buffer

  @action_root = @action_root.combine(with.action_root)
  self
end
process() click to toggle source

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

@return [String]

# File lib/parser/source/tree_rewriter.rb, line 252
def process
  source     = @source_buffer.source

  chunks = []
  last_end = 0
  @action_root.ordered_replacements.each do |range, replacement|
    chunks << source[last_end...range.begin_pos] << replacement
    last_end = range.end_pos
  end
  chunks << source[last_end...source.length]
  chunks.join
end
remove(range) click to toggle source

Shortcut for ‘replace(range, ”)`

@param [Range] range @return [Rewriter] self @raise [ClobberingError] when clobbering is detected

# File lib/parser/source/tree_rewriter.rb, line 217
def remove(range)
  replace(range, ''.freeze)
end
replace(range, content) click to toggle source

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

# File lib/parser/source/tree_rewriter.rb, line 193
def replace(range, content)
  combine(range, replacement: content)
end
transaction() { || ... } click to toggle source

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. Transactions can be nested.

@raise [RuntimeError] when no block is passed

# File lib/parser/source/tree_rewriter.rb, line 310
def transaction
  unless block_given?
    raise "#{self.class}##{__method__} requires block"
  end

  previous = @in_transaction
  @in_transaction = true
  restore_root = @action_root

  yield

  restore_root = nil

  self
ensure
  @action_root = restore_root if restore_root
  @in_transaction = previous
end
wrap(range, insert_before, insert_after) click to toggle source

Inserts the given strings before and after the given range.

@param [Range] range @param [String, nil] insert_before @param [String, nil] insert_after @return [Rewriter] self @raise [ClobberingError] when clobbering is detected

# File lib/parser/source/tree_rewriter.rb, line 206
def wrap(range, insert_before, insert_after)
  combine(range, insert_before: insert_before.to_s, insert_after: insert_after.to_s)
end