class RrToRspecConverter::TextTransformer

Public Class Methods

new(content, options = RrToRspecConverter::TextTransformerOptions.new) click to toggle source
# File lib/rr_to_rspec_converter/text_transformer.rb, line 7
def initialize(content, options = RrToRspecConverter::TextTransformerOptions.new)
  @options = options
  @content = content

  @source_buffer = Parser::Source::Buffer.new('(string)')
  @source_buffer.source = @content

  ast_builder = Astrolabe::Builder.new
  @parser = Parser::CurrentRuby.new(ast_builder)

  @source_rewriter = Parser::Source::Rewriter.new(@source_buffer)
end

Public Instance Methods

transform() click to toggle source
# File lib/rr_to_rspec_converter/text_transformer.rb, line 20
def transform
  root_node = @parser.parse(@source_buffer)
  unless root_node
    log "Parser saw some unparsable content, skipping...\n\n"
    return @source_rewriter.process
  end

  root_node.each_node(:send) do |node|
    target, verb, action, *args = node.children

    if verb == :with_any_args
      @source_rewriter.replace(node.loc.selector, 'with(any_args)')
    elsif verb == :any_times
      @source_rewriter.replace(node.loc.selector, 'at_least(:once)') if node.parent.children[1] != :times
    elsif verb == :returns
      @source_rewriter.replace(node.loc.selector, 'and_return')
    elsif verb == :times
      if action&.int_type?
        times = action.children[0]
        if times == 1
          @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), '.once')
        elsif times == 2
          @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), '.twice')
        end
      elsif action&.send_type? && action&.children[1] == :any_times
        @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), '.at_least(:once)')
      end
    elsif verb == :at_least && action.int_type?
      times = action.children[0]
      if times == 1
        @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), '.at_least(:once)')
      elsif times == 2
        @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), '.at_least(:twice)')
      else
        @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), ".at_least(#{times}).times")
      end
    elsif verb == :at_most && action.int_type?
      times = action.children[0]
      if times == 1
        @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), '.at_most(:once)')
      elsif times == 2
        @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), '.at_most(:twice)')
      else
        @source_rewriter.replace(range(target.loc.expression.end_pos, node.loc.expression.end_pos), ".at_most(#{times}).times")
      end
    elsif verb == :is_a
      @source_rewriter.replace(node.loc.selector, 'kind_of')
    elsif verb == :numeric
      @source_rewriter.replace(node.loc.selector, 'kind_of(Numeric)')

    # RR::WildcardMatchers::HashIncluding => hash_including
    elsif verb == :new && target.const_type? && target.children.last == :HashIncluding
      range = range(target.loc.expression.begin_pos, node.loc.selector.end_pos)
      @source_rewriter.replace(range, 'hash_including')

    # RR::WildcardMatchers::Satisfy => rr_satsify
    elsif verb == :new && target.const_type? && target.children.last == :Satisfy
      range = range(target.loc.expression.begin_pos, node.loc.selector.end_pos)
      @source_rewriter.replace(range, 'rr_satisfy')

    # rr_satisfy => satisfy
    elsif verb == :rr_satisfy
      @source_rewriter.replace(node.loc.selector, 'satisfy')

    # RR.reset => removed
    elsif verb == :reset && target.const_type? && target.children.last == :RR
      sibling = node.root? ? nil : node.parent.children[node.sibling_index + 1]
      end_pos = sibling ? sibling.loc.expression.begin_pos : node.loc.expression.end_pos
      @source_rewriter.remove(range(node.loc.expression.begin_pos, end_pos))

    # any_instance_of(klass, method: return) => allow_any_instance_of
    # any_instance_of(klass) { block } => allow_any_instance_of / expect_any_instance_of
    elsif verb == :any_instance_of
      class_name = action.loc.expression.source
      if !args.empty?
        stubs = []
        indent = line_indent(node)
        node.each_node(:pair) do |pair|
          method_name = pair.children.first.loc.expression.source.sub(/^:/,'')
          method_result = pair.children.last.loc.expression.source
          stubs << "allow_any_instance_of(#{class_name}).to receive(:#{method_name}).and_return(#{method_result})"
        end

        @source_rewriter.replace(node.loc.expression, stubs.join("\n#{indent}"))
      else # parent is a block type
        begin_node = node.parent.children.last
        begin_node.each_node(:send) do |send_node|
          stub_method = send_node.children[1]
          receive_method = send_node.parent.children[1]
          has_args = !send_node.parent.children[2].nil?
          next unless [:stub, :mock].include?(stub_method)

          expectation = remove_never_node(send_node) ? 'not_to' : 'to'
          expect_method = stub_method == :stub ? 'allow' : 'expect'
          @source_rewriter.replace(range(send_node.parent.loc.expression.begin_pos, send_node.parent.loc.selector.end_pos), "#{expect_method}_any_instance_of(#{class_name}).#{expectation} receive(:#{receive_method})#{has_args ? '.with' : ''}")
        end
        # Remove the any_instance_of
        # If it's a begin block, we have to go a level deeper
        first_statement = begin_node.begin_type? ? begin_node.children.first : begin_node
        last_statement = begin_node.begin_type? ? begin_node.children.last : begin_node
        @source_rewriter.remove(range(node.loc.expression.begin_pos, first_statement.loc.expression.begin_pos)) # start of block
        @source_rewriter.remove(range(last_statement.loc.expression.end_pos, node.parent.loc.expression.end_pos)) # end of block
      end

    # mock => expect().to receive
    elsif verb == :mock
      next if node.each_ancestor(:block).find { |n| n.children[0].children[1] == :any_instance_of }
      if action.nil?
        @source_rewriter.replace(node.loc.selector, 'double')
      else
        expectation = remove_never_node(node) ? 'not_to' : 'to'

        @source_rewriter.replace(node.loc.selector, 'expect')
        method_name = node.parent.loc.selector.source
        has_args = !node.parent.children[2].nil?
        @source_rewriter.replace(node.parent.loc.selector, "#{expectation} receive(:#{method_name})#{has_args ? '.with' : ''}")
      end

    # stub => allow().to receive and double
    elsif verb == :stub
      next if node.each_ancestor(:block).find { |n| n.children[0].children[1] == :any_instance_of }
      if action.nil?
        @source_rewriter.replace(node.loc.selector, 'double')
      else
        @source_rewriter.replace(node.loc.selector, 'allow')
        if node.parent.send_type?
          method_name = node.parent.loc.selector.source
          has_args = !node.parent.children[2].nil?
          @source_rewriter.replace(node.parent.loc.selector, "to receive(:#{method_name})#{has_args ? '.with' : ''}")
        else
          # Stubbing a method on Kernel
          method_name = node.children[2].children[1]
          @source_rewriter.replace(node.loc.expression, "allow(Kernel).to receive(:#{method_name})")
        end
      end

    # dont_allow => expect().not_to receive
    elsif verb == :dont_allow
      @source_rewriter.replace(node.loc.selector, 'expect')
      if args.empty?
        method_name = node.parent.loc.selector.source
        has_args = !node.parent.children[2].nil?
        @source_rewriter.replace(node.parent.loc.selector, "not_to receive(:#{method_name})#{has_args ? '.with' : ''}")
      else
        # dont_allow(object, :method) syntax
        method_name = args.first.children[0]
        @source_rewriter.replace(range(action.loc.expression.end_pos, node.loc.expression.end_pos), ").not_to receive(:#{method_name})")
      end
    end
  end

  @source_rewriter.process
