diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 6c73509c3..880adacb6 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -155,7 +155,7 @@ def complex_resolve name, gates, internal mixin = resolve(ref.name, ref.reference_gates) next unless mixin - resolved = simple_resolve(name, mixin, internal) + resolved = resolve(name, mixin) return [resolved, gates[(idx + 1)..]] if resolved end end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index be734f14a..fa237ef65 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -426,6 +426,26 @@ def argument_problems_for chain, api_map, closure_pin, locals, location result end + # Resolves any remaining generics in a fixed-arity parameter's + # already-qualified declared type (e.g. `Elem` for a made-up + # `Array#insert_first(v: Elem): void`) against the receiver's + # actual generic parameters (e.g. `Integer` for an + # `Array` receiver). This is the fixed-arity counterpart + # to the unwrap-and-resolve done in #restarg_problems_for for + # restargs. + # + # @param ptype [ComplexType, ComplexType::UniqueType] the + # parameter's declared type, as found via + # #signature_param_details + # @param pin [Pin::Method] + # @param receiver_type [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] + def resolve_param_type_against_receiver ptype, pin, receiver_type + return ptype if ptype.nil? || ptype.undefined? + # @sg-ignore pin.closure is a Pin::Namespace for a top-level method pin + ptype.resolve_generics(pin.closure, receiver_type) + end + # @param location [Location] # @param locals [Array] # @param closure_pin [Pin::Closure] @@ -434,15 +454,11 @@ def argument_problems_for chain, api_map, closure_pin, locals, location # @param sig [Pin::Signature] # @param pin [Pin::Method] # @param receiver_type [ComplexType] the type of the object the - # method is being called on. Resolving a signature's generics - # (e.g. `Elem`) against the receiver's actual generic - # parameters (e.g. `Integer` for an `Array` receiver) - # is a general problem, but this is currently only plumbed - # through to the restarg path below (see #restarg_problems_for) - # - fixed-arity params still get their types from `params` - # (built by #param_details_from_stack), which doesn't resolve - # against the receiver. Generalizing that is tracked as a - # follow-up, not attempted here. + # method is being called on. Used to resolve a signature's + # generics (e.g. `Elem`) against the receiver's actual generic + # parameters (e.g. `Integer` for an `Array` receiver), + # for both fixed-arity params (below, and in #kwarg_problems_for) + # and restargs (see #restarg_problems_for). # # @return [Array] def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin, receiver_type @@ -495,6 +511,7 @@ def signature_argument_problems_for location, locals, closure_pin, params, argum return errors end ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED + ptype = resolve_param_type_against_receiver(ptype, pin, receiver_type) ptype = ptype.self_to_type(par.context) if ptype.nil? # @todo Some level (strong, I guess) should require the param here @@ -508,7 +525,8 @@ def signature_argument_problems_for location, locals, closure_pin, params, argum end end else - errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx + errors.concat kwarg_problems_for(sig, argchain, api_map, closure_pin, locals, location, pin, params, idx, + receiver_type) next end elsif par.decl == :kwarg @@ -613,9 +631,13 @@ def restarg_arguments sig, arguments, idx # @param pin [Pin::Method] # @param params [Hash{String => Hash{Symbol => undefined}}] # @param idx [Integer] + # @param receiver_type [ComplexType] the type of the object the + # method is being called on, used to resolve a generic keyword + # parameter's declared type (e.g. `Elem`) against the + # receiver's actual generic parameters # # @return [Array] - def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx + def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx, receiver_type result = [] kwargs = convert_hash(argchain.node) par = sig.parameters[idx] @@ -630,6 +652,7 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi else # @type [ComplexType, ComplexType::UniqueType] ptype = data[:qualified] + ptype = resolve_param_type_against_receiver(ptype, pin, receiver_type) ptype = ptype.self_to_type(pin.context) unless ptype.undefined? # @type [ComplexType] diff --git a/solargraph.gemspec b/solargraph.gemspec index 9af2205c5..67e9d1294 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -65,7 +65,7 @@ Gem::Specification.new do |s| # # even more specific on RuboCop itself, which is written into _todo # file. - s.add_development_dependency 'overcommit', '~> 0.68.0' + s.add_development_dependency 'overcommit', '~> 0.71.0' s.add_development_dependency 'rubocop', '~> 1.80.0.0' s.add_development_dependency 'rubocop-rake', '~> 0.7.1' s.add_development_dependency 'rubocop-rspec', '~> 3.6.0' diff --git a/spec/api_map/constants_spec.rb b/spec/api_map/constants_spec.rb index 833a928cf..c07cd4a64 100644 --- a/spec/api_map/constants_spec.rb +++ b/spec/api_map/constants_spec.rb @@ -94,6 +94,33 @@ module Baz; end resolved = constants.resolve('Foo::Bar::Baz') expect(resolved).to eq('Foo::Bar::Baz') end + + it 'resolves remote constants' do + source_map = Solargraph::SourceMap.load_string(%( + module Module1 + class Foo + end + end + + module Module2 + include Module1 + Thing = Foo + end + + module Module3 + include Module2 + end + + module Module4 + include Module3 + x = Thing.new + end + ), 'test.rb') + store = Solargraph::ApiMap::Store.new(source_map.pins) + constants = described_class.new(store) + constant = constants.resolve('Thing', 'Module4') + expect(constant).to eq('Module2::Thing') + end end describe '#dereference' do diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 24dbe9440..116407fe6 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -73,6 +73,29 @@ def foo str; end .to contain_exactly(a_string_matching(/\AWrong argument type for Array#push: \w+ expected Integer, received String\z/)) end + it 'catches a bad #<< argument against a fixed-arity generic parameter resolved against the receiver (#1242)' do + # Array#<< has a single fixed-arity parameter typed as the + # generic `E`. Before this fix, only restarg parameters (like + # Array#push's, above) had their generic types resolved against + # the receiver's actual element type - fixed-arity params like + # this one still typed themselves from the unresolved generic + # declaration and never flagged a mismatch. + checker = type_checker(%( + y = [1] + y << 'two' + )) + expect(checker.problems.map(&:message)) + .to contain_exactly(a_string_matching(/\AWrong argument type for Array#<<: \w+ expected Integer, received String\z/)) + end + + it 'does not flag a #<< argument matching the receiver element type (#1242)' do + checker = type_checker(%( + y = [1] + y << 5 + )) + expect(checker.problems.map(&:message)).to eq([]) + end + it 'handles compatible interfaces with self types on call' do checker = type_checker(%( # @param a [Enumerable]