YorickPeterse/ruby-lint

View on GitHub
lib/ruby-lint/iterator.rb

Summary

Maintainability
A
1 hr
Test Coverage
module RubyLint
  ##
  # The Iterator class provides the means to iterate over an AST generated by
  # {RubyLint::Parser} using callback methods for the various node types
  # generated by this parser.
  #
  # For each node type two events are called: one before and one after
  # processing the node and all of its children. The names of these events are
  # the following:
  #
  # * `on_X`
  # * `after_X`
  #
  # Here "X" is the name of the event. For example, when iterator an integer
  # this would result in the event names `on_integer` and `after_integer`.
  #
  # These event names are used to call the corresponding callback methods if
  # they exist. Each callback method takes a single argument: the node (an
  # instance of {RubyLint::AST::Node}) that belongs to the event.
  #
  # Creating iterator classes is done by extending this particular class and
  # adding the needed methods to it:
  #
  #     class MyIterator < RubyLint::Iterator
  #       def on_int(node)
  #         puts node.children[0]
  #       end
  #
  #       def after_int(node)
  #         puts '---'
  #       end
  #     end
  #
  # When used this particular iterator class would display the values of all
  # integers it processes. After processing an integer it will display three
  # dashes.
  #
  # ## Skipping Child Nodes
  #
  # The `on_*` callbacks can tell the Iterator class to not process any
  # following child nodes by calling `skip_child_nodes!`:
  #
  #     def on_const(node)
  #       # ...
  #
  #       skip_child_nodes!(node)
  #     end
  #
  # Internally this uses `throw` and makes sure to only skip the child nodes of
  # the specified node (`throw` calls bubble up regardless of `catch` calls,
  # unlike when using `begin/rescue`).
  #
  # @!attribute [r] arity_cache Hash containing the amount of arguments for
  #  each method.
  #  @return [Hash]
  #
  class Iterator
    attr_reader :arity_cache

    ##
    # @param [Hash] options Hash containing custom options to set for the
    #  iterator.
    #
    def initialize(options = {})
      options.each do |key, value|
        instance_variable_set("@#{key}", value)
      end

      after_initialize if respond_to?(:after_initialize)

      @arity_cache = {}
    end

    ##
    # Recursively processes the specified list of nodes.
    #
    # @param [RubyLint::Node] node A node and optionally a set of sub nodes to
    #  iterate over.
    #
    def iterate(node)
      return unless node.is_a?(AST::Node)

      before    = :"on_#{node.type}"
      after     = :"after_#{node.type}"
      skip_node = catch :skip_child_nodes do
        execute_callback(before, node)
      end

      if skip_node != node
        node.children.each do |child|
          iterate(child) if child.is_a?(AST::Node)
        end
      end

      execute_callback(after, node)
    end

    protected

    ##
    # Instructs {#iterate} to not process any child nodes.
    #
    # @param [RubyLint::AST::Node] node
    #
    def skip_child_nodes!(node)
      throw :skip_child_nodes, node
    end

    ##
    # Executes the specified callback method if it exists.
    #
    # @param [String|Symbol] name The name of the callback method to execute.
    # @param [Array] args Arguments to pass to the callback method.
    #
    def execute_callback(name, *args)
      return unless respond_to?(name)

      unless arity_cache.key?(name)
        arity_cache[name] = method(name).arity
      end

      if arity_cache[name] == 0
        send(name)
      else
        send(name, *args)
      end
    end
  end # Iterator
end # RubyLint