end

Private Instance Methods

line_indent(node) click to toggle source
# File lib/rr_to_rspec_converter/text_transformer.rb, line 193
def line_indent(node)
  node.loc.expression.source_line.match(/^(\s*)/)[1]
end
log(str) click to toggle source
# File lib/rr_to_rspec_converter/text_transformer.rb, line 197
def log(str)
  return if @options.quiet?

  puts str
end
range(start_pos, end_pos) click to toggle source
# File lib/rr_to_rspec_converter/text_transformer.rb, line 176
def range(start_pos, end_pos)
  Parser::Source::Range.new(@source_buffer, start_pos, end_pos)
end
remove_never_node(node) click to toggle source

Find any never or times(0) methods chained in the expression and remove them. Return true if a node was found and removed, false otherwise.

# File lib/rr_to_rspec_converter/text_transformer.rb, line 182
def remove_never_node(node)
  if never_node = node.each_ancestor(:send).find { |n| n.children[1] == :never }
    @source_rewriter.remove(range(never_node.children.first.loc.expression.end_pos, never_node.loc.selector.end_pos))
    return true
  elsif times_zero_node = node.each_ancestor(:send).find { |n| n.children[1] == :times && n.children[2].int_type? && n.children[2].children[0] == 0 }
    @source_rewriter.remove(range(times_zero_node.children[0].loc.expression.end_pos, times_zero_node.loc.expression.end_pos))
    return true
  end
  false
end