Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 3619fb6

Browse files
authored
Merge pull request #366 from edzhelyov/recognize-hash-as-optional-argument
Recognize hash as optional arg when optional keyword is present
2 parents efb9add + d24df62 commit 3619fb6

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

lib/rspec/support/method_signature_verifier.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,19 @@ def invalid_kw_args_from(given_kw_args)
7777
given_kw_args - @allowed_kw_args
7878
end
7979

80+
# If the last argument is Hash, Ruby will treat only symbol keys as keyword arguments
81+
# the rest will be grouped in another Hash and passed as positional argument.
8082
def has_kw_args_in?(args)
81-
Hash === args.last && could_contain_kw_args?(args)
83+
Hash === args.last &&
84+
could_contain_kw_args?(args) &&
85+
args.last.keys.any? { |x| x.is_a?(Symbol) }
8286
end
8387

8488
# Without considering what the last arg is, could it
8589
# contain keyword arguments?
8690
def could_contain_kw_args?(args)
8791
return false if args.count <= min_non_kw_args
92+
8893
@allows_any_kw_args || @allowed_kw_args.any?
8994
end
9095

@@ -357,7 +362,14 @@ def unlimited_args?
357362

358363
def split_args(*args)
359364
kw_args = if @signature.has_kw_args_in?(args)
360-
args.pop.keys
365+
last = args.pop
366+
non_kw_args = last.reject { |k, _| k.is_a?(Symbol) }
367+
if non_kw_args.empty?
368+
last.keys
369+
else
370+
args << non_kw_args
371+
last.select { |k, _| k.is_a?(Symbol) }.keys
372+
end
361373
else
362374
[]
363375
end

spec/rspec/support/method_signature_verifier_spec.rb

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,102 @@ def arity_kw(x, y:1, z:2); end
366366
end
367367
end
368368

369+
if RubyFeatures.kw_args_supported?
370+
describe 'a method with optional argument and keyword arguments' do
371+
eval <<-RUBY
372+
def arity_kw(x, y = {}, z:2); end
373+
RUBY
374+
375+
let(:test_method) { method(:arity_kw) }
376+
377+
it 'does not require any of the arguments' do
378+
expect(valid?(nil)).to eq(true)
379+
expect(valid?(nil, nil)).to eq(true)
380+
end
381+
382+
it 'does not allow an invalid keyword arguments' do
383+
expect(valid?(nil, nil, :a => 1)).to eq(false)
384+
expect(valid?(nil, :a => 1)).to eq(false)
385+
end
386+
387+
it 'treats symbols as keyword arguments and the rest as optional argument' do
388+
expect(valid?(nil, 'a' => 1)).to eq(true)
389+
expect(valid?(nil, 'a' => 1, :z => 3)).to eq(true)
390+
expect(valid?(nil, 'a' => 1, :b => 3)).to eq(false)
391+
expect(valid?(nil, 'a' => 1, :b => 2, :z => 3)).to eq(false)
392+
end
393+
394+
it 'mentions the invalid keyword args in the error', :pending => RSpec::Support::Ruby.jruby? && !RSpec::Support::Ruby.jruby_9000? do
395+
expect(error_for(1, 2, :a => 0)).to eq("Invalid keyword arguments provided: a")
396+
expect(error_for(1, :a => 0)).to eq("Invalid keyword arguments provided: a")
397+
expect(error_for(1, 'a' => 0, :b => 0)).to eq("Invalid keyword arguments provided: b")
398+
end
399+
400+
it 'describes invalid arity precisely' do
401+
expect(error_for()).to \
402+
eq("Wrong number of arguments. Expected 1 to 2, got 0.")
403+
end
404+
405+
it 'does not blow up when given a BasicObject as the last arg' do
406+
expect(valid?(BasicObject.new)).to eq(true)
407+
end
408+
409+
it 'does not mutate the provided args array' do
410+
args = [nil, nil, { :y => 1 }]
411+
described_class.new(signature, args).valid?
412+
expect(args).to eq([nil, nil, { :y => 1 }])
413+
end
414+
415+
it 'mentions the arity and optional kw args in the description', :pending => RSpec::Support::Ruby.jruby? && !RSpec::Support::Ruby.jruby_9000? do
416+
expect(signature_description).to eq("arity of 1 to 2 and optional keyword args (:z)")
417+
end
418+
419+
it "indicates the optional keyword args" do
420+
expect(signature.optional_kw_args).to contain_exactly(:z)
421+
end
422+
423+
it "indicates it has no required keyword args" do
424+
expect(signature.required_kw_args).to eq([])
425+
end
426+
427+
describe 'with an expectation object' do
428+
it 'matches the exact arity' do
429+
expect(validate_expectation 0).to eq(false)
430+
expect(validate_expectation 1).to eq(true)
431+
expect(validate_expectation 2).to eq(true)
432+
end
433+
434+
it 'matches the exact range' do
435+
expect(validate_expectation 0, 1).to eq(false)
436+
expect(validate_expectation 1, 1).to eq(true)
437+
expect(validate_expectation 1, 2).to eq(true)
438+
expect(validate_expectation 1, 3).to eq(false)
439+
end
440+
441+
it 'does not match unlimited arguments' do
442+
expect(validate_expectation :unlimited_args).to eq(false)
443+
end
444+
445+
it 'matches optional keywords with the correct arity' do
446+
expect(validate_expectation :z).to eq(false)
447+
expect(validate_expectation 1, :z).to eq(true) # Are we OK with that?
448+
expect(validate_expectation 1, 2, :z).to eq(true)
449+
expect(validate_expectation 1, 2, :y).to eq(false)
450+
end
451+
452+
it 'does not match invalid keywords' do
453+
expect(validate_expectation :w).to eq(false)
454+
455+
expect(validate_expectation 2, :w).to eq(false)
456+
end
457+
458+
it 'does not match arbitrary keywords' do
459+
expect(validate_expectation :arbitrary_kw_args).to eq(false)
460+
end
461+
end
462+
end
463+
end
464+
369465
if RubyFeatures.required_kw_args_supported?
370466
describe 'a method with required keyword arguments' do
371467
eval <<-RUBY

0 commit comments

Comments
 (0)