kwargs are... complicated. Let me first extend the issue with additional versions of the above (I run Ruby 3.1, but from what I know, everything applies to anything >= Ruby 3.0):
[1] pry(main)> method(def a(a:, b:); end).arity
=> 1
[2] pry(main)> method(def a(a:, b: 5); end).arity
=> 1
[3] pry(main)> method(def a(a: 5, b: 5); end).arity
=> -1
[4] pry(main)> method(def a(**kwargs); end).arity
=> -1
[5] pry(main)> method(def a(**nil); end).arity
=> 0
[6] pry(main)>
So, basically, how I understand the kwargs: they are actually internally the last Hash argument of a function (that is passed with a special flag). If all kwargs are optional, then it's an optional argument (so arity is -1).
They are unlike how blocks work - it's not a separate category of arguments.
If a function is declared with kwargs arguments special syntax (later I will call it a kwargs-syntax function), then this flag is enforced while checking arity (since Ruby 3.0 I believe). But otherwise, it's not, and kwargs are just passed as a simple argument:
[8] pry(main)> def a(kwargs); kwargs; end; a(a: 5)
=> {:a=>5}
[9] pry(main)> def a(kwargs = {}); kwargs; end; a(a: 5)
=> {:a=>5}
[10] pry(main)> def a(*args); args; end; a(a: 5)
=> [{:a=>5}]
[11] pry(main)>
It is possible to call a kwargs-syntax function with a non-kwargs Hash argument (but only with restarg syntax), if we set a flag (a distinct one, from what I understand) using Hash.ruby2_keywords_hash.
[15] pry(main)> def a(**kwargs); kwargs; end; a(*[Hash.ruby2_keywords_hash({a: 5})])
=> {:a=>5}
[16] pry(main)>
So, in a way, the first list of examples would be equivalent in terms of arity to the following set of examples:
[1] pry(main)> method(def a(kwargs); end).arity
=> 1
[2] pry(main)> method(def a(kwargs); end).arity
=> 1
[3] pry(main)> method(def a(kwargs = {}); end).arity
=> -1
[4] pry(main)> method(def a(kwargs = {}); end).arity
=> -1
[5] pry(main)> method(def a(); end).arity
=> 0
[6] pry(main)>
I think a better interface to get the argument signature of a function is Method#parameters
:
[18] pry(main)> method(def a(kwargs); end).parameters
=> [[:req, :kwargs]]
[19] pry(main)> method(def a(**kwargs); end).parameters
=> [[:keyrest, :kwargs]]
[20] pry(main)> method(def a(a:, b:); end).parameters
=> [[:keyreq, :a], [:keyreq, :b]]
[21] pry(main)> method(def a(a: 5, b: 7); end).parameters
=> [[:key, :a], [:key, :b]]
[22] pry(main)> method(def a(kwargs = {}); end).parameters
=> [[:opt, :kwargs]]
[23] pry(main)> method(def a(**nil); end).parameters
=> [[:nokey]]
[24] pry(main)>
(My understanding on this topic stems from a fact, that I am working on updating kwargs support in Opal to match MRI 3.x behavior)