diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 26b42ddb4..004f59fbb 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -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].include?(rooted_tag) + if %w[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' + pin.proxy_with_signatures ComplexType.try_parse(return_tag) + end + else + if scope == :class && rooted_type.name == ComplexType::GENERIC_TAG_NAME + # A Class> 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 diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index b30002967..f5b2fc897 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -822,7 +822,7 @@ 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 @@ -830,17 +830,59 @@ def self.new 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>#new' do + source = Solargraph::Source.load_string(%( + # @generic T + # @param clazz [Class>] + # @return [generic] + 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') + end + + it 'binds the generic at call sites of a Class>#new factory' do + source = Solargraph::Source.load_string(%( + # @generic T + # @param clazz [Class>] + # @return [generic] + 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 diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 1043a192d..00b1c017b 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -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>#new' do + checker = type_checker(%( + # @generic T + # @param clazz [Class>] + # @return [generic] + def create_object(clazz) + clazz.new + end + )) + expect(checker.problems).to be_empty + end + + it 'accepts arguments through Class>#new' do + checker = type_checker(%( + # @generic T + # @param clazz [Class>] + # @param opts [Hash{Symbol => Object}] + # @return [generic] + def create_object_with_args(clazz, opts) + clazz.new(:sym, **opts) + end + )) + expect(checker.problems).to be_empty + end end end