ruby-lsp 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|