# 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(...)`. # # @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 AllowOnlyRestArgument: true (default) # # good # def foo(*args) # bar(*args) # end # # def foo(**kwargs) # bar(**kwargs) # end # # @example AllowOnlyRestArgument: false # # 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 MSG = 'Use arguments forwarding.' # @!method use_rest_arguments?(node) def_node_matcher :use_rest_arguments?, <<~PATTERN (args ({restarg kwrestarg} $_) $...) PATTERN # @!method only_rest_arguments?(node, name) def_node_matcher :only_rest_arguments?, <<~PATTERN { (send _ _ (splat (lvar %1))) (send _ _ (hash (kwsplat (lvar %1)))) } PATTERN # @!method forwarding_method_arguments?(node, rest_name, block_name, kwargs_name) def_node_matcher :forwarding_method_arguments?, <<~PATTERN { (send _ _ (splat (lvar %1)) (block-pass {(lvar %2) nil?})) (send _ _ (splat (lvar %1)) (hash (kwsplat (lvar %3))) (block-pass {(lvar %2) nil?})) } PATTERN def on_def(node) return unless node.body return unless (rest_args_name, args = use_rest_arguments?(node.arguments)) return if args.any?(&:default?) node.each_descendant(:send) do |send_node| kwargs_name, block_name = extract_argument_names_from(args) next unless forwarding_method?(send_node, rest_args_name, kwargs_name, block_name) && all_lvars_as_forwarding_method_arguments?(node, send_node) register_offense_to_forwarding_method_arguments(send_node) register_offense_to_method_definition_arguments(node) end end alias on_defs on_def private def extract_argument_names_from(args) kwargs_name = args.first.source.delete('**') if args.first&.kwrestarg_type? block_arg_name = args.last.source.delete('&') if args.last&.blockarg_type? [kwargs_name, block_arg_name].map { |name| name&.to_sym } end def forwarding_method?(node, rest_arg, kwargs, block_arg) return only_rest_arguments?(node, rest_arg) unless allow_only_rest_arguments? forwarding_method_arguments?(node, rest_arg, block_arg, kwargs) end def all_lvars_as_forwarding_method_arguments?(def_node, forwarding_method) lvars = def_node.body.each_descendant(:lvar, :lvasgn) begin_pos = forwarding_method.source_range.begin_pos end_pos = forwarding_method.source_range.end_pos lvars.all? { |lvar| lvar.source_range.begin_pos.between?(begin_pos, end_pos) } end def register_offense_to_forwarding_method_arguments(forwarding_method) add_offense(arguments_range(forwarding_method)) do |corrector| begin_pos = forwarding_method.loc.selector&.end_pos || forwarding_method.loc.dot.end_pos range = range_between(begin_pos, forwarding_method.source_range.end_pos) corrector.replace(range, '(...)') end end def register_offense_to_method_definition_arguments(method_definition) add_offense(arguments_range(method_definition)) do |corrector| arguments_range = range_with_surrounding_space( method_definition.arguments.source_range, side: :left ) corrector.replace(arguments_range, '(...)') end end def arguments_range(node) arguments = node.arguments range_between(arguments.first.source_range.begin_pos, arguments.last.source_range.end_pos) end def allow_only_rest_arguments? cop_config.fetch('AllowOnlyRestArgument', true) end end end end end