Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/solargraph/api_map/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 34 additions & 11 deletions lib/solargraph/type_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer>` 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<Pin::LocalVariable>]
# @param closure_pin [Pin::Closure]
Expand All @@ -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<Integer>` 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<Integer>` receiver),
# for both fixed-arity params (below, and in #kwarg_problems_for)
# and restargs (see #restarg_problems_for).
#
# @return [Array<Problem>]
def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin, receiver_type
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<Problem>]
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]
Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion solargraph.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
27 changes: 27 additions & 0 deletions spec/api_map/constants_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions spec/type_checker/levels/strict_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>]
Expand Down
Loading