searls/rubbycop

View on GitHub
lib/rubbycop/ast/node/send_node.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

module RubbyCop
  module AST
    # A node extension for `send` nodes. This will be used in place of a plain
    # node when the builder constructs the AST, making its methods available
    # to all `send` nodes within RubbyCop.
    class SendNode < Node
      # The receiving node of the method invocation.
      #
      # @return [Node, nil] the receiver of the invoked method or `nil`
      def receiver
        node_parts[0]
      end

      # The name of the invoked method called as a string.
      #
      # @return [Symbol] the name of the invoked method
      def method_name
        node_parts[1]
      end

      # Checks whether the method name matches the argument.
      #
      # @param [Symbol, String] name the method name to check for
      # @return [Boolean] whether the method name matches the argument
      def method?(name)
        method_name == name.to_sym
      end

      # Checks whether the method is a macro method. A macro method is defined
      # as a method that sits in a class- or module body and has an implicit
      # receiver.
      #
      # @note This does not include DSLs that use nested blocks, like RSpec
      #
      # @return [Boolean] whether the method is a macro method
      def macro?
        !receiver && macro_scope?
      end

      # Checks whether the method name matches the argument and has an
      # implicit receiver.
      #
      # @param [Symbol, String] name the method name to check for
      # @return [Boolean] whether the method name matches the argument
      def command?(name)
        !receiver && method?(name)
      end

      # An array containing the arguments of the method invocation.
      #
      # @return [Array<Node>] the arguments of the method invocation or `nil`
      def arguments
        node_parts[2..-1]
      end

      # A shorthand for getting the first argument of the method invocation.
      # Equivalent to `arguments.first`.
      #
      # @return [Node, nil] the first argument of the method invocation,
      #                     or `nil` if there are no arguments
      def first_argument
        arguments[0]
      end

      # A shorthand for getting the last argument of the method invocation.
      # Equivalent to `arguments.last`.
      #
      # @return [Node, nil] the last argument of the method invocation,
      #                     or `nil` if there are no arguments
      def last_argument
        arguments[-1]
      end

      # Checks whether this method was invoked with arguments.
      #
      # @return [Boolean] whether this method was invoked with arguments
      def arguments?
        !arguments.empty?
      end

      # Checks whether this method invocation's arguments are wrapped in
      # parentheses.
      #
      # @return [Boolean] whether this method invocation's arguments are
      #                   wrapped in parentheses
      def parenthesized?
        loc.end && loc.end.is?(')')
      end

      # Checks whether the invoked method is a setter method.
      #
      # @return [Boolean] whether the invoked method is a setter
      def setter_method?
        loc.operator
      end

      # Checks whether the invoked method is an operator method.
      #
      # @return [Boolean] whether the invoked method is an operator
      def operator_method?
        RubbyCop::Cop::Util::OPERATOR_METHODS.include?(method_name)
      end

      # Checks whether the invoked method is a comparison method.
      #
      # @return [Boolean] whether the involed method is a comparison
      def comparison_method?
        COMPARISON_OPERATORS.include?(method_name)
      end

      # Checks whether the invoked method is an assignment method.
      #
      # @return [Boolean] whether the invoked method is an assignment.
      def assignment_method?
        !comparison_method? && method_name.to_s.end_with?('=')
      end

      # Checks whether the method call uses a dot to connect the receiver and
      # the method name.
      #
      # This is useful for comparison operators, which can be called either
      # with or without a dot, i.e. `foo == bar` or `foo.== bar`.
      #
      # @return [Boolean] whether the method was called with a connecting dot
      def dot?
        loc.dot && loc.dot.is?('.')
      end

      # Checks whether the method call uses a double colon to connect the
      # receiver and the method name.
      #
      # @return [Boolean] whether the method was called with a connecting dot
      def double_colon?
        loc.dot && loc.dot.is?('::')
      end

      # Checks whether the receiver of this method invocation is `self`.
      #
      # @return [Boolean] whether the receiver of this method invocation
      #                   is `self`
      def self_receiver?
        receiver && receiver.self_type?
      end

      # Checks whether the method call is of the implicit form of `#call`,
      # e.g. `foo.(bar)`.
      #
      # @return [Boolean] whether the method is an implicit form of `#call`
      def implicit_call?
        method_name == :call && !loc.selector
      end

      # Checks whether the invoked method is a predicate method.
      #
      # @return [Boolean] whether the invoked method is a predicate method
      def predicate_method?
        method_name.to_s.end_with?('?')
      end

      # Checks whether the invoked method is a bang method.
      #
      # @return [Boolean] whether the invoked method is a bang method
      def bang_method?
        method_name.to_s.end_with?('!')
      end

      # Checks whether the invoked method is a camel case method,
      # e.g. `Integer()`.
      #
      # @return [Boolean] whether the invoked method is a camel case method
      def camel_case_method?
        method_name.to_s =~ /\A[A-Z]/
      end

      # Whether the last argument of the method invocation is a block pass,
      # i.e. `&block`.
      #
      # @return [Boolean] whether the invoked method is a block pass
      def block_argument?
        arguments? && last_argument.block_pass_type?
      end

      # Checks whether any argument of the method invocation is a splat
      # argument, i.e. `*splat`.
      #
      # @return [Boolean] whether the invoked method is a splat argument
      def splat_argument?
        arguments? && arguments.any?(&:splat_type?)
      end

      # Custom destructuring method. This can be used to normalize
      # destructuring for different variations of the node.
      #
      # @return [Array] the different parts of the `send` node
      def node_parts
        to_a
      end

      private

      def_matcher :macro_scope?, <<-PATTERN
        {^({class module} ...)
         ^^({class module} ... (begin ...))}
      PATTERN
    end
  end
end