Rishi
Rishi

Reputation: 11

Ruby 2.6.3 - 'warn' a hash with a symbol as key produces ArgumentError

Using Ruby 2.6.3, I'm coming across a bug where an ArgumentError is thrown when I try to 'warn' a hash that has at least one key as a symbol. If I try to 'puts' the same hash, there is no error and the hash is printed normally.

 >> h = {:one => 1}
  => {:one=>1}

 >> puts h
 {:one=>1}
  => nil

 >> warn h
  Traceback (most recent call last):
    5: from /Users/rishi/.rvm/rubies/ruby-2.6.3/bin/irb:23:in `<main>'
    4: from /Users/rishi/.rvm/rubies/ruby-2.6.3/bin/irb:23:in `load'
    3: from /Users/rishi/.rvm/rubies/ruby-2.6.3/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
    2: from (irb):48
    1: from /Users/rishi/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_warn.rb:15:in `block in <module:Kernel>' 
  ArgumentError (unknown keyword: one) 

It seems to be throwing the error at line 15 of my kernel_warn.rb:

module_function define_method(:warn) {|*messages, uplevel: nil|
   ...

Can anyone advise on how to fix?

Upvotes: 1

Views: 127

Answers (1)

Amadan
Amadan

Reputation: 198368

Since like forever, Ruby allowed to drop the curly braces around hash-as-last-argument in method calls to allow for keyword-like arguments. This allowed Rails to do e.g.

link_to "Delete", @item, method: "delete"

which is interpreted as

link_to("Delete", @item, { method: "delete" })

Then Ruby got support for keyword arguments, but they had to be hacked in, in a way that will still make all the old code running. So keyword arguments are also treated as if they were this argument-final hash. The problem for you is this: if there are keyword arguments in the signature, then the keyword arguments are strictly checked against the signature; you can't have extra keyword arguments, just like you can't have extra positional arguments. Remember, these are all equivalent:

warn h
warn({ one: 1 })
warn(one: 1)

Ruby sees a positional argument in the signature, and takes a look if the last argument that was passed is a hash. It is, so it assumes it's the keyword argument hash that it needs to compare to the signature - and one: is not a valid keyword.

What to do? Make sure the hash is not the last argument. At a trivial level, you can do this:

warn h, "was my hash"
# {:one=>1}
# was my hash

The other thing you can do is to remember that warn wants a string; if it doesn't get one, it will stringify the arguments. So you can do it preemptively:

warn h.to_s
# {:one=>1}

tl;dr: Ruby keyword arguments are weird.

Upvotes: 2

Related Questions