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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +24 -3
  5. data/exe/ruby-lsp-launcher +127 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +63 -12
  7. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +56 -2
  8. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +21 -6
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +15 -21
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +2 -2
  12. data/lib/ruby_indexer/test/enhancements_test.rb +51 -19
  13. data/lib/ruby_indexer/test/index_test.rb +91 -2
  14. data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
  15. data/lib/ruby_indexer/test/method_test.rb +26 -0
  16. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  17. data/lib/ruby_lsp/addon.rb +9 -2
  18. data/lib/ruby_lsp/base_server.rb +14 -5
  19. data/lib/ruby_lsp/client_capabilities.rb +60 -0
  20. data/lib/ruby_lsp/document.rb +1 -1
  21. data/lib/ruby_lsp/global_state.rb +20 -19
  22. data/lib/ruby_lsp/internal.rb +2 -0
  23. data/lib/ruby_lsp/listeners/completion.rb +62 -0
  24. data/lib/ruby_lsp/listeners/definition.rb +48 -13
  25. data/lib/ruby_lsp/listeners/hover.rb +52 -0
  26. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  27. data/lib/ruby_lsp/requests/completion.rb +7 -1
  28. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  29. data/lib/ruby_lsp/requests/definition.rb +26 -11
  30. data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
  31. data/lib/ruby_lsp/requests/hover.rb +24 -6
  32. data/lib/ruby_lsp/requests/references.rb +2 -0
  33. data/lib/ruby_lsp/requests/rename.rb +3 -1
  34. data/lib/ruby_lsp/requests/request.rb +1 -1
  35. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +11 -1
  36. data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
  37. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
  38. data/lib/ruby_lsp/server.rb +54 -16
  39. data/lib/ruby_lsp/setup_bundler.rb +132 -24
  40. data/lib/ruby_lsp/utils.rb +8 -0
  41. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d43d431cf15d817d80ebb8564f90a748b871ad3c2c278482d4b1b5c831b7bcd2
4
- data.tar.gz: 28ab582396723d582f7909ad277300430b6e14e6a281408be21a71f4d768930f
3
+ metadata.gz: a22b940469b6910a05ada8b39dbbc2ad918e26e41de0f2f287e79df46b8e46fe
4
+ data.tar.gz: a57e250627b82a42ba286fd9b14338c48fb04b5a2e76c889d28d93ea55f96f90
5
5
  SHA512:
6
- metadata.gz: 73a87212da50592a6b201a5f21ea559de74fb12ed795428e06392a4375aa73015a103aedd56734b9b05a2f0a27fcb7366b16107ea9f1a080b3459e2eff1dcd88
7
- data.tar.gz: 7ad77f3dccbabba9cb306c73ccefca4e84e9ff480bc0594d20a1c6e03727948119f7accefc522e5ed25d6de1d21fff03b18c704c7c8908b33eb92cfc5b2c665d
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 [CONTRIBUTING](CONTRIBUTING.md) for development instructions and check out our pinned
33
- [roadmap issue](https://github.com/Shopify/ruby-lsp/issues) for a list of tasks to get started.
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.20.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
- exit exec(env, "bundle exec ruby-lsp #{original_args.join(" ")}")
67
- end
81
+ base_bundle = if env["BUNDLER_VERSION"]
82
+ "bundle _#{env["BUNDLER_VERSION"]}_"
83
+ else
84
+ "bundle"
85
+ end
68
86
 
69
- require "ruby_lsp/load_sorbet"
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
- @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("tmp", "**", "*")], T::Array[String])
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
- indexables = @included_patterns.flat_map do |pattern|
113
+ @included_patterns.each do |pattern|
69
114
  load_path_entry = T.let(nil, T.nilable(String))
70
115
 
71
- Dir.glob(File.join(@workspace_path, pattern), File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
72
- path = File.expand_path(path)
73
- # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
74
- # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens
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
- IndexablePath.new(load_path_entry, path)
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.on_call_node(@index, @owner_stack.last, node, @file_path, @code_units_cache)
320
+ enhancement.on_call_node_enter(@owner_stack.last, node, @file_path, @code_units_cache)
319
321
  rescue StandardError => e
320
- @indexing_errors << "Indexing error in #{@file_path} with '#{enhancement.class.name}' enhancement: #{e.message}"
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
- module Enhancement
5
+ class Enhancement
6
6
  extend T::Sig
7
7
  extend T::Helpers
8
8
 
9
- interface!
9
+ abstract!
10
10
 
11
- requires_ancestor { Object }
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
- abstract.params(
18
- index: Index,
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 on_call_node(index, owner, node, file_path, code_units_cache); end
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 forwading argument, like `def foo(a, ...); end`
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 diffent classes in different files, we simply ignore it
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 redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside
978
- # of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with
979
- # `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the
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 "#{namespace}::#{name}" unless name.include?("::")
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
- # Find the first part of the name that is not in the nesting
993
- index = name_parts.index { |part| !nesting.include?(part) }
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 index.nil?
996
- # All parts of the nesting are redundant because they are already present in the name. We can return the name
997
- # directly
998
- name
999
- elsif index == 0
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 test_dynamically_namespaced_class_doesnt_affect_other_classes
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 test_dynamically_namespaced_module_doesnt_affect_other_modules
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
- include Enhancement
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
- include Enhancement
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 test_error_handling_in_enhancement
164
- enhancement_class = Class.new do
165
- include Enhancement
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
- def on_call_node(index, owner, node, file_path, code_units_cache)
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(%r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' enhancement}, stderr)
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