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).
-
wrap ‘:hello, ’ with ‘(’ and ‘)’
-
wrap ‘, :world’ with ‘(’ and ‘)’
=> CloberringError
## Overlapping deletions:
-
remove ‘:hello, ’
-
remove ‘, :world’
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:
-
replace ‘, ’ by ‘ => ’
-
wrap ‘:hello, :world’ with ‘{’ and ‘}’
-
replace ‘:world’ with ‘:everybody’
-
wrap ‘:world’ with ‘[’, ‘]’
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:
-
wrap ‘:hello’ with ‘(’ and ‘)’
-
wrap ‘:hello’ with ‘[’ and ‘]’
The wraps are combined in order given and results would be ‘’puts(, :world)‘`.
## Multiple replacements on same range:
-
replace ‘:hello’ by ‘:hi’, then
-
replace ‘:hello’ by ‘:hey’
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
Public Class Methods
Source
# 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
@param [Source::Buffer] source_buffer
Public Instance Methods
Source
# File lib/parser/source/tree_rewriter.rb, line 299 def as_nested_actions @action_root.nested_actions end
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})>]
Source
# File lib/parser/source/tree_rewriter.rb, line 281 def as_replacements @action_root.ordered_replacements end
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
Source
# File lib/parser/source/tree_rewriter.rb, line 125 def empty? @action_root.empty? end
Returns true iff no (non trivial) update has been recorded
@return [Boolean]
Source
# 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
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
Source
# File lib/parser/source/tree_rewriter.rb, line 329 def in_transaction? @in_transaction end
Source
# File lib/parser/source/tree_rewriter.rb, line 242 def insert_after(range, content) wrap(range, nil, content) end
Shortcut for ‘wrap(range, nil, content)`
@param [Range] range @param [String] content @return [Rewriter] self @raise [ClobberingError] when clobbering is detected
Source
# File lib/parser/source/tree_rewriter.rb, line 230 def insert_before(range, content) wrap(range, content, nil) end
Shortcut for ‘wrap(range, content, nil)`
@param [Range] range @param [String] content @return [Rewriter] self @raise [ClobberingError] when clobbering is detected
Source
# File lib/parser/source/tree_rewriter.rb, line 155 def merge(with) dup.merge!(with) end
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
Source
# 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
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
Source
# 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
Applies all scheduled changes to the ‘source_buffer` and returns modified source as a new string.
@return [String]
Source
# File lib/parser/source/tree_rewriter.rb, line 217 def remove(range) replace(range, ''.freeze) end
Shortcut for ‘replace(range, ”)`
@param [Range] range @return [Rewriter] self @raise [ClobberingError] when clobbering is detected
Source
# File lib/parser/source/tree_rewriter.rb, line 193 def replace(range, content) combine(range, replacement: content) 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
Source
# 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
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
Source
# 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
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