class Parser::Source::TreeRewriter::Action

@api private

Actions are arranged in a tree and get combined so that:

children are strictly contained by their parent
sibblings all disjoint from one another and ordered
only actions with replacement==nil may have children

Attributes

children[R]
insert_after[R]
insert_before[R]
range[R]
replacement[R]

Public Class Methods

new(range, enforcer, insert_before: '', replacement: nil, insert_after: '', children: [] ) click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 16
def initialize(range, enforcer,
     insert_before: '',
     replacement: nil,
     insert_after: '',
     children: []
  )
  @range, @enforcer, @children, @insert_before, @replacement, @insert_after =
    range, enforcer, children.freeze, insert_before.freeze, replacement, insert_after.freeze

  freeze
end

Public Instance Methods

combine(action) click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 28
def combine(action)
  return self if action.empty? # Ignore empty action
  do_combine(action)
end
contract() click to toggle source

A root action has its range set to the whole source range, even though it typically do not act on that range. This method returns the action as if it was a child action with its range contracted. @return [Action]

# File lib/parser/source/tree_rewriter/action.rb, line 67
def contract
  raise 'Empty actions can not be contracted' if empty?
  return self if insertion?
  range = @range.with(
    begin_pos: children.first.range.begin_pos,
    end_pos: children.last.range.end_pos,
  )
  with(range: range)
end
empty?() click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 33
def empty?
  @insert_before.empty? &&
    @insert_after.empty? &&
    @children.empty? &&
    (@replacement == nil || (@replacement.empty? && @range.empty?))
end
insertion?() click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 57
def insertion?
  !insert_before.empty? || !insert_after.empty? || (replacement && !replacement.empty?)
end
moved(source_buffer, offset) click to toggle source

@return [Action] that has been moved to the given source_buffer and with the given offset No check is done on validity of resulting range.

# File lib/parser/source/tree_rewriter/action.rb, line 80
def moved(source_buffer, offset)
  moved_range = ::Parser::Source::Range.new(
    source_buffer,
    @range.begin_pos + offset,
    @range.end_pos + offset
  )
  with(
    range: moved_range,
    children: children.map { |child| child.moved(source_buffer, offset) }
  )
end
nested_actions() click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 49
def nested_actions
  actions = []
  actions << [:wrap, @range, @insert_before, @insert_after] if !@insert_before.empty? ||
                                                               !@insert_after.empty?
  actions << [:replace, @range, @replacement] if @replacement
  actions.concat(@children.flat_map(&:nested_actions))
end
ordered_replacements() click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 40
def ordered_replacements
  reps = []
  reps << [@range.begin, @insert_before] unless @insert_before.empty?
  reps << [@range, @replacement] if @replacement
  reps.concat(@children.flat_map(&:ordered_replacements))
  reps << [@range.end, @insert_after] unless @insert_after.empty?
  reps
end

Protected Instance Methods

analyse_hierarchy(action) click to toggle source

Returns the children in a hierarchy with respect to `action`:

:sibbling_left, sibbling_right (for those that are disjoint from `action`)
:parent (in case one of our children contains `action`)
:child (in case `action` strictly contains some of our children)
:fusible (in case `action` overlaps some children but they can be fused in one deletion)
or raises a `CloberingError`

In case a child has equal range to `action`, it is returned as `:parent` Reminder: an empty range 1…1 is considered disjoint from 1…10

# File lib/parser/source/tree_rewriter/action.rb, line 158
def analyse_hierarchy(action)
  r = action.range
  # left_index is the index of the first child that isn't completely to the left of action
  left_index = bsearch_child_index { |child| child.range.end_pos > r.begin_pos }
  # right_index is the index of the first child that is completely on the right of action
  start = left_index == 0 ? 0 : left_index - 1  # See "corner case" below for reason of -1
  right_index = bsearch_child_index(start) { |child| child.range.begin_pos >= r.end_pos }
  center = right_index - left_index
  case center
  when 0
    # All children are disjoint from action, nothing else to do
  when -1
    # Corner case: if a child has empty range == action's range
    # then it will appear to be both disjoint and to the left of action,
    # as well as disjoint and to the right of action.
    # Since ranges are equal, we return it as parent
    left_index -= 1  # Fix indices, as otherwise this child would be
    right_index += 1 # considered as a sibbling (both left and right!)
    parent = @children[left_index]
  else
    overlap_left = @children[left_index].range.begin_pos <=> r.begin_pos
    overlap_right = @children[right_index-1].range.end_pos <=> r.end_pos

    # For one child to be the parent of action, we must have:
    if center == 1 && overlap_left <= 0 && overlap_right >= 0
      parent = @children[left_index]
    else
      # Otherwise consider all non disjoint elements (center) to be contained...
      contained = @children[left_index...right_index]
      fusible = check_fusible(action,
        (contained.shift if overlap_left < 0),  # ... but check first and last one
        (contained.pop if overlap_right > 0)    # ... for overlaps
      )
    end
  end

  {
    parent: parent,
    sibbling_left: @children[0...left_index],
    sibbling_right: @children[right_index...@children.size],
    fusible: fusible,
    child: contained,
  }
