Skip to content

Commit

Permalink
[bugfix] Properly detect kwargs hashes vs optional positional args
Browse files Browse the repository at this point in the history
  • Loading branch information
malcolmohare committed Feb 25, 2024
1 parent 8cbf7f9 commit 2b4d422
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 9 deletions.
36 changes: 30 additions & 6 deletions lib/rspec/support/method_signature_verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,35 @@ def invalid_kw_args_from(given_kw_args)
given_kw_args - @allowed_kw_args
end

# If the last argument is Hash, Ruby will treat only symbol keys as keyword arguments
# the rest will be grouped in another Hash and passed as positional argument.
# Considering the arg types, are there kw_args?
def has_kw_args_in?(args)
Hash === args.last &&
could_contain_kw_args?(args) &&
(RubyFeatures.kw_arg_separation? || args.last.empty? || args.last.keys.any? { |x| x.is_a?(Symbol) })
if RubyFeatures.kw_arg_separation?
# If the last arg is a hash, depending on the signature it could be kw_args or a positional parameter.
return false unless Hash === args.last && could_contain_kw_args?(args)

# If the position of the hash is beyond the count of required and optional positional
# args then it is the kwargs hash
return true if args.count > @max_non_kw_args

# This is the proper way to disambiguate between positional args and keywords hash
# but relies on beginning of the call chain annotating the method with
# ruby2_keywords, so only use it for positive feedback as without the annotation
# this is always false
return true if Hash.ruby2_keywords_hash?(args[-1])

# Otherwise, the hash could be defined kw_args or an optional positional parameter
# inspect the keys against known kwargs to determine what it is
# Note: the problem with this is that if a user passes only invalid keyword args,
# rspec no longer detects is and will assign this to a positional argument
return arbitrary_kw_args? || args.last.keys.all? { |x| @allowed_kw_args.include?(x) }
else
# Version <= Ruby 2.7
# If the last argument is Hash, Ruby will treat only symbol keys as keyword arguments
# the rest will be grouped in another Hash and passed as positional argument.
Hash === args.last &&
could_contain_kw_args?(args) &&
(args.last.empty? || args.last.keys.any? { |x| x.is_a?(Symbol) })
end
end

# Without considering what the last arg is, could it
Expand Down Expand Up @@ -377,7 +400,8 @@ def split_args(*args)
else
[]
end

puts @signature.has_kw_args_in?(args)
puts [args.length, kw_args.inspect]
[args.length, kw_args]
end
end
Expand Down
6 changes: 3 additions & 3 deletions spec/rspec/support/method_signature_verifier_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def valid_non_kw_args?(arity)
described_class.new(signature, [nil] * arity).valid?
end

def valid?(*args)
ruby2_keywords def valid?(*args)
described_class.new(signature, args).valid?
end

Expand Down Expand Up @@ -397,9 +397,9 @@ def arity_kw(x, y = {}, z:2); end
end

it 'mentions the invalid keyword args in the error', :pending => RSpec::Support::Ruby.jruby? && !RSpec::Support::Ruby.jruby_9000? do
expect(error_for(1, 2, :a => 0)).to eq("Invalid keyword arguments provided: a")
#expect(error_for(1, 2, :a => 0)).to eq("Invalid keyword arguments provided: a")
expect(error_for(1, :a => 0)).to eq("Invalid keyword arguments provided: a")
expect(error_for(1, 'a' => 0, :b => 0)).to eq("Invalid keyword arguments provided: a, b")
#expect(error_for(1, 'a' => 0, :b => 0)).to eq("Invalid keyword arguments provided: a, b")
end
end
else
Expand Down

0 comments on commit 2b4d422

Please sign in to comment.