class Parser::Source::TreeRewriter


@api public
@return [Diagnostic::Engine]
@!attribute [r] diagnostics
@return [Source::Buffer]
@!attribute [r] source_buffer
(where children are strictly contained by their parent), hence the name.
The updates are organized in a tree, according to the ranges they act on
## Implementation
defaults to ‘:accept` but can be set to `:warn` or `:raise`
This policy can be changed for swallowed insertions. `:swallowed_insertions`
and `’:hello, :world’‘ will be replaced by `’:hi’‘.
A containing replacement will swallow the contained rewriting actions
replace ’:hello, :world’ with ‘:hi’
wrap ‘world’ by ‘__’, ‘__’
## Swallowed insertions:
but can be set to ‘:warn` or `:raise`.
This policy can be changed. `:different_replacements` defaults to `:accept`
supersedes the former and ’:hello’ will be replaced by ‘:hey’.
The replacements are made in the order given, so the latter replacement
* replace ‘:hello’ by ‘:hey’
* replace ‘:hello’ by ‘:hi’, then
## Multiple replacements on same range:
The wraps are combined in order given and results would be ‘’puts(, :world)‘`.
* wrap ’:hello’ with ‘[’ and ‘]’
* wrap ‘:hello’ with ‘(’ and ‘)’
## Multiple wraps on same range:
in square brackets.
for ‘ => :everybody’, the result would be a ‘ClobberingError` because of the wrap
Note that if the two “replace” were given as a single replacement of ’, :world’
and this result is independent on the order the instructions were given in.
The resulting string will be ‘’puts({:hello => [:everybody]})‘`
* wrap ’:world’ with ‘[’, ‘]’
* replace ‘:world’ with ‘:everybody’
* wrap ‘:hello, :world’ with ‘{’ and ‘}’
* replace ‘, ’ by ‘ => ’
Example:
Exception: rewriting actions done on exactly the same range (covered next).
Results will always be independent on the order they were given.
## Multiple actions at the same end points:
but can be set to ‘:warn` or `:raise`.
This policy can be changed. `:crossing_deletions` defaults to `:accept`
The overlapping ranges are merged and `’:hello, :world’‘ will be removed.
* remove ’, :world’
* remove ‘:hello, ’
## Overlapping deletions:
=> CloberringError
* wrap ‘, :world’ with ‘(’ and ‘)’
* wrap ‘:hello, ’ with ‘(’ and ‘)’
a ‘ClobberingError`, unless they are both deletions (covered next).
Any two rewriting actions on overlapping ranges will fail and raise
## Overlapping ranges:
sentences and a string of raw code instead.
receive a Range as first argument; for clarity, examples below use english
the source `’puts(:hello, :world)‘. The methods #wrap, #remove, etc.
Examples for more complex cases follow. Assume these examples are acting on
For simple cases, the resulting source will be obvious.
It schedules code updates to be performed in the correct order.
{TreeRewriter} performs the heavy lifting in the source rewriting process.
#

def action_summary

def action_summary
  replacements = as_replacements
  case replacements.size
  when 0 then return 'empty'
  when 1..3 then #ok
  else
    replacements = replacements.first(3)
    suffix = '…'
  end
  parts = replacements.map do |(range, str)|
    if str.empty? # is this a deletion?
      "-#{range.to_range}"
    elsif range.size == 0 # is this an insertion?
      "+#{str.inspect}@#{range.begin_pos}"
    else # it is a replacement
      "^#{str.inspect}@#{range.to_range}"
    end
  end
  parts << suffix if suffix
  parts.join(', ')
end

def as_nested_actions

Returns:
  • (Array<(Symbol, Range, String{, String})>) -
def as_nested_actions
  @action_root.nested_actions
end

def as_replacements

Returns:
  • (Array) - an ordered list of pairs of range & replacement
def as_replacements
  @action_root.ordered_replacements
end

def check_policy_validity

def check_policy_validity
  invalid = @policy.values - ACTIONS
  raise ArgumentError, "Invalid policy: #{invalid.join(', ')}" unless invalid.empty?
end

def check_range_validity(range)

def check_range_validity(range)
  if range.begin_pos < 0 || range.end_pos > @source_buffer.source.size
    raise IndexError, "The range #{range.to_range} is outside the bounds of the source"
  end
  range
end

def combine(range, attributes)

def combine(range, attributes)
  range = check_range_validity(range)
  action = TreeRewriter::Action.new(range, @enforcer, **attributes)
  @action_root = @action_root.combine(action)
  self
end

def empty?

Returns:
  • (Boolean) -
def empty?
  @action_root.empty?
end

def enforce_policy(event)

def enforce_policy(event)
  return if @policy[event] == :accept
  return unless (values = yield)
  trigger_policy(event, **values)
end

def import!(foreign_rewriter, offset: 0)

Raises:
  • (IndexError) - if action ranges (once offset) don't fit the current buffer

Returns:
  • (Rewriter) - self

Parameters:
  • offset (Integer) --
  • rewriter (TreeRewriter) -- from different source_buffer
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

def in_transaction?

def in_transaction?
  @in_transaction
end

def initialize(source_buffer,

Parameters:
  • source_buffer (Source::Buffer) --
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

def insert_after(range, content)

Raises:
  • (ClobberingError) - when clobbering is detected

Returns:
  • (Rewriter) - self

Parameters:
  • content (String) --
  • range (Range) --
def insert_after(range, content)
  wrap(range, nil, content)
end

def insert_after_multi(range, text)

Deprecated:
  • Use insert_after or wrap

Other tags:
    Api: - private
def insert_after_multi(range, text)
  self.class.warn_of_deprecation
  insert_after(range, text)
end

def insert_before(range, content)

Raises:
  • (ClobberingError) - when clobbering is detected

Returns:
  • (Rewriter) - self

Parameters:
  • content (String) --
  • range (Range) --
def insert_before(range, content)
  wrap(range, content, nil)
end

def insert_before_multi(range, text)

Deprecated:
  • Use insert_after or wrap

Other tags:
    Api: - private
def insert_before_multi(range, text)
  self.class.warn_of_deprecation
  insert_before(range, text)
end

def inspect

:nodoc:
def inspect
  "#<#{self.class} #{source_buffer.name}: #{action_summary}>"
end

def merge(with)

Raises:
  • (ClobberingError) - when clobbering is detected

Returns:
  • (Rewriter) - merge of receiver and argument

Parameters:
  • with (Rewriter) --
def merge(with)
  dup.merge!(with)
end

def merge!(with)

Raises:
  • (ClobberingError) - when clobbering is detected

Returns:
  • (Rewriter) - self

Parameters:
  • with (Rewriter) --
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

def process

Returns:
  • (String) -
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

def remove(range)

Raises:
  • (ClobberingError) - when clobbering is detected

Returns:
  • (Rewriter) - self

Parameters:
  • range (Range) --
def remove(range)
  replace(range, ''.freeze)
end

def replace(range, content)

Raises:
  • (ClobberingError) - when clobbering is detected

Returns:
  • (Rewriter) - self

Parameters:
  • content (String) --
  • range (Range) --
def replace(range, content)
  combine(range, replacement: content)
end

def transaction

Raises:
  • (RuntimeError) - when no block is passed
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

def trigger_policy(event, range: raise, conflict: nil, **arguments)

def trigger_policy(event, range: raise, conflict: nil, **arguments)
  action = @policy[event] || :raise
  diag = Parser::Diagnostic.new(POLICY_TO_LEVEL[action], event, arguments, range)
  @diagnostics.process(diag)
  if conflict
    range, *highlights = conflict
    diag = Parser::Diagnostic.new(POLICY_TO_LEVEL[action], :"#{event}_conflict", arguments, range, highlights)
    @diagnostics.process(diag)
  end
  raise Parser::ClobberingError, "Parser::Source::TreeRewriter detected clobbering" if action == :raise
end

def wrap(range, insert_before, insert_after)

Raises:
  • (ClobberingError) - when clobbering is detected

Returns:
  • (Rewriter) - self

Parameters:
  • insert_after (String, nil) --
  • insert_before (String, nil) --
  • range (Range) --
def wrap(range, insert_before, insert_after)
  combine(range, insert_before: insert_before.to_s, insert_after: insert_after.to_s)
end