-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
module RubyLsp | ||
# A minimalistic type checker to try to resolve types that can be inferred without requiring a type system or | ||
# annotations | ||
class TypeChecker | ||
extend T::Sig | ||
|
||
sig { params(index: RubyIndexer::Index).void } | ||
def initialize(index) | ||
@index = index | ||
end | ||
|
||
sig { params(node_context: NodeContext).returns(T.nilable(String)) } | ||
def infer_receiver_type(node_context) | ||
node = node_context.node | ||
|
||
case node | ||
when Prism::CallNode | ||
infer_receiver_for_call_node(node, node_context) | ||
when Prism::InstanceVariableReadNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableWriteNode, | ||
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableTargetNode | ||
|
||
return node_context.fully_qualified_name if node_context.surrounding_method | ||
|
||
nesting = node_context.nesting | ||
"#{nesting.join("::")}::<Class:#{nesting.last}>" | ||
end | ||
end | ||
|
||
private | ||
|
||
sig { params(node: Prism::CallNode, node_context: NodeContext).returns(T.nilable(String)) } | ||
def infer_receiver_for_call_node(node, node_context) | ||
receiver = node.receiver | ||
|
||
case receiver | ||
when Prism::SelfNode, nil | ||
return node_context.fully_qualified_name if node_context.surrounding_method | ||
|
||
# If we're not inside a method, then we're inside the body of a class or module, which is a singleton | ||
# context | ||
nesting = node_context.nesting | ||
"#{nesting.join("::")}::<Class:#{nesting.last}>" | ||
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 | ||
# class/module | ||
receiver_name = constant_name(receiver) | ||
return unless receiver_name | ||
|
||
resolved_receiver = @index.resolve(receiver_name, node_context.nesting) | ||
name = resolved_receiver&.first&.name | ||
return unless name | ||
|
||
*parts, last = name.split("::") | ||
return "#{last}::<Class:#{last}>" if T.must(parts).empty? | ||
|
||
"#{T.must(parts).join("::")}::#{last}::<Class:#{last}>" | ||
end | ||
end | ||
|
||
sig do | ||
params( | ||
node: T.any( | ||
Prism::ConstantPathNode, | ||
Prism::ConstantReadNode, | ||
), | ||
).returns(T.nilable(String)) | ||
end | ||
def constant_name(node) | ||
node.full_name | ||
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError, | ||
Prism::ConstantPathNode::MissingNodesInConstantPathError | ||
nil | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
# typed: true | ||
# frozen_string_literal: true | ||
|
||
require "test_helper" | ||
|
||
module RubyLsp | ||
class TypeCheckerTest < Minitest::Test | ||
def setup | ||
@index = RubyIndexer::Index.new | ||
@type_checker = TypeChecker.new(@index) | ||
end | ||
|
||
def test_infer_receiver_type_self_inside_method | ||
node_context = index_and_locate({ line: 2, character: 4 }, <<~RUBY) | ||
class Foo | ||
def bar | ||
baz | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_self_inside_class_body | ||
node_context = index_and_locate({ line: 1, character: 2 }, <<~RUBY) | ||
class Foo | ||
baz | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_self_inside_singleton_method | ||
node_context = index_and_locate({ line: 2, character: 4 }, <<~RUBY) | ||
class Foo | ||
def self.bar | ||
baz | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_self_inside_singleton_block_body | ||
node_context = index_and_locate({ line: 2, character: 4 }, <<~RUBY) | ||
class Foo | ||
class << self | ||
baz | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>::<Class:<Class:Foo>>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_self_inside_singleton_block_method | ||
node_context = index_and_locate({ line: 3, character: 6 }, <<~RUBY) | ||
class Foo | ||
class << self | ||
def bar | ||
baz | ||
end | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_constant | ||
node_context = index_and_locate({ line: 4, character: 4 }, <<~RUBY) | ||
class Foo | ||
def bar; end | ||
end | ||
Foo.bar | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_constant_path | ||
node_context = index_and_locate({ line: 6, character: 9 }, <<~RUBY) | ||
module Foo | ||
class Bar | ||
def baz; end | ||
end | ||
end | ||
Foo::Bar.baz | ||
RUBY | ||
|
||
assert_equal("Foo::Bar::<Class:Bar>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_instance_variables_in_class_body | ||
node_context = index_and_locate({ line: 1, character: 2 }, <<~RUBY) | ||
class Foo | ||
@hello1 | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_instance_variables_in_singleton_method | ||
node_context = index_and_locate({ line: 2, character: 4 }, <<~RUBY) | ||
class Foo | ||
def self.bar | ||
@hello1 | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_instance_variables_in_singleton_block_body | ||
node_context = index_and_locate({ line: 2, character: 4 }, <<~RUBY) | ||
class Foo | ||
class << self | ||
@hello1 | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>::<Class:<Class:Foo>>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_instance_variables_in_singleton_block_method | ||
node_context = index_and_locate({ line: 3, character: 6 }, <<~RUBY) | ||
class Foo | ||
class << self | ||
def bar | ||
@hello1 | ||
end | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo::<Class:Foo>", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
def test_infer_receiver_type_instance_variables_in_instance_method | ||
node_context = index_and_locate({ line: 2, character: 4 }, <<~RUBY) | ||
class Foo | ||
def bar | ||
@hello1 | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal("Foo", @type_checker.infer_receiver_type(node_context)) | ||
end | ||
|
||
private | ||
|
||
def index_and_locate(position, source) | ||
@index.index_single(RubyIndexer::IndexablePath.new(nil, "/fake/path/foo.rb"), source) | ||
document = RubyLsp::RubyDocument.new( | ||
source: source, | ||
version: 1, | ||
uri: URI::Generic.build(scheme: "file", path: "/fake/path/foo.rb"), | ||
) | ||
document.locate_node(position) | ||
end | ||
end | ||
end |