Bug #12022
closedInconsistent behavior with splatted named arguments
Description
The Bug¶
When an empty hash is splatted (using **) into a call of a method with no parameters, the empty hash is passed in as a positional argument instead of doing nothing. This causes an ArgumentError, which is confusing because when you splat an empty array into a method that doesn't accept any arguments the method is called without raising an error.
Similarly, if you splat a hash into a method that only has positional arguments, the method is called with the hash added as the last argument. This either causes an ArgumentError or unexpected bugs.
Examples¶
(tested in MRI 2.2.2 and 2.3.0)
def without_parameters
# some code
end
def with_parameters(*args)
args
end
def with_one_parameter(arg)
arg
end
empty_hash = {}
filled_hash = { example: "value" }
array = []
without_parameters(*array)
# calls the method without an error because `array' is empty
without_parameters(**empty_hash)
# unexpectedly raises an ArgumentError despite `empty_hash' being empty
with_parameters(**empty_hash)
# unexpectedly returns [{}] instead of []
with_parameters(**filled_hash)
# unexpectedly returns [{ example: "value }] instead of raising an ArgumentError
with_one_parameter(**empty_hash)
with_one_parameter(**filled_hash)
# both unexpectedly do not raise an ArgumentError
Further Information¶
This behavior makes it more difficult to do things like write specialized decorator classes using #method_missing. The following example does not work if the method being called does not have any named parameters. The variable named_args gets passed in as a positional argument, causing ArgumentErrors or unexpected bugs:
class TrivialDecoratorExample
def initialize(value)
@value = value
end
def method_missing(name, *args, **named_args, &block)
@value.send(name, *args, **named_args, &block)
end
end
Instead one has to write something really ugly like:
def method_missing(name, *args, **named_args, &block)
if @value.method(name)
.parameters
.any? { |type, _| [:keyreq, :key].include?(type) }
@value.send(name, *args, **named_args, &block)
elsif named_args.empty?
@value.send(name, *args, &block)
else
raise ArgumentError.new
end
end