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
36 changes: 35 additions & 1 deletion lib/solargraph/api_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,41 @@ def get_methods rooted_tag, scope: :instance, visibility: [:public], deep: true
result.concat inner_get_methods('Kernel', :instance, visibility, deep, skip)
else
result.concat inner_get_methods(rooted_tag, scope, visibility, deep, skip)
unless %w[Class Class<Class>].include?(rooted_tag)
if %w[Class Class<Class>].include?(rooted_tag)
# RBS declares Class#new as untyped because it cannot
# parameterize Class. We know more: Class.new returns a new
# class whose instances are (at least) Objects, and calling
# .new on an unparameterized Class-typed receiver returns
# some instance of it - an Object.
result.map! do |pin|
next pin unless pin.path == 'Class#new'

return_tag = scope == :class ? 'Class<Object>' : 'Object'
pin.proxy_with_signatures ComplexType.try_parse(return_tag)
end
else
if scope == :class && rooted_type.name == ComplexType::GENERIC_TAG_NAME
# A Class<generic<T>> receiver: the namespace lookup on the
# unresolved generic finds no singleton methods (there is no
# 'generic' namespace), so no Class#new pin ever appears and
# .new goes undefined. The instance type is knowable, though -
# it is the generic itself, which the caller binds. Synthesize
# a permissive new pin that keeps it symbolic; argument
# checking stays lenient because the real initializer is
# unknown here.
generic_new = Pin::Method.new(
name: 'new',
scope: :class,
return_type: rooted_type,
closure: Pin::Namespace.new(name: 'Class', source: :api_map),
source: :api_map
)
generic_new.parameters = [
Pin::Parameter.new(decl: :restarg, name: 'args', closure: generic_new, source: :api_map),
Pin::Parameter.new(decl: :kwrestarg, name: 'kwargs', closure: generic_new, source: :api_map)
].freeze
result.push generic_new
end
result.map! do |pin|
next pin unless pin.path == 'Class#new'
init_pin = get_method_stack(rooted_tag, 'initialize').first
Expand Down
50 changes: 46 additions & 4 deletions spec/source_map/clip_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -822,25 +822,67 @@ def self.new
expect(clip.infer.tag).to eq('Class')
end

it 'infers undefined from Class#new' do
it 'infers Object from .new on an unparameterized Class' do
source = Solargraph::Source.load_string(%(
cls = Class.new
cls.new
), 'test.rb')
api_map = Solargraph::ApiMap.new
api_map.map source
clip = api_map.clip_at('test.rb', [2, 11])
expect(clip.infer.tag).to eq('undefined')
expect(clip.infer.tag).to eq('Object')
end

it 'infers undefined from Class.new.new' do
it 'infers Object from Class.new.new' do
source = Solargraph::Source.load_string(%(
Class.new.new
), 'test.rb')
api_map = Solargraph::ApiMap.new
api_map.map source
clip = api_map.clip_at('test.rb', [1, 17])
expect(clip.infer.tag).to eq('undefined')
expect(clip.infer.tag).to eq('Object')
end

it 'still infers Class for a variable assigned from Class.new' do
source = Solargraph::Source.load_string(%(
cls = Class.new
cls
), 'test.rb')
api_map = Solargraph::ApiMap.new
api_map.map source
clip = api_map.clip_at('test.rb', [2, 7])
expect(clip.infer.tag).to eq('Class')
end

it 'infers the symbolic generic from Class<generic<T>>#new' do
source = Solargraph::Source.load_string(%(
# @generic T
# @param clazz [Class<generic<T>>]
# @return [generic<T>]
def create_object(clazz)
clazz.new
end
), 'test.rb')
api_map = Solargraph::ApiMap.new
api_map.map source
clip = api_map.clip_at('test.rb', [5, 15])
expect(clip.infer.tag).to eq('generic<T>')
end

it 'binds the generic at call sites of a Class<generic<T>>#new factory' do
source = Solargraph::Source.load_string(%(
# @generic T
# @param clazz [Class<generic<T>>]
# @return [generic<T>]
def create_object(clazz)
clazz.new
end
create_object(String)
), 'test.rb')
api_map = Solargraph::ApiMap.new
api_map.map source
clip = api_map.clip_at('test.rb', [7, 8])
expect(clip.infer.tag).to eq('String')
end

it 'completes class instance variables in the namespace' do
Expand Down
41 changes: 41 additions & 0 deletions spec/type_checker/levels/strong_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -925,5 +925,46 @@ def baz(bases)
# an error when trying to declare sub as Subclass
expect(checker.problems.map(&:message)).not_to include('Unresolved call to bar on Base')
end

it 'infers a return through .new on an unparameterized Class' do
checker = type_checker(%(
# @return [Object]
def plain_class_new
Class.new.new
end

# @return [Object]
def local_class_new
k = Class.new
k.new
end
))
expect(checker.problems).to be_empty
end

it 'infers a generic return through Class<generic<T>>#new' do
checker = type_checker(%(
# @generic T
# @param clazz [Class<generic<T>>]
# @return [generic<T>]
def create_object(clazz)
clazz.new
end
))
expect(checker.problems).to be_empty
end

it 'accepts arguments through Class<generic<T>>#new' do
checker = type_checker(%(
# @generic T
# @param clazz [Class<generic<T>>]
# @param opts [Hash{Symbol => Object}]
# @return [generic<T>]
def create_object_with_args(clazz, opts)
clazz.new(:sym, **opts)
end
))
expect(checker.problems).to be_empty
end
end
end
Loading