lib/yarp/ripper_compat.rb



# frozen_string_literal: true

require "ripper"

module YARP
  # This class is meant to provide a compatibility layer between YARP and
  # Ripper. It functions by parsing the entire tree first and then walking it
  # and executing each of the Ripper callbacks as it goes.
  #
  # This class is going to necessarily be slower than the native Ripper API. It
  # is meant as a stopgap until developers migrate to using YARP. It is also
  # meant as a test harness for the YARP parser.
  class RipperCompat
    # This class mirrors the ::Ripper::SexpBuilder subclass of ::Ripper that
    # returns the arrays of [type, *children].
    class SexpBuilder < RipperCompat
      private

      Ripper::PARSER_EVENTS.each do |event|
        define_method(:"on_#{event}") do |*args|
          [event, *args]
        end
      end

      Ripper::SCANNER_EVENTS.each do |event|
        define_method(:"on_#{event}") do |value|
          [:"@#{event}", value, [lineno, column]]
        end
      end
    end

    # This class mirrors the ::Ripper::SexpBuilderPP subclass of ::Ripper that
    # returns the same values as ::Ripper::SexpBuilder except with a couple of
    # niceties that flatten linked lists into arrays.
    class SexpBuilderPP < SexpBuilder
      private

      def _dispatch_event_new
        []
      end

      def _dispatch_event_push(list, item)
        list << item
        list
      end

      Ripper::PARSER_EVENT_TABLE.each do |event, arity|
        case event
        when /_new\z/
          alias :"on_#{event}" :_dispatch_event_new if arity == 0
        when /_add\z/
          alias :"on_#{event}" :_dispatch_event_push
        end
      end
    end

    attr_reader :source, :lineno, :column

    def initialize(source)
      @source = source
      @result = nil
      @lineno = nil
      @column = nil
    end

    ############################################################################
    # Public interface
    ############################################################################

    def error?
      result.errors.any?
    end

    def parse
      result.value.accept(self) unless error?
    end

    ############################################################################
    # Visitor methods
    ############################################################################

    def visit(node)
      node&.accept(self)
    end

    def visit_call_node(node)
      if !node.opening_loc && node.arguments.arguments.length == 1
        bounds(node.receiver.location)
        left = visit(node.receiver)

        bounds(node.arguments.arguments.first.location)
        right = visit(node.arguments.arguments.first)

        on_binary(left, source[node.message_loc.start_offset...node.message_loc.end_offset].to_sym, right)
      else
        raise NotImplementedError
      end
    end

    def visit_integer_node(node)
      bounds(node.location)
      on_int(source[node.location.start_offset...node.location.end_offset])
    end

    def visit_statements_node(node)
      bounds(node.location)
      node.body.inject(on_stmts_new) do |stmts, stmt|
        on_stmts_add(stmts, visit(stmt))
      end
    end

    def visit_token(node)
      bounds(node.location)

      case node.type
      when :MINUS
        on_op(node.value)
      when :PLUS
        on_op(node.value)
      else
        raise NotImplementedError, "Unknown token: #{node.type}"
      end
    end

    def visit_program_node(node)
      bounds(node.location)
      on_program(visit(node.statements))
    end

    ############################################################################
    # Entrypoints for subclasses
    ############################################################################

    # This is a convenience method that runs the SexpBuilder subclass parser.
    def self.sexp_raw(source)
      SexpBuilder.new(source).parse
    end

    # This is a convenience method that runs the SexpBuilderPP subclass parser.
    def self.sexp(source)
      SexpBuilderPP.new(source).parse
    end

    private

    # This method is responsible for updating lineno and column information
    # to reflect the current node.
    #
    # This method could be drastically improved with some caching on the start
    # of every line, but for now it's good enough.
    def bounds(location)
      start_offset = location.start_offset

      @lineno = source[0..start_offset].count("\n") + 1
      @column = start_offset - (source.rindex("\n", start_offset) || 0)
    end

    def result
      @result ||= YARP.parse(source)
    end

    def _dispatch0; end
    def _dispatch1(_); end
    def _dispatch2(_, _); end
    def _dispatch3(_, _, _); end
    def _dispatch4(_, _, _, _); end
    def _dispatch5(_, _, _, _, _); end
    def _dispatch7(_, _, _, _, _, _, _); end

    (Ripper::SCANNER_EVENT_TABLE.merge(Ripper::PARSER_EVENT_TABLE)).each do |event, arity|
      alias :"on_#{event}" :"_dispatch#{arity}"
    end
  end
end