Skip to content

Commit

Permalink
+ Source::TreeRewriter can now import TreeRewriters attached to othe…
Browse files Browse the repository at this point in the history
…r source buffers and apply offsets
  • Loading branch information
marcandre committed Jun 9, 2020
1 parent fd0a434 commit 733278c
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 12 deletions.
26 changes: 26 additions & 0 deletions lib/parser/source/tree_rewriter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,32 @@ def merge(with)
dup.merge!(with)
end

##
# 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
#
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

##
# Replaces the code of the source range `range` with `content`.
#
Expand Down
31 changes: 31 additions & 0 deletions lib/parser/source/tree_rewriter/action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,37 @@ def insertion?
!insert_before.empty? || !insert_after.empty? || (replacement && !replacement.empty?)
end

##
# 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]
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

##
# @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.
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

protected

attr_reader :children
Expand Down
89 changes: 77 additions & 12 deletions test/test_source_tree_rewriter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@
require 'helper'

class TestSourceTreeRewriter < Minitest::Test
def setup
@buf = Parser::Source::Buffer.new('(rewriter)',
source: 'puts(:hello, :world)')

@hello = range(5, 6)
@ll = range(7, 2)
@comma_space = range(11,2)
@world = range(13,6)
@whole = range(0, @buf.source.length)
end
module Setup
def setup
@buf = Parser::Source::Buffer.new('(rewriter)',
source: 'puts(:hello, :world)')

@hello = range(5, 6)
@ll = range(8, 2)
@comma_space = range(11,2)
@world = range(13,6)
@whole = range(0, @buf.source.length)
end

def range(from, len)
Parser::Source::Range.new(@buf, from, from + len)
def range(from, len = nil)
from, len = from.begin, from.end - from.begin unless len
Parser::Source::Range.new(@buf, from, from + len)
end
end

include Setup

# Returns either:
# - yield rewriter
# - [diagnostic, ...] (Diagnostics)
Expand Down Expand Up @@ -261,3 +266,63 @@ def test_merge
])
end
end

class TestSourceTreeRewriterImport < Minitest::Test
include TestSourceTreeRewriter::Setup
def setup
super
@buf2 = Parser::Source::Buffer.new('(rewriter 2)',
source: ':hello')

@rewriter = Parser::Source::TreeRewriter.new(@buf)

@rewriter2 = Parser::Source::TreeRewriter.new(@buf2)

@hello2 = range2(0, 6)
@ll2 = range2(3, 2)
end

def range2(from, len)
Parser::Source::Range.new(@buf2, from, from + len)
end

def test_import_with_offset
@rewriter2.wrap(@hello2, '[', ']')
@rewriter.wrap(@hello.join(@world), '{', '}')
@rewriter.import!(@rewriter2, offset: @hello.begin_pos)
assert_equal 'puts({[:hello], :world})', @rewriter.process
end

def test_import_with_offset_from_bigger_source
@rewriter2.wrap(@ll2, '[', ']')
@rewriter.wrap(@hello, '{', '}')
@rewriter2.import!(@rewriter, offset: -@hello.begin_pos)
assert_equal '{:he[ll]o}', @rewriter2.process
end

def test_import_with_offset_and_self
@rewriter.wrap(@ll, '[', ']')
@rewriter.import!(@rewriter, offset: +3)
@rewriter.replace(range(8,1), '**')
assert_equal 'puts(:he[**l]o[, ]:world)', @rewriter.process
@rewriter.import!(@rewriter, offset: -6)
assert_equal 'pu[**s]([:h]e[**l]o[, ]:world)', @rewriter.process
end

def test_import_with_invalid_offset
@rewriter.wrap(@ll, '[', ']')
m = @rewriter.dup.import!(@rewriter, offset: -@ll.begin_pos)
assert_equal '[pu]ts(:he[ll]o, :world)', m.process
off = @buf.source.size - @ll.end_pos
m = @rewriter.dup.import!(@rewriter, offset: off)
assert_equal 'puts(:he[ll]o, :worl[d)]', m.process
assert_raises { @rewriter.import!(@rewriter, offset: -@ll.begin_pos - 1) }
assert_raises { @rewriter.import!(@rewriter, offset: off + 1) }
assert_equal 'puts(:he[ll]o, :world)', @rewriter.process # Test atomicity of import!
end

def test_empty_import
assert_equal @rewriter, @rewriter.import!(@rewriter2)
assert_equal @rewriter, @rewriter.import!(@rewriter, offset: 42)
end
end

0 comments on commit 733278c

Please sign in to comment.