Skip to content

Commit

Permalink
Introduce simple type checker
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Jun 14, 2024
1 parent 46271dd commit 7e818de
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 0 deletions.
4 changes: 4 additions & 0 deletions lib/ruby_lsp/global_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class GlobalState
sig { returns(T::Boolean) }
attr_reader :supports_watching_files

sig { returns(TypeChecker) }
attr_reader :type_checker

sig { void }
def initialize
@workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
Expand All @@ -33,6 +36,7 @@ def initialize
@test_library = T.let("minitest", String)
@has_type_checker = T.let(true, T::Boolean)
@index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
@type_checker = T.let(TypeChecker.new(@index), TypeChecker)
@supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
@supports_watching_files = T.let(false, T::Boolean)
end
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_lsp/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
require "ruby_lsp/parameter_scope"
require "ruby_lsp/global_state"
require "ruby_lsp/server"
require "ruby_lsp/type_checker"
require "ruby_lsp/requests"
require "ruby_lsp/response_builders"
require "ruby_lsp/node_context"
Expand Down
79 changes: 79 additions & 0 deletions lib/ruby_lsp/type_checker.rb
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
171 changes: 171 additions & 0 deletions test/type_checker_test.rb
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

0 comments on commit 7e818de

Please sign in to comment.