Skip to content
Open
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/ruby_lsp/global_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def initialize
@index = RubyIndexer::Index.new #: RubyIndexer::Index
@graph = Rubydex::Graph.new #: Rubydex::Graph
@supported_formatters = {} #: Hash[String, Requests::Support::Formatter]
@type_inferrer = TypeInferrer.new(@index) #: TypeInferrer
@type_inferrer = TypeInferrer.new(@graph) #: TypeInferrer
@addon_settings = {} #: Hash[String, untyped]
@top_level_bundle = begin
Bundler.with_original_env { Bundler.default_gemfile }
Expand Down
38 changes: 20 additions & 18 deletions lib/ruby_lsp/type_inferrer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ module RubyLsp
# A minimalistic type checker to try to resolve types that can be inferred without requiring a type system or
# annotations
class TypeInferrer
#: (RubyIndexer::Index index) -> void
def initialize(index)
@index = index
#: (Rubydex::Graph) -> void
def initialize(graph)
@graph = graph
end

#: (NodeContext node_context) -> Type?
Expand Down Expand Up @@ -81,11 +81,10 @@ def infer_receiver_for_call_node(node, node_context)
receiver_name = RubyIndexer::Index.constant_name(receiver)
return unless receiver_name

resolved_receiver = @index.resolve(receiver_name, node_context.nesting)
name = resolved_receiver&.first&.name
return unless name
resolved_receiver = @graph.resolve_constant(receiver_name, node_context.nesting)
return unless resolved_receiver

*parts, last = name.split("::")
*parts, last = resolved_receiver.name.split("::")
return Type.new("#{last}::<#{last}>") if parts.empty?

Type.new("#{parts.join("::")}::#{last}::<#{last}>")
Expand All @@ -96,12 +95,14 @@ def infer_receiver_for_call_node(node, node_context)
# When invoking `new`, we recursively infer the type of the receiver to get the class type its being invoked
# on and then return the attached version of that type, since it's being instantiated.
type = infer_receiver_for_call_node(receiver, node_context)

return unless type

# If the method `new` was overridden, then we cannot assume that it will return a new instance of the class
new_method = @index.resolve_method("new", type.name)&.first
return if new_method && new_method.owner&.name != "Class"
declaration = @graph[type.name] #: as Rubydex::Namespace?
return unless declaration

new_method = declaration.find_member("new()")
return if new_method && new_method.owner.name != "Class"

type.attached
elsif raw_receiver
Expand All @@ -121,11 +122,11 @@ def guess_type(raw_receiver, nesting)
.map(&:capitalize)
.join

entries = @index.resolve(guessed_name, nesting) || @index.first_unqualified_const(guessed_name)
name = entries&.first&.name
return unless name
declaration = @graph.resolve_constant(guessed_name, nesting)
declaration ||= @graph.search(guessed_name).first
return unless declaration
Comment on lines -124 to +127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice different semantics in the APIs here. I am not very familiar with this part of the code, but I am assuming the intent is to have a best effort guess at what the type is as at this point we were not able to resolve it.

The question I have is, will @index.first_unqualified_const(guessed_name) and @graph.search(guessed_name).first return the same result, or is the heuristic different?


GuessedType.new(name)
GuessedType.new(declaration.name)
end

#: (NodeContext node_context) -> Type
Expand All @@ -148,7 +149,6 @@ def self_receiver_handling(node_context)
#: (NodeContext node_context) -> Type?
def infer_receiver_for_class_variables(node_context)
nesting_parts = node_context.nesting.dup

return Type.new("Object") if nesting_parts.empty?

nesting_parts.reverse_each do |part|
Expand All @@ -157,9 +157,11 @@ def infer_receiver_for_class_variables(node_context)
nesting_parts.pop
end

receiver_name = nesting_parts.join("::")
resolved_receiver = @index.resolve(receiver_name, node_context.nesting)&.first
return unless resolved_receiver&.name
resolved_receiver = @graph.resolve_constant(
nesting_parts.last, #: as !nil
nesting_parts[0...-1], #: as !nil
)
return unless resolved_receiver

Type.new(resolved_receiver.name)
end
Expand Down
2 changes: 2 additions & 0 deletions test/requests/hover_expectations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,8 @@ def baz
end

def test_hover_for_methods_shows_overload_count
skip("[RUBYDEX] Temporarily skipped because we don't yet index RBS methods")

source = <<~RUBY
String.try_convert
RUBY
Expand Down
8 changes: 5 additions & 3 deletions test/type_inferrer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
module RubyLsp
class TypeInferrerTest < Minitest::Test
def setup
@index = RubyIndexer::Index.new
@type_inferrer = TypeInferrer.new(@index)
@graph = Rubydex::Graph.new
@type_inferrer = TypeInferrer.new(@graph)
end

def test_infer_receiver_type_self_inside_method
Expand Down Expand Up @@ -499,7 +499,9 @@ class Foo
private

def index_and_locate(source, position)
@index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), source)
@graph.index_source(URI::Generic.from_path(path: "/fake/path/foo.rb").to_s, source, "ruby")
@graph.resolve

document = RubyLsp::RubyDocument.new(
source: source,
version: 1,
Expand Down
Loading