end
bsearch_child_index(from = 0) { |children| ... } click to toggle source

Similar to @children.bsearch_index || size except allows for a starting point and `bsearch_index` is only Ruby 2.3+

# File lib/parser/source/tree_rewriter/action.rb, line 145
def bsearch_child_index(from = 0)
  size = @children.size
  (from...size).bsearch { |i| yield @children[i] } || size
end
call_enforcer_for_merge(action) click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 224
def call_enforcer_for_merge(action)
  @enforcer.call(:different_replacements) do
    if @replacement && action.replacement && @replacement != action.replacement
      {range: @range, replacement: action.replacement, other_replacement: @replacement}
    end
  end
end
check_fusible(action, *fusible) click to toggle source

@param [Array(Action | nil)] fusible

# File lib/parser/source/tree_rewriter/action.rb, line 204
def check_fusible(action, *fusible)
  fusible.compact!
  return if fusible.empty?
  fusible.each do |child|
    kind = action.insertion? || child.insertion? ? :crossing_insertions : :crossing_deletions
    @enforcer.call(kind) { {range: action.range, conflict: child.range} }
  end
  fusible
end
combine_children(more_children) click to toggle source

Assumes `more_children` all contained within `@range`

# File lib/parser/source/tree_rewriter/action.rb, line 129
def combine_children(more_children)
  more_children.inject(self) do |parent, new_child|
    parent.place_in_hierarchy(new_child)
  end
end
do_combine(action) click to toggle source

Assumes range.contains?(action.range) && action.children.empty?

# File lib/parser/source/tree_rewriter/action.rb, line 102
def do_combine(action)
  if action.range == @range
    merge(action)
  else
    place_in_hierarchy(action)
  end
end
fuse_deletions(action, fusible, other_sibblings) click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 135
def fuse_deletions(action, fusible, other_sibblings)
  without_fusible = with(children: other_sibblings)
  fused_range = [action, *fusible].map(&:range).inject(:join)
  fused_deletion = action.with(range: fused_range)
  without_fusible.do_combine(fused_deletion)
end
merge(action) click to toggle source

Assumes action.range == range && action.children.empty?

# File lib/parser/source/tree_rewriter/action.rb, line 215
def merge(action)
  call_enforcer_for_merge(action)
  with(
    insert_before: "#{action.insert_before}#{insert_before}",
    replacement: action.replacement || @replacement,
    insert_after: "#{insert_after}#{action.insert_after}",
  ).combine_children(action.children)
end
place_in_hierarchy(action) click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 110
def place_in_hierarchy(action)
  family = analyse_hierarchy(action)

  if family[:fusible]
    fuse_deletions(action, family[:fusible], [*family[:sibbling_left], *family[:child], *family[:sibbling_right]])
  else
    extra_sibbling = if family[:parent]  # action should be a descendant of one of the children
      family[:parent].do_combine(action)
    elsif family[:child]                 # or it should become the parent of some of the children,
      action.with(children: family[:child], enforcer: @enforcer)
        .combine_children(action.children)
    else                                 # or else it should become an additional child
      action
    end
    with(children: [*family[:sibbling_left], extra_sibbling, *family[:sibbling_right]])
  end
end
swallow(children) click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 232
def swallow(children)
  @enforcer.call(:swallowed_insertions) do
    insertions = children.select(&:insertion?)

    {range: @range, conflict: insertions.map(&:range)} unless insertions.empty?
  end
  []
end
with(range: @range, enforcer: @enforcer, children: @children, insert_before: @insert_before, replacement: @replacement, insert_after: @insert_after) click to toggle source
# File lib/parser/source/tree_rewriter/action.rb, line 96
def with(range: @range, enforcer: @enforcer, children: @children, insert_before: @insert_before, replacement: @replacement, insert_after: @insert_after)
  children = swallow(children) if replacement
  self.class.new(range, enforcer, children: children, insert_before: insert_before, replacement: replacement, insert_after: insert_after)
end