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
-
(Array<(Symbol, Range, String{, String})>)
-
def as_nested_actions @action_root.nested_actions end
def as_replacements
-
(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?
-
(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)
-
(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,
-
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)
-
(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)
- 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)
-
(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)
- 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
def inspect "#<#{self.class} #{source_buffer.name}: #{action_summary}>" end
def merge(with)
-
(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)
-
(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
-
(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)
-
(ClobberingError)
- when clobbering is detected
Returns:
-
(Rewriter)
- self
Parameters:
-
range
(Range
) --
def remove(range) replace(range, ''.freeze) end
def replace(range, content)
-
(ClobberingError)
- when clobbering is detected
Returns:
-
(Rewriter)
- self
Parameters:
-
content
(String
) -- -
range
(Range
) --
def replace(range, content) combine(range, replacement: content) end
def transaction
-
(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)
-
(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