# frozen_string_literal: true module RuboCop module Cop module Style # In Ruby 2.7, arguments forwarding has been added. # # This cop identifies places where `do_something(*args, &block)` # can be replaced by `do_something(...)`. # # In Ruby 3.2, anonymous args/kwargs forwarding has been added. # # This cop also identifies places where `use_args(*args)`/`use_kwargs(**kwargs)` can be # replaced by `use_args(*)`/`use_kwargs(**)`; if desired, this functionality can be disabled # by setting UseAnonymousForwarding: false. # # @example # # bad # def foo(*args, &block) # bar(*args, &block) # end # # # bad # def foo(*args, **kwargs, &block) # bar(*args, **kwargs, &block) # end # # # good # def foo(...) # bar(...) # end # # @example UseAnonymousForwarding: true (default, only relevant for Ruby >= 3.2) # # bad # def foo(*args, **kwargs) # args_only(*args) # kwargs_only(**kwargs) # end # # # good # def foo(*, **) # args_only(*) # kwargs_only(**) # end # # @example UseAnonymousForwarding: false (only relevant for Ruby >= 3.2) # # good # def foo(*args, **kwargs) # args_only(*args) # kwargs_only(**kwargs) # end # # @example AllowOnlyRestArgument: true (default, only relevant for Ruby < 3.2) # # good # def foo(*args) # bar(*args) # end # # def foo(**kwargs) # bar(**kwargs) # end # # @example AllowOnlyRestArgument: false (only relevant for Ruby < 3.2) # # bad # # The following code can replace the arguments with `...`, # # but it will change the behavior. Because `...` forwards block also. # def foo(*args) # bar(*args) # end # # def foo(**kwargs) # bar(**kwargs) # end # class ArgumentsForwarding < Base include RangeHelp extend AutoCorrector extend TargetRubyVersion minimum_target_ruby_version 2.7 FORWARDING_LVAR_TYPES = %i[splat kwsplat block_pass].freeze ADDITIONAL_ARG_TYPES = %i[lvar arg].freeze FORWARDING_MSG = 'Use shorthand syntax `...` for arguments forwarding.' ARGS_MSG = 'Use anonymous positional arguments forwarding (`*`).' KWARGS_MSG = 'Use anonymous keyword arguments forwarding (`**`).' def on_def(node) return unless node.body forwardable_args = extract_forwardable_args(node.arguments) send_classifications = classify_send_nodes( node, node.each_descendant(:send).to_a, non_splat_or_block_pass_lvar_references(node.body), forwardable_args ) return if send_classifications.empty? if only_forwards_all?(send_classifications) add_forward_all_offenses(node, send_classifications, forwardable_args) elsif target_ruby_version >= 3.2 add_post_ruby_32_offenses(node, send_classifications, forwardable_args) end end alias on_defs on_def private def extract_forwardable_args(args) [args.find(&:restarg_type?), args.find(&:kwrestarg_type?), args.find(&:blockarg_type?)] end def only_forwards_all?(send_classifications) send_classifications.all? { |_, c, _, _| c == :all } end def add_forward_all_offenses(node, send_classifications, forwardable_args) send_classifications.each do |send_node, _c, forward_rest, _forward_kwrest| register_forward_all_offense(send_node, send_node, forward_rest) end rest_arg, _kwrest_arg, _block_arg = *forwardable_args register_forward_all_offense(node, node.arguments, rest_arg) end def add_post_ruby_32_offenses(def_node, send_classifications, forwardable_args) return unless use_anonymous_forwarding? rest_arg, kwrest_arg, _block_arg = *forwardable_args send_classifications.each do |send_node, _c, forward_rest, forward_kwrest| if forward_rest register_forward_args_offense(def_node.arguments, rest_arg) register_forward_args_offense(send_node, forward_rest) end if forward_kwrest register_forward_kwargs_offense(!forward_rest, def_node.arguments, kwrest_arg) register_forward_kwargs_offense(!forward_rest, send_node, forward_kwrest) end end end def non_splat_or_block_pass_lvar_references(body) body.each_descendant(:lvar, :lvasgn).filter_map do |lvar| parent = lvar.parent next if lvar.lvar_type? && FORWARDING_LVAR_TYPES.include?(parent.type) lvar.children.first end.uniq end def classify_send_nodes(def_node, send_nodes, referenced_lvars, forwardable_args) send_nodes.filter_map do |send_node| classification_and_forwards = classification_and_forwards( def_node, send_node, referenced_lvars, forwardable_args ) next unless classification_and_forwards [send_node, *classification_and_forwards] end end def classification_and_forwards(def_node, send_node, referenced_lvars, forwardable_args) classifier = SendNodeClassifier.new( def_node, send_node, referenced_lvars, forwardable_args, target_ruby_version: target_ruby_version, allow_only_rest_arguments: allow_only_rest_arguments? ) classification = classifier.classification return unless classification [classification, classifier.forwarded_rest_arg, classifier.forwarded_kwrest_arg] end def register_forward_args_offense(def_arguments_or_send, rest_arg_or_splat) add_offense(rest_arg_or_splat, message: ARGS_MSG) do |corrector| add_parens_if_missing(def_arguments_or_send, corrector) corrector.replace(rest_arg_or_splat, '*') end end def register_forward_kwargs_offense(add_parens, def_arguments_or_send, kwrest_arg_or_splat) add_offense(kwrest_arg_or_splat, message: KWARGS_MSG) do |corrector| add_parens_if_missing(def_arguments_or_send, corrector) if add_parens corrector.replace(kwrest_arg_or_splat, '**') end end def register_forward_all_offense(def_or_send, send_or_arguments, rest_or_splat) arg_range = arguments_range(def_or_send, rest_or_splat) add_offense(arg_range, message: FORWARDING_MSG) do |corrector| add_parens_if_missing(send_or_arguments, corrector) corrector.replace(arg_range, '...') end end def arguments_range(node, first_node) arguments = node.arguments.reject { |arg| ADDITIONAL_ARG_TYPES.include?(arg.type) } start_node = first_node || arguments.first range_between(start_node.source_range.begin_pos, arguments.last.source_range.end_pos) end def allow_only_rest_arguments? cop_config.fetch('AllowOnlyRestArgument', true) end def use_anonymous_forwarding? cop_config.fetch('UseAnonymousForwarding', false) end def add_parens_if_missing(node, corrector) return if parentheses?(node) add_parentheses(node, corrector) end # Classifies send nodes for possible rest/kwrest/all (including block) forwarding. class SendNodeClassifier extend NodePattern::Macros # @!method forwarded_rest_arg?(node, rest_name) def_node_matcher :forwarded_rest_arg?, '(splat (lvar %1))' # @!method extract_forwarded_kwrest_arg(node, kwrest_name) def_node_matcher :extract_forwarded_kwrest_arg, '(hash <$(kwsplat (lvar %1)) ...>)' # @!method forwarded_block_arg?(node, block_name) def_node_matcher :forwarded_block_arg?, '(block_pass {(lvar %1) nil?})' def initialize(def_node, send_node, referenced_lvars, forwardable_args, **config) @def_node = def_node @send_node = send_node @referenced_lvars = referenced_lvars @rest_arg, @kwrest_arg, @block_arg = *forwardable_args @rest_arg_name, @kwrest_arg_name, @block_arg_name = *forwardable_args.map { |a| a&.name } @config = config end def forwarded_rest_arg return nil if referenced_rest_arg? arguments.find { |arg| forwarded_rest_arg?(arg, @rest_arg_name) } end def forwarded_kwrest_arg return nil if referenced_kwrest_arg? arguments.filter_map { |arg| extract_forwarded_kwrest_arg(arg, @kwrest_arg_name) }.first end def forwarded_block_arg return nil if referenced_block_arg? arguments.find { |arg| forwarded_block_arg?(arg, @block_arg_name) } end def classification return nil unless forwarded_rest_arg || forwarded_kwrest_arg if can_forward_all? :all else :rest_or_kwrest end end private def can_forward_all? return false if any_arg_referenced? return false if ruby_32_missing_rest_or_kwest? return false unless offensive_block_forwarding? return false if additional_kwargs_or_forwarded_kwargs? no_additional_args? || (target_ruby_version >= 3.0 && no_post_splat_args?) end def ruby_32_missing_rest_or_kwest? target_ruby_version >= 3.2 && !forwarded_rest_and_kwrest_args end def offensive_block_forwarding? @block_arg ? forwarded_block_arg : allow_offense_for_no_block? end def forwarded_rest_and_kwrest_args forwarded_rest_arg && forwarded_kwrest_arg end def arguments @send_node.arguments end def referenced_rest_arg? @referenced_lvars.include?(@rest_arg_name) end def referenced_kwrest_arg? @referenced_lvars.include?(@kwrest_arg_name) end def referenced_block_arg? @referenced_lvars.include?(@block_arg_name) end def any_arg_referenced? referenced_rest_arg? || referenced_kwrest_arg? || referenced_block_arg? end def target_ruby_version @config.fetch(:target_ruby_version) end def no_post_splat_args? return true unless (splat_index = arguments.index(forwarded_rest_arg)) arg_after_splat = arguments[splat_index + 1] [nil, :hash, :block_pass].include?(arg_after_splat&.type) end def additional_kwargs_or_forwarded_kwargs? additional_kwargs? || forward_additional_kwargs? end def additional_kwargs? @def_node.arguments.any? { |a| a.kwarg_type? || a.kwoptarg_type? } end def forward_additional_kwargs? return false unless forwarded_kwrest_arg !forwarded_kwrest_arg.parent.children.one? end def allow_offense_for_no_block? !@config.fetch(:allow_only_rest_arguments) end def no_additional_args? forwardable_count = [@rest_arg, @kwrest_arg, @block_arg].compact.size @def_node.arguments.size == forwardable_count && @send_node.arguments.size == forwardable_count end end end end end end