ruby-lsp 0.20.0 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/VERSION +1 -1
- data/exe/ruby-lsp +24 -3
- data/exe/ruby-lsp-launcher +127 -0
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +63 -12
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +56 -2
- data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +21 -6
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +15 -21
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +2 -2
- data/lib/ruby_indexer/test/enhancements_test.rb +51 -19
- data/lib/ruby_indexer/test/index_test.rb +91 -2
- data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
- data/lib/ruby_indexer/test/method_test.rb +26 -0
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
- data/lib/ruby_lsp/addon.rb +9 -2
- data/lib/ruby_lsp/base_server.rb +14 -5
- data/lib/ruby_lsp/client_capabilities.rb +60 -0
- data/lib/ruby_lsp/document.rb +1 -1
- data/lib/ruby_lsp/global_state.rb +20 -19
- data/lib/ruby_lsp/internal.rb +2 -0
- data/lib/ruby_lsp/listeners/completion.rb +62 -0
- data/lib/ruby_lsp/listeners/definition.rb +48 -13
- data/lib/ruby_lsp/listeners/hover.rb +52 -0
- data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
- data/lib/ruby_lsp/requests/completion.rb +7 -1
- data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
- data/lib/ruby_lsp/requests/definition.rb +26 -11
- data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
- data/lib/ruby_lsp/requests/hover.rb +24 -6
- data/lib/ruby_lsp/requests/references.rb +2 -0
- data/lib/ruby_lsp/requests/rename.rb +3 -1
- data/lib/ruby_lsp/requests/request.rb +1 -1
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +11 -1
- data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
- data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
- data/lib/ruby_lsp/server.rb +54 -16
- data/lib/ruby_lsp/setup_bundler.rb +132 -24
- data/lib/ruby_lsp/utils.rb +8 -0
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a22b940469b6910a05ada8b39dbbc2ad918e26e41de0f2f287e79df46b8e46fe
|
4
|
+
data.tar.gz: a57e250627b82a42ba286fd9b14338c48fb04b5a2e76c889d28d93ea55f96f90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0661e457f068016e397436e76c9d48d8d4e2f718fb47268c6fe57797b1374e4669eb4c01b7a45ca26d8fe39dbc79f308797864e73aa31ba21e013ac3c2c97265
|
7
|
+
data.tar.gz: 709e136e94d223cb12fcb8a082d6193023fecc324aa1908f7a9dc2d9c3b612d6935cfe0362e7ba22fdd02ec3e6c1113a2a44e2a0f15230ff4698075a145d5e0d
|
data/README.md
CHANGED
@@ -29,8 +29,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopif
|
|
29
29
|
be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor
|
30
30
|
Covenant](CODE_OF_CONDUCT.md) code of conduct.
|
31
31
|
|
32
|
-
If you wish to contribute, see [
|
33
|
-
[roadmap
|
32
|
+
If you wish to contribute, see [Contributing](https://shopify.github.io/ruby-lsp/contributing.html) for development instructions and check out our
|
33
|
+
[Design and roadmap](https://shopify.github.io/ruby-lsp/design-and-roadmap.html) for a list of tasks to get started.
|
34
34
|
|
35
35
|
## License
|
36
36
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.21.0
|
data/exe/ruby-lsp
CHANGED
@@ -33,6 +33,10 @@ parser = OptionParser.new do |opts|
|
|
33
33
|
options[:doctor] = true
|
34
34
|
end
|
35
35
|
|
36
|
+
opts.on("--use-launcher", "[EXPERIMENTAL] Use launcher mechanism to handle missing dependencies gracefully") do
|
37
|
+
options[:launcher] = true
|
38
|
+
end
|
39
|
+
|
36
40
|
opts.on("-h", "--help", "Print this help") do
|
37
41
|
puts opts.help
|
38
42
|
puts
|
@@ -54,6 +58,17 @@ end
|
|
54
58
|
# using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of
|
55
59
|
# the application's bundle
|
56
60
|
if ENV["BUNDLE_GEMFILE"].nil?
|
61
|
+
# Substitute the current process by the launcher. RubyGems activates all dependencies of a gem's executable eagerly,
|
62
|
+
# but we can't have that happen because we want to invoke Bundler.setup ourselves with the composed bundle and avoid
|
63
|
+
# duplicate spec activation errors. Replacing the process with the launcher executable will clear the activated specs,
|
64
|
+
# which gives us the opportunity to control which specs are activated and enter degraded mode if any gems failed to
|
65
|
+
# install rather than failing to boot the server completely
|
66
|
+
if options[:launcher]
|
67
|
+
command = +File.expand_path("ruby-lsp-launcher", __dir__)
|
68
|
+
command << " --debug" if options[:debug]
|
69
|
+
exit exec(command)
|
70
|
+
end
|
71
|
+
|
57
72
|
require_relative "../lib/ruby_lsp/setup_bundler"
|
58
73
|
|
59
74
|
begin
|
@@ -63,12 +78,18 @@ if ENV["BUNDLE_GEMFILE"].nil?
|
|
63
78
|
exit(78)
|
64
79
|
end
|
65
80
|
|
66
|
-
|
67
|
-
|
81
|
+
base_bundle = if env["BUNDLER_VERSION"]
|
82
|
+
"bundle _#{env["BUNDLER_VERSION"]}_"
|
83
|
+
else
|
84
|
+
"bundle"
|
85
|
+
end
|
68
86
|
|
69
|
-
|
87
|
+
exit exec(env, "#{base_bundle} exec ruby-lsp #{original_args.join(" ")}".strip)
|
88
|
+
end
|
70
89
|
|
71
90
|
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
91
|
+
|
92
|
+
require "ruby_lsp/load_sorbet"
|
72
93
|
require "ruby_lsp/internal"
|
73
94
|
|
74
95
|
T::Utils.run_all_sig_blocks
|
@@ -0,0 +1,127 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# !!!!!!!
|
5
|
+
# No gems can be required in this file until we invoke bundler setup except inside the forked process that sets up the
|
6
|
+
# composed bundle
|
7
|
+
# !!!!!!!
|
8
|
+
|
9
|
+
setup_error = nil
|
10
|
+
|
11
|
+
# Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
|
12
|
+
# Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
|
13
|
+
# to ensure that we're setting up the bundle in the right place
|
14
|
+
$stdin.binmode
|
15
|
+
headers = $stdin.gets("\r\n\r\n")
|
16
|
+
content_length = headers[/Content-Length: (\d+)/i, 1].to_i
|
17
|
+
raw_initialize = $stdin.read(content_length)
|
18
|
+
|
19
|
+
# Compose the Ruby LSP bundle in a forked process so that we can require gems without polluting the main process
|
20
|
+
# `$LOAD_PATH` and `Gem.loaded_specs`. Windows doesn't support forking, so we need a separate path to support it
|
21
|
+
pid = if Gem.win_platform?
|
22
|
+
# Since we can't fork on Windows and spawn won't carry over the existing load paths, we need to explicitly pass that
|
23
|
+
# down to the child process or else requiring gems during composing the bundle will fail
|
24
|
+
load_path = $LOAD_PATH.flat_map do |path|
|
25
|
+
["-I", File.expand_path(path)]
|
26
|
+
end
|
27
|
+
|
28
|
+
Process.spawn(
|
29
|
+
Gem.ruby,
|
30
|
+
*load_path,
|
31
|
+
File.expand_path("../lib/ruby_lsp/scripts/compose_bundle_windows.rb", __dir__),
|
32
|
+
raw_initialize,
|
33
|
+
)
|
34
|
+
else
|
35
|
+
fork do
|
36
|
+
require_relative "../lib/ruby_lsp/scripts/compose_bundle"
|
37
|
+
compose(raw_initialize)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
begin
|
42
|
+
# Wait until the composed Bundle is finished
|
43
|
+
Process.wait(pid)
|
44
|
+
rescue Errno::ECHILD
|
45
|
+
# In theory, the child process can finish before we even get to the wait call, but that is not an error
|
46
|
+
end
|
47
|
+
|
48
|
+
begin
|
49
|
+
bundle_env_path = File.join(".ruby-lsp", "bundle_env")
|
50
|
+
# We can't require `bundler/setup` because that file prematurely exits the process if setup fails. However, we can't
|
51
|
+
# simply require bundler either because the version required might conflict with the one locked in the composed
|
52
|
+
# bundle. We need the composed bundle sub-process to inform us of the locked Bundler version, so that we can then
|
53
|
+
# activate the right spec and require the exact Bundler version required by the app
|
54
|
+
if File.exist?(bundle_env_path)
|
55
|
+
env = File.readlines(bundle_env_path).to_h { |line| line.chomp.split("=", 2) }
|
56
|
+
ENV.merge!(env)
|
57
|
+
|
58
|
+
if env["BUNDLER_VERSION"]
|
59
|
+
Gem::Specification.find_by_name("bundler", env["BUNDLER_VERSION"]).activate
|
60
|
+
end
|
61
|
+
|
62
|
+
require "bundler"
|
63
|
+
Bundler.ui.level = :silent
|
64
|
+
Bundler.setup
|
65
|
+
$stderr.puts("Composed Bundle set up successfully")
|
66
|
+
end
|
67
|
+
rescue StandardError => e
|
68
|
+
# If installing gems failed for any reason, we don't want to exit the process prematurely. We can still provide most
|
69
|
+
# features in a degraded mode. We simply save the error so that we can report to the user that certain gems might be
|
70
|
+
# missing, but we respect the LSP life cycle
|
71
|
+
setup_error = e
|
72
|
+
$stderr.puts("Failed to set up composed Bundle\n#{e.full_message}")
|
73
|
+
|
74
|
+
# If Bundler.setup fails, we need to restore the original $LOAD_PATH so that we can still require the Ruby LSP server
|
75
|
+
# in degraded mode
|
76
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
77
|
+
ensure
|
78
|
+
require "fileutils"
|
79
|
+
FileUtils.rm(bundle_env_path) if File.exist?(bundle_env_path)
|
80
|
+
end
|
81
|
+
|
82
|
+
error_path = File.join(".ruby-lsp", "install_error")
|
83
|
+
|
84
|
+
install_error = if File.exist?(error_path)
|
85
|
+
Marshal.load(File.read(error_path))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
|
89
|
+
# configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP
|
90
|
+
# paths into the load path manually or we may end up requiring the wrong version of the gem
|
91
|
+
require "ruby_lsp/load_sorbet"
|
92
|
+
require "ruby_lsp/internal"
|
93
|
+
|
94
|
+
T::Utils.run_all_sig_blocks
|
95
|
+
|
96
|
+
if ARGV.include?("--debug")
|
97
|
+
if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
|
98
|
+
$stderr.puts "Debugging is not supported on Windows"
|
99
|
+
else
|
100
|
+
begin
|
101
|
+
ENV.delete("RUBY_DEBUG_IRB_CONSOLE")
|
102
|
+
require "debug/open_nonstop"
|
103
|
+
rescue LoadError
|
104
|
+
$stderr.puts("You need to install the debug gem to use the --debug flag")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
|
110
|
+
$> = $stderr
|
111
|
+
|
112
|
+
initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize
|
113
|
+
|
114
|
+
begin
|
115
|
+
RubyLsp::Server.new(
|
116
|
+
install_error: install_error,
|
117
|
+
setup_error: setup_error,
|
118
|
+
initialize_request: initialize_request,
|
119
|
+
).start
|
120
|
+
rescue ArgumentError
|
121
|
+
# If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat
|
122
|
+
# and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize
|
123
|
+
# request manually and then start the main loop
|
124
|
+
server = RubyLsp::Server.new
|
125
|
+
server.process_message(initialize_request)
|
126
|
+
server.start
|
127
|
+
end
|
@@ -28,7 +28,17 @@ module RubyIndexer
|
|
28
28
|
@encoding = T.let(Encoding::UTF_8, Encoding)
|
29
29
|
@excluded_gems = T.let(initial_excluded_gems, T::Array[String])
|
30
30
|
@included_gems = T.let([], T::Array[String])
|
31
|
-
|
31
|
+
|
32
|
+
@excluded_patterns = T.let(
|
33
|
+
[
|
34
|
+
File.join("**", "*_test.rb"),
|
35
|
+
File.join("node_modules", "**", "*"),
|
36
|
+
File.join("spec", "**", "*"),
|
37
|
+
File.join("test", "**", "*"),
|
38
|
+
File.join("tmp", "**", "*"),
|
39
|
+
],
|
40
|
+
T::Array[String],
|
41
|
+
)
|
32
42
|
|
33
43
|
path = Bundler.settings["path"]
|
34
44
|
if path
|
@@ -56,6 +66,21 @@ module RubyIndexer
|
|
56
66
|
)
|
57
67
|
end
|
58
68
|
|
69
|
+
sig { returns(String) }
|
70
|
+
def merged_excluded_file_pattern
|
71
|
+
# This regex looks for @excluded_patterns that follow the format of "something/**/*", where
|
72
|
+
# "something" is one or more non-"/"
|
73
|
+
#
|
74
|
+
# Returns "/path/to/workspace/{tmp,node_modules}/**/*"
|
75
|
+
@excluded_patterns
|
76
|
+
.filter_map do |pattern|
|
77
|
+
next if File.absolute_path?(pattern)
|
78
|
+
|
79
|
+
pattern.match(%r{\A([^/]+)/\*\*/\*\z})&.captures&.first
|
80
|
+
end
|
81
|
+
.then { |dirs| File.join(@workspace_path, "{#{dirs.join(",")}}/**/*") }
|
82
|
+
end
|
83
|
+
|
59
84
|
sig { returns(T::Array[IndexablePath]) }
|
60
85
|
def indexables
|
61
86
|
excluded_gems = @excluded_gems - @included_gems
|
@@ -64,21 +89,47 @@ module RubyIndexer
|
|
64
89
|
# NOTE: indexing the patterns (both included and excluded) needs to happen before indexing gems, otherwise we risk
|
65
90
|
# having duplicates if BUNDLE_PATH is set to a folder inside the project structure
|
66
91
|
|
92
|
+
flags = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
93
|
+
|
94
|
+
# In order to speed up indexing, only traverse into top-level directories that are not entirely excluded.
|
95
|
+
# For example, if "tmp/**/*" is excluded, we don't need to traverse into "tmp" at all. However, if
|
96
|
+
# "vendor/bundle/**/*" is excluded, we will traverse all of "vendor" and `reject!` out all "vendor/bundle" entries
|
97
|
+
# later.
|
98
|
+
excluded_pattern = merged_excluded_file_pattern
|
99
|
+
included_paths = Dir.glob(File.join(@workspace_path, "*/"), flags)
|
100
|
+
.filter_map do |included_path|
|
101
|
+
next if File.fnmatch?(excluded_pattern, included_path, flags)
|
102
|
+
|
103
|
+
relative_path = included_path
|
104
|
+
.delete_prefix(@workspace_path)
|
105
|
+
.tap { |path| path.delete_prefix!("/") }
|
106
|
+
|
107
|
+
[included_path, relative_path]
|
108
|
+
end
|
109
|
+
|
110
|
+
indexables = T.let([], T::Array[IndexablePath])
|
111
|
+
|
67
112
|
# Add user specified patterns
|
68
|
-
|
113
|
+
@included_patterns.each do |pattern|
|
69
114
|
load_path_entry = T.let(nil, T.nilable(String))
|
70
115
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
# on repositories that define multiple gems, like Rails. All frameworks are defined inside the current
|
76
|
-
# workspace directory, but each one of them belongs to a different $LOAD_PATH entry
|
77
|
-
if load_path_entry.nil? || !path.start_with?(load_path_entry)
|
78
|
-
load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
|
79
|
-
end
|
116
|
+
included_paths.each do |included_path, relative_path|
|
117
|
+
relative_pattern = pattern.delete_prefix(File.join(relative_path, "/"))
|
118
|
+
|
119
|
+
next unless pattern.start_with?("**") || pattern.start_with?(relative_path)
|
80
120
|
|
81
|
-
|
121
|
+
Dir.glob(File.join(included_path, relative_pattern), flags).each do |path|
|
122
|
+
path = File.expand_path(path)
|
123
|
+
# All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
|
124
|
+
# entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This
|
125
|
+
# happens on repositories that define multiple gems, like Rails. All frameworks are defined inside the
|
126
|
+
# current workspace directory, but each one of them belongs to a different $LOAD_PATH entry
|
127
|
+
if load_path_entry.nil? || !path.start_with?(load_path_entry)
|
128
|
+
load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
|
129
|
+
end
|
130
|
+
|
131
|
+
indexables << IndexablePath.new(load_path_entry, path)
|
132
|
+
end
|
82
133
|
end
|
83
134
|
end
|
84
135
|
|
@@ -312,12 +312,16 @@ module RubyIndexer
|
|
312
312
|
@visibility_stack.push(Entry::Visibility::PROTECTED)
|
313
313
|
when :private
|
314
314
|
@visibility_stack.push(Entry::Visibility::PRIVATE)
|
315
|
+
when :module_function
|
316
|
+
handle_module_function(node)
|
315
317
|
end
|
316
318
|
|
317
319
|
@enhancements.each do |enhancement|
|
318
|
-
enhancement.
|
320
|
+
enhancement.on_call_node_enter(@owner_stack.last, node, @file_path, @code_units_cache)
|
319
321
|
rescue StandardError => e
|
320
|
-
@indexing_errors <<
|
322
|
+
@indexing_errors << <<~MSG
|
323
|
+
Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message}
|
324
|
+
MSG
|
321
325
|
end
|
322
326
|
end
|
323
327
|
|
@@ -332,6 +336,14 @@ module RubyIndexer
|
|
332
336
|
@visibility_stack.pop
|
333
337
|
end
|
334
338
|
end
|
339
|
+
|
340
|
+
@enhancements.each do |enhancement|
|
341
|
+
enhancement.on_call_node_leave(@owner_stack.last, node, @file_path, @code_units_cache)
|
342
|
+
rescue StandardError => e
|
343
|
+
@indexing_errors << <<~MSG
|
344
|
+
Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message}
|
345
|
+
MSG
|
346
|
+
end
|
335
347
|
end
|
336
348
|
|
337
349
|
sig { params(node: Prism::DefNode).void }
|
@@ -751,6 +763,48 @@ module RubyIndexer
|
|
751
763
|
end
|
752
764
|
end
|
753
765
|
|
766
|
+
sig { params(node: Prism::CallNode).void }
|
767
|
+
def handle_module_function(node)
|
768
|
+
arguments_node = node.arguments
|
769
|
+
return unless arguments_node
|
770
|
+
|
771
|
+
owner_name = @owner_stack.last&.name
|
772
|
+
return unless owner_name
|
773
|
+
|
774
|
+
arguments_node.arguments.each do |argument|
|
775
|
+
method_name = case argument
|
776
|
+
when Prism::StringNode
|
777
|
+
argument.content
|
778
|
+
when Prism::SymbolNode
|
779
|
+
argument.value
|
780
|
+
end
|
781
|
+
next unless method_name
|
782
|
+
|
783
|
+
entries = @index.resolve_method(method_name, owner_name)
|
784
|
+
next unless entries
|
785
|
+
|
786
|
+
entries.each do |entry|
|
787
|
+
entry_owner_name = entry.owner&.name
|
788
|
+
next unless entry_owner_name
|
789
|
+
|
790
|
+
entry.visibility = Entry::Visibility::PRIVATE
|
791
|
+
|
792
|
+
singleton = @index.existing_or_new_singleton_class(entry_owner_name)
|
793
|
+
location = Location.from_prism_location(argument.location, @code_units_cache)
|
794
|
+
@index.add(Entry::Method.new(
|
795
|
+
method_name,
|
796
|
+
@file_path,
|
797
|
+
location,
|
798
|
+
location,
|
799
|
+
collect_comments(node)&.concat(entry.comments),
|
800
|
+
entry.signatures,
|
801
|
+
Entry::Visibility::PUBLIC,
|
802
|
+
singleton,
|
803
|
+
))
|
804
|
+
end
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
754
808
|
sig { returns(Entry::Visibility) }
|
755
809
|
def current_visibility
|
756
810
|
T.must(@visibility_stack.last)
|
@@ -2,20 +2,35 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module RubyIndexer
|
5
|
-
|
5
|
+
class Enhancement
|
6
6
|
extend T::Sig
|
7
7
|
extend T::Helpers
|
8
8
|
|
9
|
-
|
9
|
+
abstract!
|
10
10
|
|
11
|
-
|
11
|
+
sig { params(index: Index).void }
|
12
|
+
def initialize(index)
|
13
|
+
@index = index
|
14
|
+
end
|
12
15
|
|
13
16
|
# The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to
|
14
17
|
# register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the
|
15
18
|
# `ClassMethods` modules
|
16
19
|
sig do
|
17
|
-
|
18
|
-
|
20
|
+
overridable.params(
|
21
|
+
owner: T.nilable(Entry::Namespace),
|
22
|
+
node: Prism::CallNode,
|
23
|
+
file_path: String,
|
24
|
+
code_units_cache: T.any(
|
25
|
+
T.proc.params(arg0: Integer).returns(Integer),
|
26
|
+
Prism::CodeUnitsCache,
|
27
|
+
),
|
28
|
+
).void
|
29
|
+
end
|
30
|
+
def on_call_node_enter(owner, node, file_path, code_units_cache); end
|
31
|
+
|
32
|
+
sig do
|
33
|
+
overridable.params(
|
19
34
|
owner: T.nilable(Entry::Namespace),
|
20
35
|
node: Prism::CallNode,
|
21
36
|
file_path: String,
|
@@ -25,6 +40,6 @@ module RubyIndexer
|
|
25
40
|
),
|
26
41
|
).void
|
27
42
|
end
|
28
|
-
def
|
43
|
+
def on_call_node_leave(owner, node, file_path, code_units_cache); end
|
29
44
|
end
|
30
45
|
end
|
@@ -646,7 +646,7 @@ module RubyIndexer
|
|
646
646
|
(positionals.empty? && forwarding_arguments.any?) ||
|
647
647
|
(
|
648
648
|
# Check if positional arguments match. This includes required, optional, rest arguments. We also need to
|
649
|
-
# verify if there's a trailing
|
649
|
+
# verify if there's a trailing forwarding argument, like `def foo(a, ...); end`
|
650
650
|
positional_arguments_match?(positionals, forwarding_arguments, keyword_args, min_pos, max_pos) &&
|
651
651
|
# If the positional arguments match, we move on to checking keyword, optional keyword and keyword rest
|
652
652
|
# arguments. If there's a forward argument, then it will always match. If the method accepts a keyword rest
|
@@ -784,7 +784,7 @@ module RubyIndexer
|
|
784
784
|
singleton_levels
|
785
785
|
)
|
786
786
|
# Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
|
787
|
-
# from two
|
787
|
+
# from two different classes in different files, we simply ignore it
|
788
788
|
superclass = T.cast(
|
789
789
|
if singleton_levels > 0
|
790
790
|
self[attached_class_name]&.find { |n| n.is_a?(Entry::Class) && n.parent_class }
|
@@ -974,35 +974,29 @@ module RubyIndexer
|
|
974
974
|
[]
|
975
975
|
end
|
976
976
|
|
977
|
-
# Removes
|
978
|
-
# of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up
|
979
|
-
# `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and
|
980
|
-
# nesting
|
977
|
+
# Removes redundancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo`
|
978
|
+
# inside of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up
|
979
|
+
# with `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and
|
980
|
+
# the nesting
|
981
981
|
sig { params(name: String, nesting: T::Array[String]).returns(String) }
|
982
982
|
def build_non_redundant_full_name(name, nesting)
|
983
|
+
# If there's no nesting, then we can just return the name as is
|
983
984
|
return name if nesting.empty?
|
984
985
|
|
985
|
-
namespace = nesting.join("::")
|
986
|
-
|
987
986
|
# If the name is not qualified, we can just concatenate the nesting and the name
|
988
|
-
return "#{
|
987
|
+
return "#{nesting.join("::")}::#{name}" unless name.include?("::")
|
989
988
|
|
990
989
|
name_parts = name.split("::")
|
990
|
+
first_redundant_part = nesting.index(name_parts[0])
|
991
991
|
|
992
|
-
#
|
993
|
-
|
992
|
+
# If there are no redundant parts between the name and the nesting, then the full name is both combined
|
993
|
+
return "#{nesting.join("::")}::#{name}" unless first_redundant_part
|
994
994
|
|
995
|
-
if
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
# No parts of the nesting are in the name, we can concatenate the namespace and the name
|
1001
|
-
"#{namespace}::#{name}"
|
1002
|
-
else
|
1003
|
-
# The name includes some parts of the nesting. We need to remove the redundant parts
|
1004
|
-
"#{namespace}::#{T.must(name_parts[index..-1]).join("::")}"
|
1005
|
-
end
|
995
|
+
# Otherwise, push all of the leading parts of the nesting that aren't redundant into the name. For example, if we
|
996
|
+
# have a reference to `Foo::Bar` inside the `[Namespace, Foo]` nesting, then only the `Foo` part is redundant, but
|
997
|
+
# we still need to include the `Namespace` part
|
998
|
+
T.unsafe(name_parts).unshift(*nesting[0...first_redundant_part])
|
999
|
+
name_parts.join("::")
|
1006
1000
|
end
|
1007
1001
|
|
1008
1002
|
sig do
|
@@ -72,7 +72,7 @@ module RubyIndexer
|
|
72
72
|
assert_entry("self::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
73
73
|
end
|
74
74
|
|
75
|
-
def
|
75
|
+
def test_dynamically_namespaced_class_does_not_affect_other_classes
|
76
76
|
index(<<~RUBY)
|
77
77
|
class Foo
|
78
78
|
class self::Bar
|
@@ -143,7 +143,7 @@ module RubyIndexer
|
|
143
143
|
assert_entry("self::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
|
144
144
|
end
|
145
145
|
|
146
|
-
def
|
146
|
+
def test_dynamically_namespaced_module_does_not_affect_other_modules
|
147
147
|
index(<<~RUBY)
|
148
148
|
module Foo
|
149
149
|
class self::Bar
|
@@ -6,10 +6,8 @@ require_relative "test_case"
|
|
6
6
|
module RubyIndexer
|
7
7
|
class EnhancementTest < TestCase
|
8
8
|
def test_enhancing_indexing_included_hook
|
9
|
-
enhancement_class = Class.new do
|
10
|
-
|
11
|
-
|
12
|
-
def on_call_node(index, owner, node, file_path, code_units_cache)
|
9
|
+
enhancement_class = Class.new(Enhancement) do
|
10
|
+
def on_call_node_enter(owner, node, file_path, code_units_cache)
|
13
11
|
return unless owner
|
14
12
|
return unless node.name == :extend
|
15
13
|
|
@@ -24,7 +22,7 @@ module RubyIndexer
|
|
24
22
|
module_name = node.full_name
|
25
23
|
next unless module_name == "ActiveSupport::Concern"
|
26
24
|
|
27
|
-
index.register_included_hook(owner.name) do |index, base|
|
25
|
+
@index.register_included_hook(owner.name) do |index, base|
|
28
26
|
class_methods_name = "#{owner.name}::ClassMethods"
|
29
27
|
|
30
28
|
if index.indexed?(class_methods_name)
|
@@ -33,7 +31,7 @@ module RubyIndexer
|
|
33
31
|
end
|
34
32
|
end
|
35
33
|
|
36
|
-
index.add(Entry::Method.new(
|
34
|
+
@index.add(Entry::Method.new(
|
37
35
|
"new_method",
|
38
36
|
file_path,
|
39
37
|
location,
|
@@ -50,7 +48,7 @@ module RubyIndexer
|
|
50
48
|
end
|
51
49
|
end
|
52
50
|
|
53
|
-
@index.register_enhancement(enhancement_class.new)
|
51
|
+
@index.register_enhancement(enhancement_class.new(@index))
|
54
52
|
index(<<~RUBY)
|
55
53
|
module ActiveSupport
|
56
54
|
module Concern
|
@@ -98,10 +96,8 @@ module RubyIndexer
|
|
98
96
|
end
|
99
97
|
|
100
98
|
def test_enhancing_indexing_configuration_dsl
|
101
|
-
enhancement_class = Class.new do
|
102
|
-
|
103
|
-
|
104
|
-
def on_call_node(index, owner, node, file_path, code_units_cache)
|
99
|
+
enhancement_class = Class.new(Enhancement) do
|
100
|
+
def on_call_node_enter(owner, node, file_path, code_units_cache)
|
105
101
|
return unless owner
|
106
102
|
|
107
103
|
name = node.name
|
@@ -115,7 +111,7 @@ module RubyIndexer
|
|
115
111
|
|
116
112
|
location = Location.from_prism_location(association_name.location, code_units_cache)
|
117
113
|
|
118
|
-
index.add(Entry::Method.new(
|
114
|
+
@index.add(Entry::Method.new(
|
119
115
|
T.must(association_name.value),
|
120
116
|
file_path,
|
121
117
|
location,
|
@@ -128,7 +124,7 @@ module RubyIndexer
|
|
128
124
|
end
|
129
125
|
end
|
130
126
|
|
131
|
-
@index.register_enhancement(enhancement_class.new)
|
127
|
+
@index.register_enhancement(enhancement_class.new(@index))
|
132
128
|
index(<<~RUBY)
|
133
129
|
module ActiveSupport
|
134
130
|
module Concern
|
@@ -160,11 +156,44 @@ module RubyIndexer
|
|
160
156
|
assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17")
|
161
157
|
end
|
162
158
|
|
163
|
-
def
|
164
|
-
enhancement_class = Class.new do
|
165
|
-
|
159
|
+
def test_error_handling_in_on_call_node_enter_enhancement
|
160
|
+
enhancement_class = Class.new(Enhancement) do
|
161
|
+
def on_call_node_enter(owner, node, file_path, code_units_cache)
|
162
|
+
raise "Error"
|
163
|
+
end
|
164
|
+
|
165
|
+
class << self
|
166
|
+
def name
|
167
|
+
"TestEnhancement"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
@index.register_enhancement(enhancement_class.new(@index))
|
173
|
+
|
174
|
+
_stdout, stderr = capture_io do
|
175
|
+
index(<<~RUBY)
|
176
|
+
module ActiveSupport
|
177
|
+
module Concern
|
178
|
+
def self.extended(base)
|
179
|
+
base.class_eval("def new_method(a); end")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
RUBY
|
184
|
+
end
|
185
|
+
|
186
|
+
assert_match(
|
187
|
+
%r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node enter enhancement},
|
188
|
+
stderr,
|
189
|
+
)
|
190
|
+
# The module should still be indexed
|
191
|
+
assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
|
192
|
+
end
|
166
193
|
|
167
|
-
|
194
|
+
def test_error_handling_in_on_call_node_leave_enhancement
|
195
|
+
enhancement_class = Class.new(Enhancement) do
|
196
|
+
def on_call_node_leave(owner, node, file_path, code_units_cache)
|
168
197
|
raise "Error"
|
169
198
|
end
|
170
199
|
|
@@ -175,7 +204,7 @@ module RubyIndexer
|
|
175
204
|
end
|
176
205
|
end
|
177
206
|
|
178
|
-
@index.register_enhancement(enhancement_class.new)
|
207
|
+
@index.register_enhancement(enhancement_class.new(@index))
|
179
208
|
|
180
209
|
_stdout, stderr = capture_io do
|
181
210
|
index(<<~RUBY)
|
@@ -189,7 +218,10 @@ module RubyIndexer
|
|
189
218
|
RUBY
|
190
219
|
end
|
191
220
|
|
192
|
-
assert_match(
|
221
|
+
assert_match(
|
222
|
+
%r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node leave enhancement},
|
223
|
+
stderr,
|
224
|
+
)
|
193
225
|
# The module should still be indexed
|
194
226
|
assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
|
195
227
|
end
|