-
Notifications
You must be signed in to change notification settings - Fork 172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor dependency detector #1042
Conversation
d4571fa
to
b859892
Compare
lib/ruby_lsp/requests/hover.rb
Outdated
@@ -71,21 +71,21 @@ def merge_response!(other) | |||
|
|||
sig { params(node: YARP::ConstantReadNode).void } | |||
def on_constant_read(node) | |||
return if DependencyDetector::HAS_TYPECHECKER | |||
return if DependencyDetector.new.typechecker? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of this, we can pass in typechecker?
as a boolean from Executor
. (and similarly for the other requests).
71b849c
to
d97052d
Compare
d97052d
to
d425fac
Compare
lib/ruby_lsp/requests/definition.rb
Outdated
@@ -26,25 +26,29 @@ class Definition < ExtensibleListener | |||
sig { override.returns(ResponseType) } | |||
attr_reader :_response | |||
|
|||
# rubocop:disable Metrics/ParameterLists |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After this, we are thinking of introduce some kind of context object to group common parameters together, and reduce the number of things we need to pass around.
test/test_helper.rb
Outdated
Minitest::Test.make_my_diffs_pretty! | ||
|
||
sig { void } | ||
def stub_typechecking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(not sure if this name is clear enough)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'd go with something like stub_missing_typechecker
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good. One thing about naming conventions: I think both type checker
and typechecker
are correct, but we usually prefer typechecker
.
This PR is mixing both a little bit. Can we standardize on typechecker
?
lib/ruby_lsp/executor.rb
Outdated
@@ -13,7 +13,7 @@ def initialize(store, message_queue) | |||
# Requests that mutate the store must be run sequentially! Parallel requests only receive a temporary copy of the | |||
# store | |||
@store = store | |||
@test_library = T.let(DependencyDetector.detected_test_library, String) | |||
@dependency_detector = T.let(DependencyDetector.instance, RubyLsp::DependencyDetector) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this variable anymore when using the singleton. The requests can just access the singleton directly. It doesn't even need to be passed as a parameter anymore.
83c6334
to
9d211fe
Compare
lib/ruby_lsp/requests/completion.rb
Outdated
@@ -39,6 +39,7 @@ def initialize(index, nesting, emitter, message_queue) | |||
@_response = T.let([], ResponseType) | |||
@index = index | |||
@nesting = nesting | |||
@typechecker = T.let(DependencyDetector.instance.typechecker?, T::Boolean) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering whether it's worth storing this in an instance var, or just calling the singleton each time.
@@ -97,7 +96,7 @@ def run(request) | |||
folding_range = Requests::FoldingRanges.new(document.parse_result.comments, emitter, @message_queue) | |||
document_symbol = Requests::DocumentSymbol.new(emitter, @message_queue) | |||
document_link = Requests::DocumentLink.new(uri, document.comments, emitter, @message_queue) | |||
code_lens = Requests::CodeLens.new(uri, @test_library, emitter, @message_queue) | |||
code_lens = Requests::CodeLens.new(uri, emitter, @message_queue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed test_library
from the required parameters so that access is through the singleton, making it consistent with approach for other dependency checks.
@@ -0,0 +1,5 @@ | |||
# typed: true | |||
|
|||
module Singleton |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we upstream this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(just noticed we also use this in formatting_test_runner
but mark it unsafe)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we can push an RBI change to Sorbet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
d96ea49
to
c13831d
Compare
Using the singleton does mean we have to be careful about state leaking between tests - as I discovered, it's easy to accidentally have tests which will fail only occasionally. I've taken the approach that each test should be responsible for cleaning up after itself, so I've added a ( This approach also means we're resetting the singleton unnecessarily in many places, so slightly slowing the tests down, but I think it's worth it for the benefit of stability. |
def typechecker? | ||
@typechecker | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's get rid of the ?
and make this an attr reader. They have performance optimizations we'd miss out on with a regular method.
|
||
sig { void } | ||
def initialize | ||
@dependency_keys = T.let(nil, T.nilable(T::Array[String])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this instance variable is used anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used for the memoization. We could restructure the class to eliminate it, but think it's more readable this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need it here in the initialize if you do this in the memoized method.
sig { returns(T::Array[String]) }
def something
@something ||= T.let(whatever, T.nilable(T::Array[String]))
end
test/test_helper.rb
Outdated
def teardown | ||
Singleton.__init__(RubyLsp::DependencyDetector) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a big fan of this pattern to be honest. Without knowing about this piece of code, there's no way devs will know they have to invoke super
on their teardown
implementations.
Are there too many tests that depend on the state of the singleton being reset? Can we reset the singleton on each test that requires it instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there too many tests that depend on the state of the singleton being reset? Can we reset the singleton on each test that requires it instead?
We could do that, but I think it has a similar problem that if we introduce a new test, then we need to remember to add that teardown (if needed). Since it's order dependant, CI may still pass without it.
Another approach would be to switch the tests to use ActiveSupport::TestCase
. That provides a teardown {}
method that takes a block, which automatically calls super
. I know it's another dependency to add, but it would only be required for tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, let's not add this teardown. The test for the dependency detector itself can do this in its own teardown.
For all other tests that depend on a specific value being returned by something, we should use stubbing.
test/test_helper.rb
Outdated
Minitest::Test.make_my_diffs_pretty! | ||
|
||
sig { void } | ||
def stub_typechecking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'd go with something like stub_missing_typechecker
.
@index = RubyIndexer::Index.new | ||
end | ||
|
||
def teardown |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would stubbing on the setup
method not work here? Do we need to repeat it every time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried that originally but something wasn't working for test_matches_only_gem_symbols_if_typechecker_is_present
. I'll take another look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in d4476e2
|
||
sig { void } | ||
def initialize | ||
@dependency_keys = T.let(nil, T.nilable(T::Array[String])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need it here in the initialize if you do this in the memoized method.
sig { returns(T::Array[String]) }
def something
@something ||= T.let(whatever, T.nilable(T::Array[String]))
end
@@ -0,0 +1,5 @@ | |||
# typed: true | |||
|
|||
module Singleton |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we can push an RBI change to Sorbet.
test/requests/completion_test.rb
Outdated
@@ -198,7 +199,7 @@ def test_completion_is_not_triggered_if_argument_is_not_a_string | |||
end | |||
|
|||
def test_completion_for_constants | |||
RubyLsp::DependencyDetector.const_set(:HAS_TYPECHECKER, false) | |||
stub_no_typechecker |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do this in the setup
method and avoid the duplicates?
@@ -5,14 +5,10 @@ | |||
|
|||
class WorkspaceSymbolTest < Minitest::Test | |||
def setup | |||
RubyLsp::DependencyDetector.const_set(:HAS_TYPECHECKER, false) | |||
RubyLsp::DependencyDetector.instance.stubs(typechecker: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does invoking the new stub_typechecker
method work here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not use the stub_no_typechecker
here?
test/test_helper.rb
Outdated
def teardown | ||
Singleton.__init__(RubyLsp::DependencyDetector) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, let's not add this teardown. The test for the dependency detector itself can do this in its own teardown.
For all other tests that depend on a specific value being returned by something, we should use stubbing.
185b6c5
to
b53d10d
Compare
b53d10d
to
0e68bba
Compare
@@ -5,44 +5,50 @@ | |||
|
|||
module RubyLsp | |||
class DependencyDetectorTest < Minitest::Test | |||
def teardown |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this empty teardown necessary?
@@ -5,14 +5,10 @@ | |||
|
|||
class WorkspaceSymbolTest < Minitest::Test | |||
def setup | |||
RubyLsp::DependencyDetector.const_set(:HAS_TYPECHECKER, false) | |||
RubyLsp::DependencyDetector.instance.stubs(typechecker: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not use the stub_no_typechecker
here?
…nd-patch-6ccc4187eb Bump the minor-and-patch group with 3 updates
Motivation
The current DependencyDetector is somewhat awkward to work with, and because it cannot be instantiated, we are repeated several checks unnecessarily.
Implementation
Convert into a class. Move its use up to the
executor
layer.Automated Tests
Updated
Manual Tests
Verified locally.