lib/rubocop/ast/node/mixin/method_dispatch_node.rb
# frozen_string_literal: true
module RuboCop
module AST
# Common functionality for nodes that are a kind of method dispatch:
# `send`, `csend`, `super`, `zsuper`, `yield`, `defined?`,
# and (modern only): `index`, `indexasgn`, `lambda`
module MethodDispatchNode # rubocop:disable Metrics/ModuleLength
extend NodePattern::Macros
include MethodIdentifierPredicates
ARITHMETIC_OPERATORS = %i[+ - * / % **].freeze
private_constant :ARITHMETIC_OPERATORS
SPECIAL_MODIFIERS = %w[private protected].freeze
private_constant :SPECIAL_MODIFIERS
# The receiving node of the method dispatch.
#
# @return [Node, nil] the receiver of the dispatched method or `nil`
def receiver
node_parts[0]
end
# The name of the dispatched method as a symbol.
#
# @return [Symbol] the name of the dispatched method
def method_name
node_parts[1]
end
# The source range for the method name or keyword that dispatches this call.
#
# @return [Parser::Source::Range] the source range for the method name or keyword
def selector
if loc.respond_to? :keyword
loc.keyword
else
loc.selector
end
end
# The `block` or `numblock` node associated with this method dispatch, if any.
#
# @return [BlockNode, nil] the `block` or `numblock` node associated with this method
# call or `nil`
def block_node
parent if block_literal?
end
# Checks whether the dispatched method is a macro method. A macro method
# is defined as a method that sits in a class, module, or block body and
# has an implicit receiver.
#
# @note This does not include DSLs that use nested blocks, like RSpec
#
# @return [Boolean] whether the dispatched method is a macro method
def macro?
!receiver && in_macro_scope?
end
# Checks whether the dispatched method is an access modifier.
#
# @return [Boolean] whether the dispatched method is an access modifier
def access_modifier?
bare_access_modifier? || non_bare_access_modifier?
end
# Checks whether the dispatched method is a bare access modifier that
# affects all methods defined after the macro.
#
# @return [Boolean] whether the dispatched method is a bare
# access modifier
def bare_access_modifier?
macro? && bare_access_modifier_declaration?
end
# Checks whether the dispatched method is a non-bare access modifier that
# affects only the method it receives.
#
# @return [Boolean] whether the dispatched method is a non-bare
# access modifier
def non_bare_access_modifier?
macro? && non_bare_access_modifier_declaration?
end
# Checks whether the dispatched method is a bare `private` or `protected`
# access modifier that affects all methods defined after the macro.
#
# @return [Boolean] whether the dispatched method is a bare
# `private` or `protected` access modifier
def special_modifier?
bare_access_modifier? && SPECIAL_MODIFIERS.include?(source)
end
# Checks whether the name of the dispatched method 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
# Checks whether the dispatched method is a setter method.
#
# @return [Boolean] whether the dispatched method is a setter
def setter_method?
loc.respond_to?(:operator) && loc.operator
end
alias assignment? setter_method?
# Checks whether the dispatched method 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.respond_to?(:dot) && loc.dot&.is?('.')
end
# Checks whether the dispatched method 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.respond_to?(:dot) && loc.dot&.is?('::')
end
# Checks whether the dispatched method uses a safe navigation operator to
# connect the receiver and the method name.
#
# @return [Boolean] whether the method was called with a connecting dot
def safe_navigation?
loc.respond_to?(:dot) && loc.dot&.is?('&.')
end
# Checks whether the *explicit* receiver of this method dispatch is
# `self`.
#
# @return [Boolean] whether the receiver of this method dispatch is `self`
def self_receiver?
receiver&.self_type?
end
# Checks whether the *explicit* receiver of this method dispatch is a
# `const` node.
#
# @return [Boolean] whether the receiver of this method dispatch
# is a `const` node
def const_receiver?
receiver&.const_type?
end
# Checks whether the method dispatch is the implicit form of `#call`,
# e.g. `foo.(bar)`.
#
# @return [Boolean] whether the method is the implicit form of `#call`
def implicit_call?
method?(:call) && !selector
end
# Whether this method dispatch has an explicit block.
#
# @return [Boolean] whether the dispatched method has a block
def block_literal?
(parent&.block_type? || parent&.numblock_type?) && eql?(parent.send_node)
end
# Checks whether this node is an arithmetic operation
#
# @return [Boolean] whether the dispatched method is an arithmetic
# operation
def arithmetic_operation?
ARITHMETIC_OPERATORS.include?(method_name)
end
# Checks if this node is part of a chain of `def` or `defs` modifiers.
#
# @example
#
# private def foo; end
#
# @return whether the `def|defs` node is a modifier or not.
# See also `def_modifier` that returns the node or `nil`
def def_modifier?(node = self)
!!def_modifier(node)
end
# Checks if this node is part of a chain of `def` or `defs` modifiers.
#
# @example
#
# private def foo; end
#
# @return [Node | nil] returns the `def|defs` node this is a modifier for,
# or `nil` if it isn't a def modifier
def def_modifier(node = self)
arg = node.children[2]
return unless node.send_type? && node.receiver.nil? && arg.is_a?(::AST::Node)
return arg if arg.def_type? || arg.defs_type?
def_modifier(arg)
end
# Checks whether this is a lambda. Some versions of parser parses
# non-literal lambdas as a method send.
#
# @return [Boolean] whether this method is a lambda
def lambda?
block_literal? && command?(:lambda)
end
# Checks whether this is a lambda literal (stabby lambda.)
#
# @example
#
# -> (foo) { bar }
#
# @return [Boolean] whether this method is a lambda literal
def lambda_literal?
loc.expression.source == '->' && block_literal?
end
# Checks whether this is a unary operation.
#
# @example
#
# -foo
#
# @return [Boolean] whether this method is a unary operation
def unary_operation?
return false unless selector
operator_method? && loc.expression.begin_pos == selector.begin_pos
end
# Checks whether this is a binary operation.
#
# @example
#
# foo + bar
#
# @return [Boolean] whether this method is a binary operation
def binary_operation?
return false unless selector
operator_method? && loc.expression.begin_pos != selector.begin_pos
end
private
# @!method in_macro_scope?(node = self)
def_node_matcher :in_macro_scope?, <<~PATTERN
{
root? # Either a root node,
^{ # or the parent is...
sclass class module class_constructor? # a class-like node
[ { # or some "wrapper"
kwbegin begin block numblock
(if _condition <%0 _>) # note: we're excluding the condition of `if` nodes
}
#in_macro_scope? # that is itself in a macro scope
]
}
}
PATTERN
# @!method adjacent_def_modifier?(node = self)
def_node_matcher :adjacent_def_modifier?, <<~PATTERN
(send nil? _ ({def defs} ...))
PATTERN
# @!method bare_access_modifier_declaration?(node = self)
def_node_matcher :bare_access_modifier_declaration?, <<~PATTERN
(send nil? {:public :protected :private :module_function})
PATTERN
# @!method non_bare_access_modifier_declaration?(node = self)
def_node_matcher :non_bare_access_modifier_declaration?, <<~PATTERN
(send nil? {:public :protected :private :module_function} _)
PATTERN
end
end
end