diff --git a/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb b/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb index c9a0f309d..fb1b89cfe 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb @@ -327,7 +327,7 @@ def on_def_node_enter(node) node.location, node.name_loc, comments, - [Entry::Signature.new(list_params(node.parameters))], + [Entry::Signature.new(list_params(node.parameters), ["Object"])], current_visibility, @owner_stack.last, )) @@ -343,7 +343,7 @@ def on_def_node_enter(node) node.location, node.name_loc, comments, - [Entry::Signature.new(list_params(node.parameters))], + [Entry::Signature.new(list_params(node.parameters), ["Object"])], current_visibility, singleton, )) diff --git a/lib/ruby_indexer/lib/ruby_indexer/entry.rb b/lib/ruby_indexer/lib/ruby_indexer/entry.rb index 5bbf9e2b0..dbda979f5 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/entry.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/entry.rb @@ -366,7 +366,7 @@ def signatures begin params = [] params << RequiredParameter.new(name: name.delete_suffix("=").to_sym) if name.end_with?("=") - [Entry::Signature.new(params)] + [Entry::Signature.new(params, ["Object"])] end, T.nilable(T::Array[Signature]), ) @@ -576,9 +576,14 @@ class Signature sig { returns(T::Array[Parameter]) } attr_reader :parameters - sig { params(parameters: T::Array[Parameter]).void } - def initialize(parameters) + sig { returns(T::Array[String]) } + attr_reader :return_types + + sig { params(parameters: T::Array[Parameter], return_types: T::Array[String]).void } + def initialize(parameters, return_types) @parameters = parameters + # Return types should only be used to assist type inference, so we don't want to display them in signature help + @return_types = return_types end # Returns a string with the decorated names of the parameters of this member. E.g.: `(a, b = 1, c: 2)` diff --git a/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb b/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb index 89cca8642..edb85ecea 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb @@ -132,20 +132,42 @@ def handle_method(member, owner) sig { params(member: RBS::AST::Members::MethodDefinition).returns(T::Array[Entry::Signature]) } def signatures(member) member.overloads.map do |overload| - parameters = process_overload(overload) - Entry::Signature.new(parameters) - end - end + function = T.cast(overload.method_type.type, RBS::Types::Function) + parameters = parse_arguments(function) - sig { params(overload: RBS::AST::Members::MethodDefinition::Overload).returns(T::Array[Entry::Parameter]) } - def process_overload(overload) - function = T.cast(overload.method_type.type, RBS::Types::Function) - parameters = parse_arguments(function) + block = overload.method_type.block + parameters << Entry::BlockParameter.anonymous if block&.required - block = overload.method_type.block - parameters << Entry::BlockParameter.anonymous if block&.required + rbs_return_type = function.return_type - parameters + return_types = rbs_type_to_ruby_types(rbs_return_type) + + Entry::Signature.new(parameters, return_types) + end + end + + sig do + params(rbs_type: T.untyped).returns(T::Array[String]) + end + def rbs_type_to_ruby_types(rbs_type) + case rbs_type + when RBS::Types::Bases::Void, RBS::Types::Bases::Nil + ["NilClass"] + when RBS::Types::Bases::Self + ["self"] + when RBS::Types::Bases::Bool + ["TrueClass", "FalseClass"] + when RBS::Types::Optional + rbs_type_to_ruby_types(rbs_type.type) + when RBS::Types::Union, RBS::Types::Tuple, RBS::Types::Intersection + rbs_type.types.map { |type| rbs_type_to_ruby_types(type) }.flatten + else + if rbs_type.respond_to?(:name) + [rbs_type.name.name.to_s] + else + ["Object"] + end + end end sig { params(function: RBS::Types::Function).returns(T::Array[Entry::Parameter]) } diff --git a/lib/ruby_indexer/test/enhancements_test.rb b/lib/ruby_indexer/test/enhancements_test.rb index df33ff568..068dacd39 100644 --- a/lib/ruby_indexer/test/enhancements_test.rb +++ b/lib/ruby_indexer/test/enhancements_test.rb @@ -39,7 +39,7 @@ def on_call_node(index, owner, node, file_path) location, location, [], - [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])], + [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)], ["Object"])], Entry::Visibility::PUBLIC, owner, )) diff --git a/lib/ruby_lsp/type_inferrer.rb b/lib/ruby_lsp/type_inferrer.rb index b381b148c..03bec5dec 100644 --- a/lib/ruby_lsp/type_inferrer.rb +++ b/lib/ruby_lsp/type_inferrer.rb @@ -77,6 +77,30 @@ def infer_receiver_for_call_node(node, node_context) Type.new("Range") when Prism::LambdaNode Type.new("Proc") + when Prism::CallNode + method_name = receiver.message + return unless method_name + + inferred_receiver_type = infer_receiver_for_call_node(receiver, node_context) + + if inferred_receiver_type + methods = @index.resolve_method(method_name, inferred_receiver_type.name, inherited_only: false) + + if methods + method = methods.first + return unless method + + first_signature = method.signatures.first + return unless first_signature + + return_type = first_signature.return_types.first + return unless return_type + + Type.new(return_type) + else + infer_with_guessed_types(node, node_context) + end + end when Prism::ConstantPathNode, Prism::ConstantReadNode # When the receiver is a constant reference, we have to try to resolve it to figure out the right # receiver. But since the invocation is directly on the constant, that's the singleton context of that @@ -93,22 +117,27 @@ def infer_receiver_for_call_node(node, node_context) Type.new("#{parts.join("::")}::#{last}::") else - return unless @experimental_features - - raw_receiver = node.receiver&.slice - - if raw_receiver - guessed_name = raw_receiver - .delete_prefix("@") - .delete_prefix("@@") - .split("_") - .map(&:capitalize) - .join + infer_with_guessed_types(node, node_context) + end + end - entries = @index.resolve(guessed_name, node_context.nesting) || @index.first_unqualified_const(guessed_name) - name = entries&.first&.name - GuessedType.new(name) if name - end + sig { params(node: Prism::CallNode, node_context: NodeContext).returns(T.nilable(Type)) } + def infer_with_guessed_types(node, node_context) + return unless @experimental_features + + raw_receiver = node.receiver&.slice + + if raw_receiver + guessed_name = raw_receiver + .delete_prefix("@") + .delete_prefix("@@") + .split("_") + .map(&:capitalize) + .join + + entries = @index.resolve(guessed_name, node_context.nesting) || @index.first_unqualified_const(guessed_name) + name = entries&.first&.name + GuessedType.new(name) if name end end diff --git a/test/type_inferrer_test.rb b/test/type_inferrer_test.rb index 16b3d0cde..5a22fca7b 100644 --- a/test/type_inferrer_test.rb +++ b/test/type_inferrer_test.rb @@ -270,6 +270,21 @@ def test_infer_string_literal assert_equal("String", @type_inferrer.infer_receiver_type(node_context).name) end + def test_infer_chained_call_with_literal_receiver + RubyIndexer::RBSIndexer.new(@index).index_ruby_core + node_context = index_and_locate(<<~RUBY, { line: 0, character: 10 }) + "".upcase.downcase + RUBY + + assert_equal("String", @type_inferrer.infer_receiver_type(node_context).name) + + node_context = index_and_locate(<<~RUBY, { line: 0, character: 10 }) + "".count.to_s + RUBY + + assert_equal("Integer", @type_inferrer.infer_receiver_type(node_context).name) + end + def test_infer_symbol_literal node_context = index_and_locate(<<~RUBY, { line: 0, character: 5 }) :foo.to_s