Skip to content

Commit

Permalink
Handle Sorbet levels to prevent some feature duplication
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Jul 17, 2024
1 parent 085e677 commit 62f3426
Show file tree
Hide file tree
Showing 15 changed files with 335 additions and 71 deletions.
25 changes: 21 additions & 4 deletions lib/ruby_lsp/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ class LanguageId < T::Enum
end
end

class SorbetLevel < T::Enum
enums do
Disabled = new("disabled")
Enabled = new("enabled")
Strict = new("strict")
end
end

extend T::Sig
extend T::Helpers

Expand Down Expand Up @@ -213,10 +221,19 @@ def locate(node, char_position, node_types: [])
NodeContext.new(closest, parent, nesting_nodes, call_node)
end

sig { returns(T::Boolean) }
def sorbet_sigil_is_true_or_higher
parse_result.magic_comments.any? do |comment|
comment.key == "typed" && ["true", "strict", "strong"].include?(comment.value)
sig { returns(SorbetLevel) }
def sorbet_level
sigil = parse_result.magic_comments.find do |comment|
comment.key == "typed"
end&.value

case sigil
when "true"
SorbetLevel::Enabled
when "strict", "strong"
SorbetLevel::Strict
else
SorbetLevel::Disabled
end
end

Expand Down
58 changes: 31 additions & 27 deletions lib/ruby_lsp/listeners/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Completion
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
global_state: GlobalState,
node_context: NodeContext,
typechecker_enabled: T::Boolean,
sorbet_level: T.nilable(Document::SorbetLevel),
dispatcher: Prism::Dispatcher,
uri: URI::Generic,
trigger_character: T.nilable(String),
Expand All @@ -22,7 +22,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists
response_builder,
global_state,
node_context,
typechecker_enabled,
sorbet_level,
dispatcher,
uri,
trigger_character
Expand All @@ -32,7 +32,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists
@index = T.let(global_state.index, RubyIndexer::Index)
@type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
@node_context = node_context
@typechecker_enabled = typechecker_enabled
@sorbet_level = sorbet_level
@uri = uri
@trigger_character = trigger_character

Expand Down Expand Up @@ -84,28 +84,30 @@ def on_constant_path_node_enter(node)

sig { params(node: Prism::CallNode).void }
def on_call_node_enter(node)
receiver = node.receiver

# When writing `Foo::`, the AST assigns a method call node (because you can use that syntax to invoke singleton
# methods). However, in addition to providing method completion, we also need to show possible constant
# completions
if (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)) &&
node.call_operator == "::"

name = constant_name(receiver)

if name
start_loc = node.location
end_loc = T.must(node.call_operator_loc)

constant_path_completion(
"#{name}::",
Interface::Range.new(
start: Interface::Position.new(line: start_loc.start_line - 1, character: start_loc.start_column),
end: Interface::Position.new(line: end_loc.end_line - 1, character: end_loc.end_column),
),
)
return
unless @global_state.has_type_checker
receiver = node.receiver

# When writing `Foo::`, the AST assigns a method call node (because you can use that syntax to invoke
# singleton methods). However, in addition to providing method completion, we also need to show possible
# constant completions
if (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)) &&
node.call_operator == "::"

name = constant_name(receiver)

if name
start_loc = node.location
end_loc = T.must(node.call_operator_loc)

constant_path_completion(
"#{name}::",
Interface::Range.new(
start: Interface::Position.new(line: start_loc.start_line - 1, character: start_loc.start_column),
end: Interface::Position.new(line: end_loc.end_line - 1, character: end_loc.end_column),
),
)
return
end
end
end

Expand All @@ -118,7 +120,7 @@ def on_call_node_enter(node)
when "require_relative"
complete_require_relative(node)
else
complete_methods(node, name) unless @typechecker_enabled
complete_methods(node, name)
end
end

Expand Down Expand Up @@ -279,6 +281,8 @@ def complete_require_relative(node)
def complete_methods(node, name)
add_local_completions(node, name)

return if @sorbet_level != Document::SorbetLevel::Disabled && self_receiver?(node)

type = @type_inferrer.infer_receiver_type(@node_context)
return unless type

Expand Down Expand Up @@ -326,7 +330,7 @@ def complete_methods(node, name)

sig { params(node: Prism::CallNode, name: String).void }
def add_local_completions(node, name)
return if @global_state.has_type_checker
return unless @sorbet_level == Document::SorbetLevel::Disabled

