lib/prism/ripper_compat.rb



# frozen_string_literal: true

require "ripper"

module Prism
  # Note: This integration is not finished, and therefore still has many
  # inconsistencies with Ripper. If you'd like to help out, pull requests would
  # be greatly appreciated!
  #
  # This class is meant to provide a compatibility layer between prism 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 prism. It is also
  # meant as a test harness for the prism parser.
  #
  # To use this class, you treat `Prism::RipperCompat` effectively as you would
  # treat the `Ripper` class.
  class RipperCompat < Visitor
    # 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 # :nodoc:
        []
      end

      def _dispatch_event_push(list, item) # :nodoc:
        list << item
        list
      end

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

    # The source that is being parsed.
    attr_reader :source

    # The current line number of the parser.
    attr_reader :lineno

    # The current column number of the parser.
    attr_reader :column

    # Create a new RipperCompat object with the given source.
    def initialize(source)
      @source = source
      @result = nil
      @lineno = nil
      @column = nil
    end

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

    # True if the parser encountered an error during parsing.
    def error?
      result.failure?
    end

    # Parse the source and return the result.
    def parse
      result.magic_comments.each do |magic_comment|
        on_magic_comment(magic_comment.key, magic_comment.value)
      end

      if error?
        result.errors.each do |error|
          on_parse_error(error.message)
        end

        nil
      else
        result.value.accept(self)
      end
    end

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

    # Visit an ArrayNode node.
    def visit_array_node(node)
      elements = visit_elements(node.elements) unless node.elements.empty?
      bounds(node.location)
      on_array(elements)
    end

    # Visit a CallNode node.
    def visit_call_node(node)
      if node.variable_call?
        if node.message.match?(/^[[:alpha:]_]/)
          bounds(node.message_loc)
          return on_vcall(on_ident(node.message))
        end

        raise NotImplementedError, "Non-alpha variable call"
      end

      if node.opening_loc.nil?
        left = visit(node.receiver)
        if node.arguments&.arguments&.length == 1
          right = visit(node.arguments.arguments.first)

          on_binary(left, node.name, right)
        elsif !node.arguments || node.arguments.empty?
          on_unary(node.name, left)
        else
          raise NotImplementedError, "More than two arguments for operator"
        end
      else
        raise NotImplementedError, "Non-nil opening_loc"
      end
    end

    # Visit a FloatNode node.
    def visit_float_node(node)
      visit_number(node) { |text| on_float(text) }
    end

    # Visit a ImaginaryNode node.
    def visit_imaginary_node(node)
      visit_number(node) { |text| on_imaginary(text) }
    end

    # Visit an IntegerNode node.
    def visit_integer_node(node)
      visit_number(node) { |text| on_int(text) }
    end

    # Visit a ParenthesesNode node.
    def visit_parentheses_node(node)
      body =
        if node.body.nil?
          on_stmts_add(on_stmts_new, on_void_stmt)
        else
          visit(node.body)
        end

      bounds(node.location)
      on_paren(body)
    end

    # Visit a ProgramNode node.
    def visit_program_node(node)
      statements = visit(node.statements)
      bounds(node.location)
      on_program(statements)
    end

    # Visit a RangeNode node.
    def visit_range_node(node)
      left = visit(node.left)
      right = visit(node.right)

      bounds(node.location)
      if node.exclude_end?
        on_dot3(left, right)
      else
        on_dot2(left, right)
      end
    end

    # Visit a RationalNode node.
    def visit_rational_node(node)
      visit_number(node) { |text| on_rational(text) }
    end

    # Visit a StatementsNode node.
    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

    ############################################################################
    # 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

    # Visit a list of elements, like the elements of an array or arguments.
    def visit_elements(elements)
      bounds(elements.first.location)
      elements.inject(on_args_new) do |args, element|
        on_args_add(args, visit(element))
      end
    end

    # Visit a node that represents a number. We need to explicitly handle the
    # unary - operator.
    def visit_number(node)
      slice = node.slice
      location = node.location

      if slice[0] == "-"
        bounds_values(location.start_line, location.start_column + 1)
        value = yield slice[1..-1]

        bounds(node.location)
        on_unary(RUBY_ENGINE == "jruby" ? :- : :-@, value)
      else
        bounds(location)
        yield slice
      end
    end

    # 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)
      @lineno = location.start_line
      @column = location.start_column
    end

    # If we need to do something unusual, we can directly update the line number
    # and column to reflect the current node.
    def bounds_values(lineno, column)
      @lineno = lineno
      @column = column
    end

    # Lazily initialize the parse result.
    def result
      @result ||= Prism.parse(source)
    end

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

    alias_method :on_parse_error, :_dispatch1
    alias_method :on_magic_comment, :_dispatch2

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