Skip to content

Commit f82fccc

Browse files
authored
Infer types for method calls on immediately instantiated objects (#3007)
### Motivation Closes #2994 This PR adds inference for methods invoked directly on an object instantiation. It's fairly easy to add, so I think it's worth getting it done. ### Implementation When the receiver of a method call is another method call using `new`, we infer the type of the constant receiver and then return the attached version of it. For example: ```ruby # In this case, `Foo::<Class:Foo>` is the receiver of `new`, but new will return an # object. Thus the right type is the attached class, which is `Foo` Foo.new.something ``` The only exception is if the method `new` was overridden, which is valid in Ruby. We cannot guarantee that the override actually returns a new object of the class, so it's better to not infer anything in those cases. ### Automated Tests Added tests.
1 parent e40f625 commit f82fccc

File tree

3 files changed

+72
-17
lines changed

3 files changed

+72
-17
lines changed

jekyll/index.markdown

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ It also allows developers to discover constants or methods that are available to
169169
Sorry, your browser doesn't support embedded videos. This video illustrates the completion feature, providing completion candidates as the user types.
170170
</video>
171171

172+
{: .note }
173+
Completion for method calls can only be provided when the type of the receiver is known. For example, when typing `foo.`
174+
it's only possible to show method completion candidates if know the type of `foo`. Since the Ruby LSP does not require
175+
users to adopt a type system, completion for methods ends up being available only when types can be determined even
176+
without annotations (e.g.: methods invoked on literals, constants, direct instantiations of objects using `new`).<br><br>
177+
If you would like to have more accurate completion, consider adopting a
178+
[type system](design-and-roadmap#accuracy-correctness-and-type-checking).
179+
172180
### Signature Help
173181

174182
Signature help often appears right after users finish typing a method, providing hints about the method's parameters. This feature is invaluable for understanding the expected arguments and improving code accuracy.

lib/ruby_lsp/type_inferrer.rb

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -91,29 +91,45 @@ def infer_receiver_for_call_node(node, node_context)
9191
return Type.new("#{last}::<Class:#{last}>") if parts.empty?
9292

9393
Type.new("#{parts.join("::")}::#{last}::<Class:#{last}>")
94-
else
94+
when Prism::CallNode
95+
raw_receiver = receiver.message
9596

96-
raw_receiver = if receiver.is_a?(Prism::CallNode)
97-
receiver.message
98-
else
99-
receiver.slice
100-
end
97+
if raw_receiver == "new"
98+
# When invoking `new`, we recursively infer the type of the receiver to get the class type its being invoked
99+
# on and then return the attached version of that type, since it's being instantiated.
100+
type = infer_receiver_for_call_node(receiver, node_context)
101101

102-
if raw_receiver
103-
guessed_name = raw_receiver
104-
.delete_prefix("@")
105-
.delete_prefix("@@")
106-
.split("_")
107-
.map(&:capitalize)
108-
.join
109-
110-
entries = @index.resolve(guessed_name, node_context.nesting) || @index.first_unqualified_const(guessed_name)
111-
name = entries&.first&.name
112-
GuessedType.new(name) if name
102+
return unless type
103+
104+
# If the method `new` was overridden, then we cannot assume that it will return a new instance of the class
105+
new_method = @index.resolve_method("new", type.name)&.first
106+
return if new_method && new_method.owner&.name != "Class"
107+
108+
type.attached
109+
elsif raw_receiver
110+
guess_type(raw_receiver, node_context.nesting)
113111
end
112+
else
113+
guess_type(receiver.slice, node_context.nesting)
114114
end
115115
end
116116

117+
sig { params(raw_receiver: String, nesting: T::Array[String]).returns(T.nilable(GuessedType)) }
118+
def guess_type(raw_receiver, nesting)
119+
guessed_name = raw_receiver
120+
.delete_prefix("@")
121+
.delete_prefix("@@")
122+
.split("_")
123+
.map(&:capitalize)
124+
.join
125+
126+
entries = @index.resolve(guessed_name, nesting) || @index.first_unqualified_const(guessed_name)
127+
name = entries&.first&.name
128+
return unless name
129+
130+
GuessedType.new(name)
131+
end
132+
117133
sig { params(node_context: NodeContext).returns(Type) }
118134
def self_receiver_handling(node_context)
119135
nesting = node_context.nesting
@@ -176,6 +192,12 @@ class Type
176192
def initialize(name)
177193
@name = name
178194
end
195+
196+
# Returns the attached version of this type by removing the `<Class:...>` part from its name
197+
sig { returns(Type) }
198+
def attached
199+
Type.new(T.must(@name.split("::")[..-2]).join("::"))
200+
end
179201
end
180202

181203
# A type that was guessed based on the receiver raw name

test/type_inferrer_test.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,31 @@ def test_infer_top_level_class_variables
471471
assert_equal("Object", @type_inferrer.infer_receiver_type(node_context).name)
472472
end
473473

474+
def test_infer_object_instantiation_receiver
475+
node_context = index_and_locate(<<~RUBY, { line: 1, character: 8 })
476+
class Foo; end
477+
Foo.new.bar
478+
RUBY
479+
480+
assert_equal("Foo", @type_inferrer.infer_receiver_type(node_context).name)
481+
end
482+
483+
def test_infer_object_instantiation_receiver_is_ignored_if_new_is_overridden
484+
node_context = index_and_locate(<<~RUBY, { line: 8, character: 8 })
485+
module Bar
486+
def new; end
487+
end
488+
489+
class Foo
490+
extend Bar
491+
end
492+
493+
Foo.new.bar
494+
RUBY
495+
496+
assert_nil(@type_inferrer.infer_receiver_type(node_context))
497+
end
498+
474499
private
475500

476501
def index_and_locate(source, position)

0 commit comments

Comments
 (0)