# If the call node has a receiver, then it cannot possibly be a local variable
return if node.receiver
Expand Down
16 changes: 11 additions & 5 deletions lib/ruby_lsp/listeners/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ class Definition
uri: URI::Generic,
node_context: NodeContext,
dispatcher: Prism::Dispatcher,
typechecker_enabled: T::Boolean,
sorbet_level: Document::SorbetLevel,
).void
end
def initialize(response_builder, global_state, language_id, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
def initialize(response_builder, global_state, language_id, uri, node_context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
@response_builder = response_builder
@global_state = global_state
@index = T.let(global_state.index, RubyIndexer::Index)
@type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
@language_id = language_id
@uri = uri
@node_context = node_context
@typechecker_enabled = typechecker_enabled
@sorbet_level = sorbet_level

dispatcher.register(
self,
Expand All @@ -53,6 +53,8 @@ def initialize(response_builder, global_state, language_id, uri, node_context, d

sig { params(node: Prism::CallNode).void }
def on_call_node_enter(node)
return if @sorbet_level != Document::SorbetLevel::Disabled && self_receiver?(node)

message = node.message
return unless message

Expand Down Expand Up @@ -149,6 +151,8 @@ def on_forwarding_super_node_enter(node)

sig { void }
def handle_super_node_definition
return unless @sorbet_level == Document::SorbetLevel::Disabled

surrounding_method = @node_context.surrounding_method
return unless surrounding_method

Expand All @@ -161,6 +165,8 @@ def handle_super_node_definition

sig { params(name: String).void }
def handle_instance_variable_definition(name)
return if @sorbet_level == Document::SorbetLevel::Strict

type = @type_inferrer.infer_receiver_type(@node_context)
return unless type

Expand Down Expand Up @@ -196,7 +202,7 @@ def handle_method_definition(message, receiver_type, inherited_only: false)

methods.each do |target_method|
file_path = target_method.file_path
next if @typechecker_enabled && not_in_dependencies?(file_path)
next if @sorbet_level != Document::SorbetLevel::Disabled && not_in_dependencies?(file_path)

@response_builder << Interface::LocationLink.new(
target_uri: URI::Generic.from_path(path: file_path).to_s,
Expand Down Expand Up @@ -256,7 +262,7 @@ def find_in_index(value)
# additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
# in the project, even if the files are typed false
file_path = entry.file_path
next if @typechecker_enabled && not_in_dependencies?(file_path)
next if @sorbet_level != Document::SorbetLevel::Disabled && not_in_dependencies?(file_path)

@response_builder << Interface::LocationLink.new(
target_uri: URI::Generic.from_path(path: file_path).to_s,
Expand Down
14 changes: 9 additions & 5 deletions lib/ruby_lsp/listeners/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ class Hover
uri: URI::Generic,
node_context: NodeContext,
dispatcher: Prism::Dispatcher,
typechecker_enabled: T::Boolean,
sorbet_level: Document::SorbetLevel,
).void
end
def initialize(response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
def initialize(response_builder, global_state, uri, node_context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
@response_builder = response_builder
@global_state = global_state
@index = T.let(global_state.index, RubyIndexer::Index)
@type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
@path = T.let(uri.to_standardized_path, T.nilable(String))
@node_context = node_context
@typechecker_enabled = typechecker_enabled
@sorbet_level = sorbet_level

dispatcher.register(
self,
Expand All @@ -73,7 +73,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, ty

sig { params(node: Prism::ConstantReadNode).void }
def on_constant_read_node_enter(node)
return if @typechecker_enabled
return if @sorbet_level != Document::SorbetLevel::Disabled

name = constant_name(node)
return if name.nil?
Expand Down Expand Up @@ -105,7 +105,7 @@ def on_call_node_enter(node)
return
end

return if @typechecker_enabled
return if @sorbet_level != Document::SorbetLevel::Disabled && self_receiver?(node)

message = node.message
return unless message
Expand Down Expand Up @@ -157,6 +157,8 @@ def on_forwarding_super_node_enter(node)

sig { void }
def handle_super_node_hover
return unless @sorbet_level == Document::SorbetLevel::Disabled

surrounding_method = @node_context.surrounding_method
return unless surrounding_method

Expand All @@ -180,6 +182,8 @@ def handle_method_hover(message, inherited_only: false)

sig { params(name: String).void }
def handle_instance_variable_hover(name)
return if @sorbet_level == Document::SorbetLevel::Strict

type = @type_inferrer.infer_receiver_type(@node_context)
return unless type

Expand Down
8 changes: 4 additions & 4 deletions lib/ruby_lsp/listeners/signature_help.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ class SignatureHelp
global_state: GlobalState,
node_context: NodeContext,
dispatcher: Prism::Dispatcher,
typechecker_enabled: T::Boolean,
sorbet_level: Document::SorbetLevel,
).void
end
def initialize(response_builder, global_state, node_context, dispatcher, typechecker_enabled)
@typechecker_enabled = typechecker_enabled
def initialize(response_builder, global_state, node_context, dispatcher, sorbet_level)
@sorbet_level = sorbet_level
@response_builder = response_builder
@global_state = global_state
@index = T.let(global_state.index, RubyIndexer::Index)
Expand All @@ -28,7 +28,7 @@ def initialize(response_builder, global_state, node_context, dispatcher, typeche

sig { params(node: Prism::CallNode).void }
def on_call_node_enter(node)
return if @typechecker_enabled
return unless @sorbet_level == Document::SorbetLevel::Disabled

message = node.message
return unless message
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_lsp/requests/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ def provider
document: Document,
global_state: GlobalState,
params: T::Hash[Symbol, T.untyped],
typechecker_enabled: T::Boolean,
sorbet_level: T.nilable(Document::SorbetLevel),
dispatcher: Prism::Dispatcher,
).void
end
def initialize(document, global_state, params, typechecker_enabled, dispatcher)
def initialize(document, global_state, params, sorbet_level, dispatcher)
super()
@target = T.let(nil, T.nilable(Prism::Node))
@dispatcher = dispatcher
Expand Down Expand Up @@ -84,7 +84,7 @@ def initialize(document, global_state, params, typechecker_enabled, dispatcher)
@response_builder,
global_state,
node_context,
typechecker_enabled,
sorbet_level,
dispatcher,
document.uri,
params.dig(:context, :triggerCharacter),
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_lsp/requests/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ class Definition < Request
global_state: GlobalState,
position: T::Hash[Symbol, T.untyped],
dispatcher: Prism::Dispatcher,
typechecker_enabled: T::Boolean,
sorbet_level: Document::SorbetLevel,
).void
end
def initialize(document, global_state, position, dispatcher, typechecker_enabled)
def initialize(document, global_state, position, dispatcher, sorbet_level)
super()
@response_builder = T.let(
ResponseBuilders::CollectionResponseBuilder[T.any(Interface::Location, Interface::LocationLink)].new,
Expand Down Expand Up @@ -96,7 +96,7 @@ def initialize(document, global_state, position, dispatcher, typechecker_enabled
document.uri,
node_context,
dispatcher,
typechecker_enabled,
sorbet_level,
)

Addon.addons.each do |addon|
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_lsp/requests/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ def provider
global_state: GlobalState,
position: T::Hash[Symbol, T.untyped],
dispatcher: Prism::Dispatcher,
typechecker_enabled: T::Boolean,
sorbet_level: Document::SorbetLevel,
).void
end
def initialize(document, global_state, position, dispatcher, typechecker_enabled)
def initialize(document, global_state, position, dispatcher, sorbet_level)
super()
node_context = document.locate_node(position, node_types: Listeners::Hover::ALLOWED_TARGETS)
target = node_context.node
Expand All @@ -65,7 +65,7 @@ def initialize(document, global_state, position, dispatcher, typechecker_enabled
@target = T.let(target, T.nilable(Prism::Node))
uri = document.uri
@response_builder = T.let(ResponseBuilders::Hover.new, ResponseBuilders::Hover)
Listeners::Hover.new(@response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled)
Listeners::Hover.new(@response_builder, global_state, uri, node_context, dispatcher, sorbet_level)
Addon.addons.each do |addon|
addon.create_hover_listener(@response_builder, node_context, dispatcher)
end
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_lsp/requests/signature_help.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ def provider
position: T::Hash[Symbol, T.untyped],
context: T.nilable(T::Hash[Symbol, T.untyped]),
dispatcher: Prism::Dispatcher,
typechecker_enabled: T::Boolean,
sorbet_level: Document::SorbetLevel,
).void
end
def initialize(document, global_state, position, context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
def initialize(document, global_state, position, context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
super()
node_context = document.locate_node(
{ line: position[:line], character: position[:character] },
Expand All @@ -61,7 +61,7 @@ def initialize(document, global_state, position, context, dispatcher, typechecke
@target = T.let(target, T.nilable(Prism::Node))
@dispatcher = dispatcher
@response_builder = T.let(ResponseBuilders::SignatureHelp.new, ResponseBuilders::SignatureHelp)
Listeners::SignatureHelp.new(@response_builder, global_state, node_context, dispatcher, typechecker_enabled)
Listeners::SignatureHelp.new(@response_builder, global_state, node_context, dispatcher, sorbet_level)
end

sig { override.returns(T.nilable(Interface::SignatureHelp)) }
Expand Down
Loading

0 comments on commit 62f3426

Please sign in to comment.