# frozen_string_literal: true module Parser module Source ## # {Rewriter} is deprecated. Use {TreeRewriter} instead. # # TreeRewriter has simplified semantics, and customizable policies # with regards to clobbering. Please read the documentation. # # Keep in mind: # - Rewriter was discarding the `end_pos` of the given range for `insert_before`, # and the `begin_pos` for `insert_after`. These are meaningful in TreeRewriter. # - TreeRewriter's wrap/insert_before/insert_after are multiple by default, while # Rewriter would raise clobbering errors if the non '_multi' version was called. # - The TreeRewriter policy closest to Rewriter's behavior is: # different_replacements: :raise, # swallowed_insertions: :raise, # crossing_deletions: :accept # # @!attribute [r] source_buffer # @return [Source::Buffer] # # @!attribute [r] diagnostics # @return [Diagnostic::Engine] # # @api public # @deprecated Use {TreeRewriter} # class Rewriter attr_reader :source_buffer attr_reader :diagnostics ## # @param [Source::Buffer] source_buffer # @deprecated Use {TreeRewriter} # def initialize(source_buffer) self.class.warn_of_deprecation @diagnostics = Diagnostic::Engine.new @diagnostics.consumer = lambda do |diag| $stderr.puts diag.render end @source_buffer = source_buffer @queue = [] @clobber = 0 @insertions = 0 # clobbered zero-length positions; index 0 is the far left @insert_before_multi_order = 0 @insert_after_multi_order = 0 @pending_queue = nil @pending_clobber = nil @pending_insertions = nil end ## # Removes the source range. # # @param [Range] range # @return [Rewriter] self # @raise [ClobberingError] when clobbering is detected # @deprecated Use {TreeRewriter#remove} # def remove(range) append Rewriter::Action.new(range, ''.freeze) end ## # Inserts new code before the given source range. # # @param [Range] range # @param [String] content # @return [Rewriter] self # @raise [ClobberingError] when clobbering is detected # @deprecated Use {TreeRewriter#insert_before} # def insert_before(range, content) append Rewriter::Action.new(range.begin, content) end ## # Inserts new code before and after the given source range. # # @param [Range] range # @param [String] before # @param [String] after # @return [Rewriter] self # @raise [ClobberingError] when clobbering is detected # @deprecated Use {TreeRewriter#wrap} # def wrap(range, before, after) append Rewriter::Action.new(range.begin, before) append Rewriter::Action.new(range.end, after) end ## # Inserts new code before the given source range by allowing other # insertions at the same position. # Note that an insertion with latter invocation comes _before_ earlier # insertion at the same position in the rewritten source. # # @example Inserting '[(' # rewriter. # insert_before_multi(range, '('). # insert_before_multi(range, '['). # process # # @param [Range] range # @param [String] content # @return [Rewriter] self # @raise [ClobberingError] when clobbering is detected # @deprecated Use {TreeRewriter#insert_before} # def insert_before_multi(range, content) @insert_before_multi_order -= 1 append Rewriter::Action.new(range.begin, content, true, @insert_before_multi_order) end ## # Inserts new code after the given source range. # # @param [Range] range # @param [String] content # @return [Rewriter] self # @raise [ClobberingError] when clobbering is detected # @deprecated Use {TreeRewriter#insert_after} # def insert_after(range, content) append Rewriter::Action.new(range.end, content) end ## # Inserts new code after the given source range by allowing other # insertions at the same position. # Note that an insertion with latter invocation comes _after_ earlier # insertion at the same position in the rewritten source. # # @example Inserting ')]' # rewriter. # insert_after_multi(range, ')'). # insert_after_multi(range, ']'). # process # # @param [Range] range # @param [String] content # @return [Rewriter] self # @raise [ClobberingError] when clobbering is detected # @deprecated Use {TreeRewriter#insert_after} # def insert_after_multi(range, content) @insert_after_multi_order += 1 append Rewriter::Action.new(range.end, content, true, @insert_after_multi_order) 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 # @deprecated Use {TreeRewriter#replace} # def replace(range, content) append Rewriter::Action.new(range, content) end ## # Applies all scheduled changes to the `source_buffer` and returns # modified source as a new string. # # @return [String] # @deprecated Use {TreeRewriter#process} # def process if in_transaction? raise "Do not call #{self.class}##{__method__} inside a transaction" end adjustment = 0 source = @source_buffer.source.dup @queue.sort.each do |action| begin_pos = action.range.begin_pos + adjustment end_pos = begin_pos + action.range.length source[begin_pos...end_pos] = action.replacement adjustment += (action.replacement.length - action.range.length) end source 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. # # @example # begin # rewriter.transaction do # rewriter.insert_before(range_of_something, '(') # rewriter.insert_after(range_of_something, ')') # end # rescue Parser::ClobberingError # end # # @raise [RuntimeError] when no block is passed # @raise [RuntimeError] when already in a transaction # @deprecated Use {TreeRewriter#transaction} # def transaction unless block_given? raise "#{self.class}##{__method__} requires block" end if in_transaction? raise 'Nested transaction is not supported' end @pending_queue = @queue.dup @pending_clobber = @clobber @pending_insertions = @insertions yield @queue = @pending_queue @clobber = @pending_clobber @insertions = @pending_insertions self ensure @pending_queue = nil @pending_clobber = nil @pending_insertions = nil end private # Schedule a code update. If it overlaps with another update, check # whether they conflict, and raise a clobbering error if they do. # (As a special case, zero-length ranges at the same position are # considered to "overlap".) Otherwise, merge them. # # Updates which are adjacent to each other, but do not overlap, are also # merged. # # RULES: # # - Insertion ("replacing" a zero-length range): # - Two insertions at the same point conflict. This is true even # if the earlier insertion has already been merged with an adjacent # update, and even if they are both inserting the same text. # - An insertion never conflicts with a replace or remove operation # on its right or left side, which does not overlap it (in other # words, which does not update BOTH its right and left sides). # - An insertion always conflicts with a remove operation which spans # both its sides. # - An insertion conflicts with a replace operation which spans both its # sides, unless the replacement text is longer than the replaced text # by the size of the insertion (or more), and the portion of # replacement text immediately after the insertion position is # identical to the inserted text. # # - Removal operations never conflict with each other. # # - Replacement operations: # - Take the portion of each replacement text which falls within: # - The other operation's replaced region # - The other operation's replacement text, if it extends past the # end of its own replaced region (in other words, if the replacement # text is longer than the text it replaces) # - If and only if the taken texts are identical for both operations, # they do not conflict. # def append(action) range = action.range # Is this an insertion? if range.empty? # Replacing nothing with... nothing? return self if action.replacement.empty? if !action.allow_multiple_insertions? && (conflicting = clobbered_insertion?(range)) raise_clobber_error(action, [conflicting]) end record_insertion(range) if (adjacent = adjacent_updates?(range)) conflicting = adjacent.find do |a| a.range.overlaps?(range) && !replace_compatible_with_insertion?(a, action) end raise_clobber_error(action, [conflicting]) if conflicting merge_actions!(action, adjacent) else active_queue << action end else # It's a replace or remove operation. if (insertions = adjacent_insertions?(range)) insertions.each do |insertion| if range.overlaps?(insertion.range) && !replace_compatible_with_insertion?(action, insertion) raise_clobber_error(action, [insertion]) else action = merge_actions(action, [insertion]) active_queue.delete(insertion) end end end if (adjacent = adjacent_updates?(range)) if can_merge?(action, adjacent) record_replace(range) merge_actions!(action, adjacent) else raise_clobber_error(action, adjacent) end else record_replace(range) active_queue << action end end self end def record_insertion(range) self.active_insertions = active_insertions | (1 << range.begin_pos) end def record_replace(range) self.active_clobber = active_clobber | clobbered_position_mask(range) end def clobbered_position_mask(range) ((1 << range.size) - 1) << range.begin_pos end def adjacent_position_mask(range) ((1 << (range.size + 2)) - 1) << (range.begin_pos - 1) end def adjacent_insertion_mask(range) ((1 << (range.size + 1)) - 1) << range.begin_pos end def clobbered_insertion?(insertion) insertion_pos = insertion.begin_pos if active_insertions & (1 << insertion_pos) != 0 # The clobbered insertion may have already been merged with other # updates, so it won't necessarily have the same begin_pos. active_queue.find do |a| a.range.begin_pos <= insertion_pos && insertion_pos <= a.range.end_pos end end end def adjacent_insertions?(range) # Just retrieve insertions which have not been merged with an adjacent # remove or replace. if active_insertions & adjacent_insertion_mask(range) != 0 result = active_queue.select do |a| a.range.empty? && adjacent?(range, a.range) end result.empty? ? nil : result end end def adjacent_updates?(range) if active_clobber & adjacent_position_mask(range) != 0 active_queue.select { |a| adjacent?(range, a.range) } end end def replace_compatible_with_insertion?(replace, insertion) (replace.replacement.length - replace.range.size) >= insertion.range.size && (offset = insertion.range.begin_pos - replace.range.begin_pos) && replace.replacement[offset, insertion.replacement.length] == insertion.replacement end def can_merge?(action, existing) # Compare 2 replace/remove operations (neither is an insertion) range = action.range existing.all? do |other| overlap = range.intersect(other.range) next true if overlap.nil? # adjacent, not overlapping repl1_offset = overlap.begin_pos - range.begin_pos repl2_offset = overlap.begin_pos - other.range.begin_pos repl1_length = [other.range.length - repl2_offset, other.replacement.length - repl2_offset].max repl2_length = [range.length - repl1_offset, action.replacement.length - repl1_offset].max replacement1 = action.replacement[repl1_offset, repl1_length] || ''.freeze replacement2 = other.replacement[repl2_offset, repl2_length] || ''.freeze replacement1 == replacement2 end end def merge_actions(action, existing) actions = existing.push(action).sort_by do |a| [a.range.begin_pos, a.range.end_pos] end range = actions.first.range.join(actions.max_by { |a| a.range.end_pos }.range) Rewriter::Action.new(range, merge_replacements(actions)) end def merge_actions!(action, existing) new_action = merge_actions(action, existing) active_queue.delete(action) replace_actions(existing, new_action) end def merge_replacements(actions) result = ''.dup prev_act = nil actions.each do |act| if !prev_act || act.range.disjoint?(prev_act.range) result << act.replacement else prev_end = [prev_act.range.begin_pos + prev_act.replacement.length, prev_act.range.end_pos].max offset = prev_end - act.range.begin_pos result << act.replacement[offset..-1] if offset < act.replacement.size end prev_act = act end result end def replace_actions(old, updated) old.each { |act| active_queue.delete(act) } active_queue << updated end def raise_clobber_error(action, existing) # cannot replace 3 characters with "foobar" diagnostic = Diagnostic.new(:error, :invalid_action, { :action => action }, action.range) @diagnostics.process(diagnostic) # clobbered by: remove 3 characters diagnostic = Diagnostic.new(:note, :clobbered, { :action => existing[0] }, existing[0].range) @diagnostics.process(diagnostic) raise ClobberingError, "Parser::Source::Rewriter detected clobbering" end def in_transaction? !@pending_queue.nil? end def active_queue @pending_queue || @queue end def active_clobber @pending_clobber || @clobber end def active_insertions @pending_insertions || @insertions end def active_clobber=(value) if @pending_clobber @pending_clobber = value else @clobber = value end end def active_insertions=(value) if @pending_insertions @pending_insertions = value else @insertions = value end end def adjacent?(range1, range2) range1.begin_pos <= range2.end_pos && range2.begin_pos <= range1.end_pos end DEPRECATION_WARNING = [ 'Parser::Source::Rewriter is deprecated.', 'Please update your code to use Parser::Source::TreeRewriter instead' ].join("\n").freeze extend Deprecation end end end