From 9a834472404a1f26329471f321c7dd8f608ecc2c Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Tue, 23 Jul 2024 10:51:33 -0400 Subject: [PATCH] Handle Sorbet levels to prevent some feature duplication (#2322) --- lib/ruby_lsp/document.rb | 31 +++- lib/ruby_lsp/listeners/completion.rb | 76 ++++++---- lib/ruby_lsp/listeners/definition.rb | 26 +++- lib/ruby_lsp/listeners/hover.rb | 21 ++- lib/ruby_lsp/listeners/signature_help.rb | 8 +- lib/ruby_lsp/requests/completion.rb | 6 +- lib/ruby_lsp/requests/definition.rb | 6 +- lib/ruby_lsp/requests/hover.rb | 6 +- lib/ruby_lsp/requests/signature_help.rb | 6 +- lib/ruby_lsp/requests/support/common.rb | 5 + lib/ruby_lsp/server.rb | 16 +- test/requests/completion_test.rb | 137 ++++++++++++++++-- test/requests/definition_expectations_test.rb | 93 +++++++++++- test/requests/hover_expectations_test.rb | 77 +++++++++- test/requests/signature_help_test.rb | 26 ++++ test/ruby_document_test.rb | 16 +- 16 files changed, 460 insertions(+), 96 deletions(-) diff --git a/lib/ruby_lsp/document.rb b/lib/ruby_lsp/document.rb index d2f439fb7..e57ad3de8 100644 --- a/lib/ruby_lsp/document.rb +++ b/lib/ruby_lsp/document.rb @@ -10,6 +10,16 @@ class LanguageId < T::Enum end end + class SorbetLevel < T::Enum + enums do + None = new("none") + Ignore = new("ignore") + False = new("false") + True = new("true") + Strict = new("strict") + end + end + extend T::Sig extend T::Helpers @@ -213,10 +223,23 @@ 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 "ignore" + SorbetLevel::Ignore + when "false" + SorbetLevel::False + when "true" + SorbetLevel::True + when "strict", "strong" + SorbetLevel::Strict + else + SorbetLevel::None end end diff --git a/lib/ruby_lsp/listeners/completion.rb b/lib/ruby_lsp/listeners/completion.rb index 1a74d062e..967b1f439 100644 --- a/lib/ruby_lsp/listeners/completion.rb +++ b/lib/ruby_lsp/listeners/completion.rb @@ -56,7 +56,7 @@ class Completion response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem], global_state: GlobalState, node_context: NodeContext, - typechecker_enabled: T::Boolean, + sorbet_level: Document::SorbetLevel, dispatcher: Prism::Dispatcher, uri: URI::Generic, trigger_character: T.nilable(String), @@ -66,7 +66,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists response_builder, global_state, node_context, - typechecker_enabled, + sorbet_level, dispatcher, uri, trigger_character @@ -76,7 +76,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 @@ -97,7 +97,9 @@ def initialize( # rubocop:disable Metrics/ParameterLists # Handle completion on regular constant references (e.g. `Bar`) sig { params(node: Prism::ConstantReadNode).void } def on_constant_read_node_enter(node) - return if @global_state.has_type_checker + # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has + # no sigil, Sorbet will still provide completion for constants + return if @sorbet_level != Document::SorbetLevel::Ignore name = constant_name(node) return if name.nil? @@ -118,7 +120,9 @@ def on_constant_read_node_enter(node) # Handle completion on namespaced constant references (e.g. `Foo::Bar`) sig { params(node: Prism::ConstantPathNode).void } def on_constant_path_node_enter(node) - return if @global_state.has_type_checker + # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has + # no sigil, Sorbet will still provide completion for constants + return if @sorbet_level != Document::SorbetLevel::Ignore name = constant_name(node) return if name.nil? @@ -128,28 +132,32 @@ 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 + # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has + # no sigil, Sorbet will still provide completion for constants + if @sorbet_level == Document::SorbetLevel::Ignore + 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 @@ -162,7 +170,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 @@ -247,6 +255,10 @@ def constant_path_completion(name, range) sig { params(name: String, location: Prism::Location).void } def handle_instance_variable_completion(name, location) + # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able + # to provide all features for them + return if @sorbet_level == Document::SorbetLevel::Strict + type = @type_inferrer.infer_receiver_type(@node_context) return unless type @@ -321,12 +333,16 @@ def complete_require_relative(node) sig { params(node: Prism::CallNode, name: String).void } def complete_methods(node, name) - # If the node has a receiver, then we don't need to provide local nor keyword completions - if !@global_state.has_type_checker && !node.receiver + # If the node has a receiver, then we don't need to provide local nor keyword completions. Sorbet can provide + # local and keyword completion for any file with a Sorbet level of true or higher + if !sorbet_level_true_or_higher?(@sorbet_level) && !node.receiver add_local_completions(node, name) add_keyword_completions(node, name) end + # Sorbet can provide completion for methods invoked on self on typed true or higher files + return if sorbet_level_true_or_higher?(@sorbet_level) && self_receiver?(node) + type = @type_inferrer.infer_receiver_type(@node_context) return unless type diff --git a/lib/ruby_lsp/listeners/definition.rb b/lib/ruby_lsp/listeners/definition.rb index 8c775a0f2..9eda0e698 100644 --- a/lib/ruby_lsp/listeners/definition.rb +++ b/lib/ruby_lsp/listeners/definition.rb @@ -20,10 +20,10 @@ 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) @@ -31,7 +31,7 @@ def initialize(response_builder, global_state, language_id, uri, node_context, d @language_id = language_id @uri = uri @node_context = node_context - @typechecker_enabled = typechecker_enabled + @sorbet_level = sorbet_level dispatcher.register( self, @@ -53,6 +53,11 @@ def initialize(response_builder, global_state, language_id, uri, node_context, d sig { params(node: Prism::CallNode).void } def on_call_node_enter(node) + # Sorbet can handle go to definition for methods invoked on self on typed true or higher + if (@sorbet_level == Document::SorbetLevel::True || @sorbet_level == Document::SorbetLevel::Strict) && + self_receiver?(node) + end + message = node.message return unless message @@ -149,6 +154,9 @@ def on_forwarding_super_node_enter(node) sig { void } def handle_super_node_definition + # Sorbet can handle super hover on typed true or higher + return if sorbet_level_true_or_higher?(@sorbet_level) + surrounding_method = @node_context.surrounding_method return unless surrounding_method @@ -161,6 +169,10 @@ def handle_super_node_definition sig { params(name: String).void } def handle_instance_variable_definition(name) + # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able + # to provide all features for them + return if @sorbet_level == Document::SorbetLevel::Strict + type = @type_inferrer.infer_receiver_type(@node_context) return unless type @@ -196,7 +208,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_true_or_higher?(@sorbet_level) && not_in_dependencies?(file_path) @response_builder << Interface::LocationLink.new( target_uri: URI::Generic.from_path(path: file_path).to_s, @@ -253,10 +265,10 @@ def find_in_index(value) entries.each do |entry| # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an - # 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 + # additional behavior on top of jumping to RBIs. The only sigil where Sorbet cannot handle constants is typed + # ignore file_path = entry.file_path - next if @typechecker_enabled && not_in_dependencies?(file_path) + next if @sorbet_level != Document::SorbetLevel::Ignore && not_in_dependencies?(file_path) @response_builder << Interface::LocationLink.new( target_uri: URI::Generic.from_path(path: file_path).to_s, diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 57ab20b7c..cee8ef1e6 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -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, @@ -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::Ignore name = constant_name(node) return if name.nil? @@ -83,14 +83,14 @@ def on_constant_read_node_enter(node) sig { params(node: Prism::ConstantWriteNode).void } def on_constant_write_node_enter(node) - return if @global_state.has_type_checker + return if @sorbet_level != Document::SorbetLevel::Ignore generate_hover(node.name.to_s, node.name_loc) end sig { params(node: Prism::ConstantPathNode).void } def on_constant_path_node_enter(node) - return if @global_state.has_type_checker + return if @sorbet_level != Document::SorbetLevel::Ignore name = constant_name(node) return if name.nil? @@ -105,7 +105,7 @@ def on_call_node_enter(node) return end - return if @typechecker_enabled + return if sorbet_level_true_or_higher?(@sorbet_level) && self_receiver?(node) message = node.message return unless message @@ -157,6 +157,9 @@ def on_forwarding_super_node_enter(node) sig { void } def handle_super_node_hover + # Sorbet can handle super hover on typed true or higher + return if sorbet_level_true_or_higher?(@sorbet_level) + surrounding_method = @node_context.surrounding_method return unless surrounding_method @@ -180,6 +183,10 @@ def handle_method_hover(message, inherited_only: false) sig { params(name: String).void } def handle_instance_variable_hover(name) + # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able + # to provide all features for them + return if @sorbet_level == Document::SorbetLevel::Strict + type = @type_inferrer.infer_receiver_type(@node_context) return unless type diff --git a/lib/ruby_lsp/listeners/signature_help.rb b/lib/ruby_lsp/listeners/signature_help.rb index eab16126d..05f5a592b 100644 --- a/lib/ruby_lsp/listeners/signature_help.rb +++ b/lib/ruby_lsp/listeners/signature_help.rb @@ -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) @@ -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 if sorbet_level_true_or_higher?(@sorbet_level) message = node.message return unless message diff --git a/lib/ruby_lsp/requests/completion.rb b/lib/ruby_lsp/requests/completion.rb index bc1a8d5d2..820c5c8dd 100644 --- a/lib/ruby_lsp/requests/completion.rb +++ b/lib/ruby_lsp/requests/completion.rb @@ -49,11 +49,11 @@ def provider document: Document, global_state: GlobalState, params: T::Hash[Symbol, T.untyped], - typechecker_enabled: T::Boolean, + sorbet_level: 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 @@ -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), diff --git a/lib/ruby_lsp/requests/definition.rb b/lib/ruby_lsp/requests/definition.rb index 59e394f08..4cf4cab9f 100644 --- a/lib/ruby_lsp/requests/definition.rb +++ b/lib/ruby_lsp/requests/definition.rb @@ -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, @@ -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| diff --git a/lib/ruby_lsp/requests/hover.rb b/lib/ruby_lsp/requests/hover.rb index 23291f21b..27b10b7ba 100644 --- a/lib/ruby_lsp/requests/hover.rb +++ b/lib/ruby_lsp/requests/hover.rb @@ -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 @@ -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 diff --git a/lib/ruby_lsp/requests/signature_help.rb b/lib/ruby_lsp/requests/signature_help.rb index 3c36950df..5b50db6de 100644 --- a/lib/ruby_lsp/requests/signature_help.rb +++ b/lib/ruby_lsp/requests/signature_help.rb @@ -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] }, @@ -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)) } diff --git a/lib/ruby_lsp/requests/support/common.rb b/lib/ruby_lsp/requests/support/common.rb index 2f06de37a..232c5f155 100644 --- a/lib/ruby_lsp/requests/support/common.rb +++ b/lib/ruby_lsp/requests/support/common.rb @@ -204,6 +204,11 @@ def kind_for_entry(entry) Constant::SymbolKind::FIELD end end + + sig { params(sorbet_level: Document::SorbetLevel).returns(T::Boolean) } + def sorbet_level_true_or_higher?(sorbet_level) + sorbet_level == Document::SorbetLevel::True || sorbet_level == Document::SorbetLevel::Strict + end end end end diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index 051fa9aff..512e5bf05 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -477,15 +477,17 @@ def text_document_hover(message) @global_state, params[:position], dispatcher, - typechecker_enabled?(document), + sorbet_level(document), ).perform, ), ) end - sig { params(document: Document).returns(T::Boolean) } - def typechecker_enabled?(document) - @global_state.has_type_checker && document.sorbet_sigil_is_true_or_higher + sig { params(document: Document).returns(Document::SorbetLevel) } + def sorbet_level(document) + return Document::SorbetLevel::Ignore unless @global_state.has_type_checker + + document.sorbet_level end sig { params(message: T::Hash[Symbol, T.untyped]).void } @@ -594,7 +596,7 @@ def text_document_completion(message) document, @global_state, params, - typechecker_enabled?(document), + sorbet_level(document), dispatcher, ).perform, ), @@ -624,7 +626,7 @@ def text_document_signature_help(message) params[:position], params[:context], dispatcher, - typechecker_enabled?(document), + sorbet_level(document), ).perform, ), ) @@ -644,7 +646,7 @@ def text_document_definition(message) @global_state, params[:position], dispatcher, - typechecker_enabled?(document), + sorbet_level(document), ).perform, ), ) diff --git a/test/requests/completion_test.rb b/test/requests/completion_test.rb index 84bb8f765..cb5bf8bcf 100644 --- a/test/requests/completion_test.rb +++ b/test/requests/completion_test.rb @@ -477,11 +477,11 @@ class Foo def test_completion_for_methods_invoked_on_self source = +<<~RUBY class Foo - def bar(a, b); end - def baz(c, d); end + def qux1(a, b); end + def qux2(c, d); end def process - b + q end end RUBY @@ -494,9 +494,9 @@ def process }) result = server.pop_response.response - assert_equal(["bar", "baz"], result.map(&:label)) - assert_equal(["bar", "baz"], result.map(&:filter_text)) - assert_equal(["bar", "baz"], result.map { |completion| completion.text_edit.new_text }) + assert_equal(["qux1", "qux2"], result.map(&:label)) + assert_equal(["qux1", "qux2"], result.map(&:filter_text)) + assert_equal(["qux1", "qux2"], result.map { |completion| completion.text_edit.new_text }) assert_equal(["fake.rb", "fake.rb"], result.map { _1.label_details.description }) end end @@ -559,10 +559,10 @@ def process def test_completion_for_attributes source = +<<~RUBY class Foo - attr_accessor :bar + attr_accessor :qux - def qux - b + def bar + q end end RUBY @@ -575,9 +575,9 @@ def qux }) result = server.pop_response.response - assert_equal(["bar", "bar="], result.map(&:label)) - assert_equal(["bar", "bar="], result.map(&:filter_text)) - assert_equal(["bar", "bar="], result.map { |completion| completion.text_edit.new_text }) + assert_equal(["qux", "qux="], result.map(&:label)) + assert_equal(["qux", "qux="], result.map(&:filter_text)) + assert_equal(["qux", "qux="], result.map { |completion| completion.text_edit.new_text }) end end end @@ -663,7 +663,7 @@ def do_it }) result = server.pop_response.response - assert_equal(["method1", "method2"], result.map(&:label)) + assert_equal(["module", "method1", "method2"], result.map(&:label)) end end end @@ -1179,6 +1179,117 @@ def do_something end end + def test_self_method_completion_is_disabled_on_typed_true + source = +<<~RUBY + # typed: true + class Foo + def bar + b + end + + def baz; end + end + RUBY + + with_server(source) do |server, uri| + server.process_message(id: 1, method: "textDocument/completion", params: { + textDocument: { uri: uri }, + position: { line: 3, character: 5 }, + }) + + assert_empty(server.pop_response.response) + end + end + + def test_local_completion_is_disabled_on_typed_true + source = +<<~RUBY + # typed: true + class Foo + def bar + abc = 123 + a + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message(id: 1, method: "textDocument/completion", params: { + textDocument: { uri: uri }, + position: { line: 4, character: 5 }, + }) + + assert_empty(server.pop_response.response) + end + end + + def test_keyword_completion_is_disabled_on_typed_true + source = +<<~RUBY + # typed: true + class Foo + def bar + yie + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message(id: 1, method: "textDocument/completion", params: { + textDocument: { uri: uri }, + position: { line: 3, character: 7 }, + }) + + assert_empty(server.pop_response.response) + end + end + + def test_instance_variable_completion_is_disabled_on_typed_strict + source = +<<~RUBY + # typed: strict + class Foo + def initialize + @hello = 123 + end + + def bar + @ + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message(id: 1, method: "textDocument/completion", params: { + textDocument: { uri: uri }, + position: { line: 7, character: 5 }, + }) + + assert_empty(server.pop_response.response) + end + end + + def test_provides_constant_completion_on_type_ignore + source = +<<~RUBY + # typed: ignore + class Foo + end + + F + RUBY + + end_position = { line: 4, character: 1 } + + with_server(source) do |server, uri| + with_file_structure(server) do + server.process_message(id: 1, method: "textDocument/completion", params: { + textDocument: { uri: uri }, + position: end_position, + }) + result = server.pop_response.response + assert_equal(["Foo"], result.map(&:label)) + assert_equal(["fake.rb"], result.map { _1.label_details.description }) + end + end + end + private def with_file_structure(server, &block) diff --git a/test/requests/definition_expectations_test.rb b/test/requests/definition_expectations_test.rb index c438087ec..af4a74dff 100644 --- a/test/requests/definition_expectations_test.rb +++ b/test/requests/definition_expectations_test.rb @@ -103,7 +103,7 @@ class Baz Foo::Bar::Baz RUBY - with_server(source) do |server, uri| + with_server(source, stub_no_typechecker: true) do |server, uri| # Foo server.process_message( id: 1, @@ -170,7 +170,7 @@ class A end RUBY - with_server(source) do |server, uri| + with_server(source, stub_no_typechecker: true) do |server, uri| server.process_message( id: 1, method: "textDocument/definition", @@ -211,7 +211,7 @@ def test_definition_addons begin create_definition_addon - with_server(source) do |server, uri| + with_server(source, stub_no_typechecker: true) do |server, uri| server.global_state.index.index_single( RubyIndexer::IndexablePath.new( "#{Dir.pwd}/lib", @@ -780,6 +780,93 @@ def bar end end + def test_definition_for_super_calls_is_disabled_on_typed_true + source = <<~RUBY + # typed: true + class Parent + def foo; end + def bar; end + end + + class Child < Parent + def foo(a) + super() + end + + def bar + super + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/definition", + params: { textDocument: { uri: uri }, position: { character: 4, line: 8 } }, + ) + + assert_empty(server.pop_response.response) + + server.process_message( + id: 1, + method: "textDocument/definition", + params: { textDocument: { uri: uri }, position: { character: 4, line: 12 } }, + ) + + assert_empty(server.pop_response.response) + end + end + + def test_definition_on_self_is_disabled_for_typed_true + source = <<~RUBY + # typed: true + class Foo + def bar + baz + end + + def baz + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/definition", + params: { textDocument: { uri: uri }, position: { character: 4, line: 3 } }, + ) + + assert_empty(server.pop_response.response) + end + end + + def test_definition_for_instance_variables_is_disabled_on_typed_strict + source = <<~RUBY + # typed: strict + class Foo + def initialize + @something = T.let(123, Integer) + end + + def baz + @something + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/definition", + params: { textDocument: { uri: uri }, position: { character: 4, line: 7 } }, + ) + + assert_empty(server.pop_response.response) + end + end + private def create_definition_addon diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index 7d2477741..b1435a168 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -274,7 +274,7 @@ class Post begin create_hover_addon - with_server(source) do |server, uri| + with_server(source, stub_no_typechecker: true) do |server, uri| server.process_message( id: 1, method: "textDocument/hover", @@ -615,6 +615,81 @@ def bar end end + def test_hover_is_disabled_for_self_methods_on_typed_true + source = <<~RUBY + # typed: true + class Child + def foo + bar + end + + # Hey! + def bar + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/hover", + params: { textDocument: { uri: uri }, position: { character: 4, line: 3 } }, + ) + + assert_nil(server.pop_response.response) + end + end + + def test_hover_is_disabled_for_instance_variables_on_typed_strict + source = <<~RUBY + # typed: strict + class Child + def initialize + # Hello + @something = T.let(123, Integer) + end + + def bar + @something + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/hover", + params: { textDocument: { uri: uri }, position: { character: 4, line: 8 } }, + ) + + assert_nil(server.pop_response.response) + end + end + + def test_hover_is_disabled_on_super_for_typed_true + source = <<~RUBY + # typed: true + class Parent + def foo; end + end + class Child < Parent + def foo + super + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/hover", + params: { textDocument: { uri: uri }, position: { character: 4, line: 6 } }, + ) + + assert_nil(server.pop_response.response) + end + end + private def create_hover_addon diff --git a/test/requests/signature_help_test.rb b/test/requests/signature_help_test.rb index 6774854b3..40629dd2a 100644 --- a/test/requests/signature_help_test.rb +++ b/test/requests/signature_help_test.rb @@ -345,4 +345,30 @@ def do_something assert_equal(0, result.active_parameter) end end + + def test_help_is_disabled_on_typed_true + source = +<<~RUBY + # typed: true + class Foo + def bar(a, b) + end + + def baz + bar() + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message(id: 1, method: "textDocument/signatureHelp", params: { + textDocument: { uri: uri }, + position: { line: 6, character: 7 }, + context: { + triggerCharacter: "(", + activeSignatureHelp: nil, + }, + }) + assert_nil(server.pop_response.response) + end + end end diff --git a/test/ruby_document_test.rb b/test/ruby_document_test.rb index 8528801bd..1aca63b30 100644 --- a/test/ruby_document_test.rb +++ b/test/ruby_document_test.rb @@ -601,32 +601,32 @@ def test_no_sigil version: 1, uri: URI("file:///foo/bar.rb"), ) - refute_predicate(document, :sorbet_sigil_is_true_or_higher) + assert_equal(RubyLsp::Document::SorbetLevel::None, document.sorbet_level) end def test_sigil_ignore - document = RubyLsp::RubyDocument.new(source: +"# typed: ignored", version: 1, uri: URI("file:///foo/bar.rb")) - refute_predicate(document, :sorbet_sigil_is_true_or_higher) + document = RubyLsp::RubyDocument.new(source: +"# typed: ignore", version: 1, uri: URI("file:///foo/bar.rb")) + assert_equal(RubyLsp::Document::SorbetLevel::Ignore, document.sorbet_level) end def test_sigil_false document = RubyLsp::RubyDocument.new(source: +"# typed: false", version: 1, uri: URI("file:///foo/bar.rb")) - refute_predicate(document, :sorbet_sigil_is_true_or_higher) + assert_equal(RubyLsp::Document::SorbetLevel::False, document.sorbet_level) end def test_sigil_true document = RubyLsp::RubyDocument.new(source: +"# typed: true", version: 1, uri: URI("file:///foo/bar.rb")) - assert_predicate(document, :sorbet_sigil_is_true_or_higher) + assert_equal(RubyLsp::Document::SorbetLevel::True, document.sorbet_level) end def test_sigil_strict document = RubyLsp::RubyDocument.new(source: +"# typed: strict", version: 1, uri: URI("file:///foo/bar.rb")) - assert_predicate(document, :sorbet_sigil_is_true_or_higher) + assert_equal(RubyLsp::Document::SorbetLevel::Strict, document.sorbet_level) end def test_sigil_strong document = RubyLsp::RubyDocument.new(source: +"# typed: strong", version: 1, uri: URI("file:///foo/bar.rb")) - assert_predicate(document, :sorbet_sigil_is_true_or_higher) + assert_equal(RubyLsp::Document::SorbetLevel::Strict, document.sorbet_level) end def test_sorbet_sigil_only_in_magic_comment @@ -651,7 +651,7 @@ def baz CODE end RUBY - refute_predicate(document, :sorbet_sigil_is_true_or_higher) + assert_equal(RubyLsp::Document::SorbetLevel::False, document.sorbet_level) end def test_locating_compact_namespace_declaration