diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index cbc69c804..7f1c09e1a 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.6', '2.7', '3.0', '3.1'] + ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2'] steps: - uses: actions/checkout@v3 diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 059caaeff..ef6d663de 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -254,6 +254,11 @@ def call_problems result end + # @param chain [Solargraph::Source::Chain] + # @param api_map [Solargraph::ApiMap] + # @param block_pin [Solargraph::Pin::Base] + # @param locals [Array] + # @param location [Solargraph::Location] def argument_problems_for chain, api_map, block_pin, locals, location result = [] base = chain @@ -279,9 +284,14 @@ def argument_problems_for chain, api_map, block_pin, locals, location errors = [] sig.parameters.each_with_index do |par, idx| argchain = base.links.last.arguments[idx] - if argchain.nil? && par.decl == :arg - errors.push Problem.new(location, "Not enough arguments to #{pin.path}") - next + if argchain.nil? + if par.decl == :arg + errors.push Problem.new(location, "Not enough arguments to #{pin.path}") + next + else + last = base.links.last.arguments.last + argchain = last if last && [:kwsplat, :HASH].include?(last.node.type) + end end if argchain if par.decl != :arg @@ -317,30 +327,29 @@ def argument_problems_for chain, api_map, block_pin, locals, location result end - def kwarg_problems_for argchain, api_map, block_pin, locals, location, pin, params, first + def kwarg_problems_for argchain, api_map, block_pin, locals, location, pin, params, idx result = [] kwargs = convert_hash(argchain.node) - pin.signatures.first.parameters[first..-1].each_with_index do |par, cur| - idx = first + cur - argchain = kwargs[par.name.to_sym] - if par.decl == :kwrestarg || (par.decl == :optarg && idx == pin.parameters.length - 1 && par.asgn_code == '{}') - result.concat kwrestarg_problems_for(api_map, block_pin, locals, location, pin, params, kwargs) - else - if argchain - data = params[par.name] - if data.nil? - # @todo Some level (strong, I guess) should require the param here - else - ptype = data[:qualified] - next if ptype.undefined? + par = pin.signatures.first.parameters[idx] + argchain = kwargs[par.name.to_sym] + if par.decl == :kwrestarg || (par.decl == :optarg && idx == pin.parameters.length - 1 && par.asgn_code == '{}') + result.concat kwrestarg_problems_for(api_map, block_pin, locals, location, pin, params, kwargs) + else + if argchain + data = params[par.name] + if data.nil? + # @todo Some level (strong, I guess) should require the param here + else + ptype = data[:qualified] + unless ptype.undefined? argtype = argchain.infer(api_map, block_pin, locals) if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") end end - elsif par.decl == :kwarg - result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") end + elsif par.decl == :kwarg + result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") end end result diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 22bbd3631..c3ef2301f 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -376,6 +376,53 @@ def bar(baz) expect(checker.problems.first.message).to include('Not enough arguments') end + it 'reports solo missing kwarg' do + checker = type_checker(%( + class Foo + def bar(baz:) + end + end + Foo.new.bar + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('Missing keyword arguments') + end + + it 'reports not enough kwargs' do + checker = type_checker(%( + class Foo + def bar(foo:, baz:) + end + end + Foo.new.bar(foo: 100) + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('Missing keyword argument') + expect(checker.problems.first.message).to include('baz') + end + + it 'accepts passed kwargs' do + checker = type_checker(%( + class Foo + def bar(baz:) + end + end + Foo.new.bar(baz: 123) + )) + expect(checker.problems).to be_empty + end + + it 'accepts multiple passed kwargs' do + checker = type_checker(%( + class Foo + def bar(baz:, bing:) + end + end + Foo.new.bar(baz: 123, bing: 456) + )) + expect(checker.problems).to be_empty + end + it 'requires strict return tags' do checker = type_checker(%( class Foo