ruby-lsp 0.23.11 → 0.26.1

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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +10 -4
  5. data/exe/ruby-lsp-check +0 -4
  6. data/exe/ruby-lsp-launcher +45 -22
  7. data/exe/ruby-lsp-test-exec +6 -0
  8. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +1 -2
  9. data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +3 -6
  10. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +82 -116
  11. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +140 -183
  12. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +10 -14
  13. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +107 -236
  14. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +166 -281
  15. data/lib/ruby_indexer/lib/ruby_indexer/location.rb +4 -27
  16. data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +23 -27
  17. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +25 -57
  18. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +58 -68
  19. data/lib/ruby_indexer/lib/ruby_indexer/uri.rb +17 -19
  20. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +7 -11
  21. data/lib/ruby_indexer/test/class_variables_test.rb +14 -14
  22. data/lib/ruby_indexer/test/classes_and_modules_test.rb +65 -40
  23. data/lib/ruby_indexer/test/configuration_test.rb +49 -9
  24. data/lib/ruby_indexer/test/constant_test.rb +34 -34
  25. data/lib/ruby_indexer/test/enhancements_test.rb +1 -1
  26. data/lib/ruby_indexer/test/index_test.rb +185 -135
  27. data/lib/ruby_indexer/test/instance_variables_test.rb +61 -37
  28. data/lib/ruby_indexer/test/method_test.rb +166 -123
  29. data/lib/ruby_indexer/test/prefix_tree_test.rb +21 -21
  30. data/lib/ruby_indexer/test/rbs_indexer_test.rb +70 -75
  31. data/lib/ruby_indexer/test/reference_finder_test.rb +79 -14
  32. data/lib/ruby_indexer/test/test_case.rb +9 -3
  33. data/lib/ruby_indexer/test/uri_test.rb +15 -2
  34. data/lib/ruby_lsp/addon.rb +88 -86
  35. data/lib/ruby_lsp/base_server.rb +59 -54
  36. data/lib/ruby_lsp/client_capabilities.rb +16 -13
  37. data/lib/ruby_lsp/document.rb +205 -104
  38. data/lib/ruby_lsp/erb_document.rb +45 -47
  39. data/lib/ruby_lsp/global_state.rb +73 -57
  40. data/lib/ruby_lsp/internal.rb +8 -3
  41. data/lib/ruby_lsp/listeners/code_lens.rb +82 -89
  42. data/lib/ruby_lsp/listeners/completion.rb +81 -76
  43. data/lib/ruby_lsp/listeners/definition.rb +44 -58
  44. data/lib/ruby_lsp/listeners/document_highlight.rb +123 -150
  45. data/lib/ruby_lsp/listeners/document_link.rb +50 -70
  46. data/lib/ruby_lsp/listeners/document_symbol.rb +38 -52
  47. data/lib/ruby_lsp/listeners/folding_ranges.rb +40 -43
  48. data/lib/ruby_lsp/listeners/hover.rb +107 -115
  49. data/lib/ruby_lsp/listeners/inlay_hints.rb +8 -13
  50. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +54 -56
  51. data/lib/ruby_lsp/listeners/signature_help.rb +12 -27
  52. data/lib/ruby_lsp/listeners/spec_style.rb +214 -0
  53. data/lib/ruby_lsp/listeners/test_discovery.rb +92 -0
  54. data/lib/ruby_lsp/listeners/test_style.rb +205 -95
  55. data/lib/ruby_lsp/node_context.rb +12 -39
  56. data/lib/ruby_lsp/rbs_document.rb +10 -11
  57. data/lib/ruby_lsp/requests/code_action_resolve.rb +65 -61
  58. data/lib/ruby_lsp/requests/code_actions.rb +14 -26
  59. data/lib/ruby_lsp/requests/code_lens.rb +31 -21
  60. data/lib/ruby_lsp/requests/completion.rb +8 -21
  61. data/lib/ruby_lsp/requests/completion_resolve.rb +6 -6
  62. data/lib/ruby_lsp/requests/definition.rb +8 -20
  63. data/lib/ruby_lsp/requests/diagnostics.rb +8 -11
  64. data/lib/ruby_lsp/requests/discover_tests.rb +20 -7
  65. data/lib/ruby_lsp/requests/document_highlight.rb +6 -16
  66. data/lib/ruby_lsp/requests/document_link.rb +6 -17
  67. data/lib/ruby_lsp/requests/document_symbol.rb +5 -8
  68. data/lib/ruby_lsp/requests/folding_ranges.rb +7 -15
  69. data/lib/ruby_lsp/requests/formatting.rb +6 -9
  70. data/lib/ruby_lsp/requests/go_to_relevant_file.rb +85 -0
  71. data/lib/ruby_lsp/requests/hover.rb +12 -25
  72. data/lib/ruby_lsp/requests/inlay_hints.rb +8 -19
  73. data/lib/ruby_lsp/requests/on_type_formatting.rb +32 -40
  74. data/lib/ruby_lsp/requests/prepare_rename.rb +5 -10
  75. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +5 -15
  76. data/lib/ruby_lsp/requests/range_formatting.rb +5 -6
  77. data/lib/ruby_lsp/requests/references.rb +17 -57
  78. data/lib/ruby_lsp/requests/rename.rb +27 -51
  79. data/lib/ruby_lsp/requests/request.rb +13 -25
  80. data/lib/ruby_lsp/requests/selection_ranges.rb +7 -7
  81. data/lib/ruby_lsp/requests/semantic_highlighting.rb +16 -35
  82. data/lib/ruby_lsp/requests/show_syntax_tree.rb +7 -8
  83. data/lib/ruby_lsp/requests/signature_help.rb +9 -27
  84. data/lib/ruby_lsp/requests/support/annotation.rb +4 -10
  85. data/lib/ruby_lsp/requests/support/common.rb +16 -58
  86. data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
  87. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +27 -35
  88. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +13 -16
  89. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +34 -36
  90. data/lib/ruby_lsp/requests/support/selection_range.rb +1 -3
  91. data/lib/ruby_lsp/requests/support/sorbet.rb +29 -38
  92. data/lib/ruby_lsp/requests/support/source_uri.rb +20 -32
  93. data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +12 -19
  94. data/lib/ruby_lsp/requests/support/test_item.rb +16 -14
  95. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +5 -6
  96. data/lib/ruby_lsp/requests/workspace_symbol.rb +4 -4
  97. data/lib/ruby_lsp/response_builders/collection_response_builder.rb +6 -9
  98. data/lib/ruby_lsp/response_builders/document_symbol.rb +15 -21
  99. data/lib/ruby_lsp/response_builders/hover.rb +12 -18
  100. data/lib/ruby_lsp/response_builders/response_builder.rb +6 -7
  101. data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +62 -91
  102. data/lib/ruby_lsp/response_builders/signature_help.rb +6 -8
  103. data/lib/ruby_lsp/response_builders/test_collection.rb +35 -13
  104. data/lib/ruby_lsp/ruby_document.rb +32 -98
  105. data/lib/ruby_lsp/scope.rb +7 -11
  106. data/lib/ruby_lsp/scripts/compose_bundle.rb +6 -4
  107. data/lib/ruby_lsp/server.rb +303 -196
  108. data/lib/ruby_lsp/setup_bundler.rb +121 -82
  109. data/lib/ruby_lsp/static_docs.rb +12 -7
  110. data/lib/ruby_lsp/store.rb +21 -49
  111. data/lib/ruby_lsp/test_helper.rb +3 -16
  112. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +233 -0
  113. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +145 -0
  114. data/lib/ruby_lsp/test_reporters/test_unit_reporter.rb +92 -0
  115. data/lib/ruby_lsp/type_inferrer.rb +13 -14
  116. data/lib/ruby_lsp/utils.rb +138 -93
  117. data/static_docs/break.md +103 -0
  118. metadata +14 -20
  119. data/lib/ruby_lsp/load_sorbet.rb +0 -62
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b93894c0d2c2687def87d9e173c7a9e45e810088613cb902c2a4b1a9d93a8404
4
- data.tar.gz: 9e92f360c8367cde864ac1a5dd2305170a975fa3164e6fc2c1d6f492396fbbc5
3
+ metadata.gz: c80f549675508ffbb28d649de04506adabec01eac9aa4c6eee057ec848adf858
4
+ data.tar.gz: 71ea1a4d628444b98bc1173748f5aecf0d71bdc8d3dc80f33b2779c9c78d9de0
5
5
  SHA512:
6
- metadata.gz: fc2172fba1c50192cb86d2dfc619c633cf756f3ebb574caaaeabb81fe23d283b7b896f48fe23981d29c472fce98248137171d119b33c75d1a7f6fafc73c7a775
7
- data.tar.gz: 03b0b5987fc8dcc6c989ce6c5df55e16fff906836ef74aa993ddc445b50c5d3458775fee1a31b9e228710df44f80b8338390ef1fdf75d464deac4e38fe822644
6
+ metadata.gz: 7261bf15c095154ff36152492aa252cbd84b84f6e83eb458623c7bb9790a57957885a855c2ee10683b7035267177e323c013988befc02e96f3fdf0274d2312ca
7
+ data.tar.gz: 74bcea4844e876230713400776bf7def1db44492bcf913526195b5d01919efd24ac69b517594ce2499e332184bf7ef4881e17e7694574bf4d35afabeda25b8c4
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![Build Status](https://github.com/Shopify/ruby-lsp/workflows/CI/badge.svg)](https://github.com/Shopify/ruby-lsp/actions/workflows/ci.yml)
6
6
  [![Ruby LSP extension](https://img.shields.io/badge/VS%20Code-Ruby%20LSP-success?logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
7
- [![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://join.slack.com/t/ruby-dx/shared_invite/zt-2yd77ayis-yAiVc1TX_kH0mHMBbi89dA)
7
+ [![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://shopify.github.io/ruby-lsp/invite)
8
8
 
9
9
  # Ruby LSP
10
10
 
@@ -13,7 +13,7 @@ for Ruby, used to improve rich features in editors. It is a part of a wider goal
13
13
  experience to Ruby developers using modern standards for cross-editor features, documentation and debugging.
14
14
 
15
15
  Want to discuss Ruby developer experience? Consider joining the public
16
- [Ruby DX Slack workspace](https://join.slack.com/t/ruby-dx/shared_invite/zt-2yd77ayis-yAiVc1TX_kH0mHMBbi89dA).
16
+ [Ruby DX Slack workspace](https://shopify.github.io/ruby-lsp/invite).
17
17
 
18
18
  ## Getting Started
19
19
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.23.11
1
+ 0.26.1
data/exe/ruby-lsp CHANGED
@@ -88,13 +88,17 @@ if ENV["BUNDLE_GEMFILE"].nil?
88
88
  exit exec(env, "#{base_command} exec ruby-lsp #{original_args.join(" ")}".strip)
89
89
  end
90
90
 
91
+ $stdin.sync = true
92
+ $stdout.sync = true
93
+ $stderr.sync = true
94
+ $stdin.binmode
95
+ $stdout.binmode
96
+ $stderr.binmode
97
+
91
98
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
92
99
 
93
- require "ruby_lsp/load_sorbet"
94
100
  require "ruby_lsp/internal"
95
101
 
96
- T::Utils.run_all_sig_blocks
97
-
98
102
  if options[:debug]
99
103
  if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
100
104
  $stderr.puts "Debugging is not supported on Windows"
@@ -147,8 +151,10 @@ if options[:doctor]
147
151
  return
148
152
  end
149
153
 
154
+ server = RubyLsp::Server.new
155
+
150
156
  # Ensure all output goes out stderr by default to allow puts/p/pp to work
151
157
  # without specifying output device.
152
158
  $> = $stderr
153
159
 
154
- RubyLsp::Server.new.start
160
+ server.start
data/exe/ruby-lsp-check CHANGED
@@ -3,13 +3,9 @@
3
3
 
4
4
  # This executable checks if all automatic LSP requests run successfully on every Ruby file under the current directory
5
5
 
6
- require "ruby_lsp/load_sorbet"
7
-
8
6
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
9
7
  require "ruby_lsp/internal"
10
8
 
11
- T::Utils.run_all_sig_blocks
12
-
13
9
  files = Dir.glob("#{Dir.pwd}/**/*.rb")
14
10
 
15
11
  puts "Verifying that all automatic LSP requests execute successfully. This may take a while..."
@@ -6,22 +6,35 @@
6
6
  # composed bundle
7
7
  # !!!!!!!
8
8
 
9
+ $stdin.sync = true
10
+ $stdout.sync = true
11
+ $stderr.sync = true
12
+ $stdin.binmode
13
+ $stdout.binmode
14
+ $stderr.binmode
15
+
9
16
  setup_error = nil
10
17
  install_error = nil
11
18
  reboot = false
12
19
 
13
20
  workspace_uri = ARGV.first
21
+ raw_initialize_path = File.join(".ruby-lsp", "raw_initialize")
14
22
 
15
23
  raw_initialize = if workspace_uri && !workspace_uri.start_with?("--")
16
24
  # If there's an argument without `--`, then it's the server asking to compose the bundle and passing to this
17
25
  # executable the workspace URI. We can't require gems at this point, so we built a fake initialize request manually
18
26
  reboot = true
19
27
  "{\"params\":{\"workspaceFolders\":[{\"uri\":\"#{workspace_uri}\"}]}}"
28
+ elsif ARGV.include?("--retry")
29
+ # If we're trying to re-boot automatically, we can't try to read the same initialize request again from the pipe. We
30
+ # need to ensure that the retry mechanism always writes the request to a file, so that we can reuse it
31
+ content = File.read(raw_initialize_path)
32
+ File.delete(raw_initialize_path)
33
+ content
20
34
  else
21
35
  # Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
22
36
  # Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
23
37
  # to ensure that we're setting up the bundle in the right place
24
- $stdin.binmode
25
38
  headers = $stdin.gets("\r\n\r\n")
26
39
  content_length = headers[/Content-Length: (\d+)/i, 1].to_i
27
40
  $stdin.read(content_length)
@@ -76,26 +89,33 @@ begin
76
89
  # This Marshal load can only happen after requiring Bundler because it will load a custom error class from Bundler
77
90
  # itself. If we try to load before requiring, the class will not be defined and loading will fail
78
91
  error_path = File.join(".ruby-lsp", "install_error")
79
- install_error = if File.exist?(error_path)
80
- Marshal.load(File.read(error_path))
92
+ install_error = begin
93
+ Marshal.load(File.read(error_path)) if File.exist?(error_path)
94
+ rescue ArgumentError
95
+ # The class we tried to load is not defined. This might happen when the user upgrades Bundler and new error
96
+ # classes are introduced or removed
97
+ File.delete(error_path)
98
+ nil
81
99
  end
82
100
 
83
101
  Bundler.setup
84
102
  $stderr.puts("Composed Bundle set up successfully")
85
103
  end
86
- rescue StandardError => e
87
- # If installing gems failed for any reason, we don't want to exit the process prematurely. We can still provide most
88
- # features in a degraded mode. We simply save the error so that we can report to the user that certain gems might be
89
- # missing, but we respect the LSP life cycle.
90
- #
91
- # If an install error occurred and one of the gems is not installed, Bundler.setup is guaranteed to fail with
92
- # `Bundler::GemNotFound`. We don't set `setup_error` here because that would essentially report the same problem twice
93
- # to telemetry, which is not useful
94
- unless install_error && e.is_a?(Bundler::GemNotFound)
95
- setup_error = e
96
- $stderr.puts("Failed to set up composed Bundle\n#{e.full_message}")
104
+ rescue Bundler::GemNotFound, Bundler::GitError
105
+ # Sometimes, we successfully set up the bundle, but users either change their Gemfile or uninstall gems from an
106
+ # external process. If there's no install error, but the gem is still not found, then we need to attempt to start from
107
+ # scratch
108
+ unless install_error || ARGV.include?("--retry")
109
+ $stderr.puts("Initial bundle compose succeeded, but Bundler.setup failed. Trying to restart from scratch...")
110
+ File.write(raw_initialize_path, raw_initialize)
111
+ exec(Gem.ruby, __FILE__, *ARGV, "--retry")
97
112
  end
98
113
 
114
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
115
+ rescue StandardError => e
116
+ setup_error = e
117
+ $stderr.puts("Failed to set up composed Bundle\n#{e.full_message}")
118
+
99
119
  # If Bundler.setup fails, we need to restore the original $LOAD_PATH so that we can still require the Ruby LSP server
100
120
  # in degraded mode
101
121
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
@@ -111,11 +131,8 @@ end
111
131
  # Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
112
132
  # configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP
113
133
  # paths into the load path manually or we may end up requiring the wrong version of the gem
114
- require "ruby_lsp/load_sorbet"
115
134
  require "ruby_lsp/internal"
116
135
 
117
- T::Utils.run_all_sig_blocks
118
-
119
136
  if ARGV.include?("--debug")
120
137
  if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
121
138
  $stderr.puts "Debugging is not supported on Windows"
@@ -129,22 +146,28 @@ if ARGV.include?("--debug")
129
146
  end
130
147
  end
131
148
 
132
- # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
133
- $> = $stderr
134
-
135
149
  initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize
136
150
 
137
151
  begin
138
- RubyLsp::Server.new(
152
+ server = RubyLsp::Server.new(
139
153
  install_error: install_error,
140
154
  setup_error: setup_error,
141
155
  initialize_request: initialize_request,
142
- ).start
156
+ )
157
+
158
+ # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
159
+ $> = $stderr
160
+
161
+ server.start
143
162
  rescue ArgumentError
144
163
  # If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat
145
164
  # and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize
146
165
  # request manually and then start the main loop
147
166
  server = RubyLsp::Server.new
167
+
168
+ # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
169
+ $> = $stderr
170
+
148
171
  server.process_message(initialize_request)
149
172
  server.start
150
173
  end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This executable will be removed thanks to the changes in https://github.com/Shopify/ruby-lsp/pull/3661.
5
+ # Remove this a few months after extension updates have rolled out
6
+ exec(*ARGV)
@@ -2,7 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "rubocop"
5
- require "sorbet-runtime"
6
5
 
7
6
  module RuboCop
8
7
  module Cop
@@ -29,7 +28,7 @@ module RuboCop
29
28
  class UseLanguageServerAliases < RuboCop::Cop::Base
30
29
  extend RuboCop::Cop::AutoCorrector
31
30
 
32
- ALIASED_CONSTANTS = T.let([:Interface, :Transport, :Constant].freeze, T::Array[Symbol])
31
+ ALIASED_CONSTANTS = [:Interface, :Transport, :Constant].freeze #: Array[Symbol]
33
32
 
34
33
  MSG = "Use constant alias `%{constant}`."
35
34
 
@@ -2,7 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "rubocop"
5
- require "sorbet-runtime"
6
5
 
7
6
  module RuboCop
8
7
  module Cop
@@ -63,8 +62,6 @@ module RuboCop
63
62
  # end
64
63
  # end
65
64
  class UseRegisterWithHandlerMethod < RuboCop::Cop::Base
66
- extend T::Sig
67
-
68
65
  MSG_MISSING_HANDLER = "Registered to `%{listener}` without a handler defined."
69
66
  MSG_MISSING_LISTENER = "Created a handler without registering the associated `%{listener}` event."
70
67
 
@@ -93,12 +90,12 @@ module RuboCop
93
90
 
94
91
  private
95
92
 
96
- sig { params(event_name: Symbol).returns(T::Boolean) }
93
+ #: (Symbol event_name) -> bool
97
94
  def valid_event_name?(event_name)
98
95
  /^on_.*(node_enter|node_leave)$/.match?(event_name)
99
96
  end
100
97
 
101
- sig { params(listeners: T::Array[RuboCop::AST::SymbolNode], handlers: T::Array[RuboCop::AST::DefNode]).void }
98
+ #: (Array[RuboCop::AST::SymbolNode] listeners, Array[RuboCop::AST::DefNode] handlers) -> void
102
99
  def add_offense_to_listeners_without_handler(listeners, handlers)
103
100
  return if listeners.none?
104
101
 
@@ -107,7 +104,7 @@ module RuboCop
107
104
  .each { |node| add_offense(node, message: format(MSG_MISSING_HANDLER, listener: node.value)) }
108
105
  end
109
106
 
110
- sig { params(listeners: T::Array[RuboCop::AST::SymbolNode], handlers: T::Array[RuboCop::AST::DefNode]).void }
107
+ #: (Array[RuboCop::AST::SymbolNode] listeners, Array[RuboCop::AST::DefNode] handlers) -> void
111
108
  def add_offense_handlers_without_listener(listeners, handlers)
112
109
  return if handlers.none?
113
110
 
@@ -3,85 +3,59 @@
3
3
 
4
4
  module RubyIndexer
5
5
  class Configuration
6
- extend T::Sig
7
-
8
- CONFIGURATION_SCHEMA = T.let(
9
- {
10
- "excluded_gems" => Array,
11
- "included_gems" => Array,
12
- "excluded_patterns" => Array,
13
- "included_patterns" => Array,
14
- "excluded_magic_comments" => Array,
15
- }.freeze,
16
- T::Hash[String, T.untyped],
17
- )
18
-
19
- sig { params(workspace_path: String).void }
6
+ CONFIGURATION_SCHEMA = {
7
+ "excluded_gems" => Array,
8
+ "included_gems" => Array,
9
+ "excluded_patterns" => Array,
10
+ "included_patterns" => Array,
11
+ "excluded_magic_comments" => Array,
12
+ }.freeze #: Hash[String, untyped]
13
+
14
+ #: String
20
15
  attr_writer :workspace_path
21
16
 
22
- sig { returns(Encoding) }
17
+ #: Encoding
23
18
  attr_accessor :encoding
24
19
 
25
- sig { void }
20
+ #: -> void
26
21
  def initialize
27
- @workspace_path = T.let(Dir.pwd, String)
28
- @encoding = T.let(Encoding::UTF_8, Encoding)
29
- @excluded_gems = T.let(initial_excluded_gems, T::Array[String])
30
- @included_gems = T.let([], 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
- )
22
+ @workspace_path = Dir.pwd #: String
23
+ @encoding = Encoding::UTF_8 #: Encoding
24
+ @excluded_gems = initial_excluded_gems #: Array[String]
25
+ @included_gems = [] #: Array[String]
26
+
27
+ @excluded_patterns = [
28
+ "**/{test,spec}/**/{*_test.rb,test_*.rb,*_spec.rb}",
29
+ "**/fixtures/**/*",
30
+ ] #: Array[String]
42
31
 
43
32
  path = Bundler.settings["path"]
44
33
  if path
45
34
  # Substitute Windows backslashes into forward slashes, which are used in glob patterns
46
35
  glob = path.gsub(/[\\]+/, "/")
47
- @excluded_patterns << File.join(glob, "**", "*.rb")
36
+ glob.delete_suffix!("/")
37
+ @excluded_patterns << "#{glob}/**/*.rb"
48
38
  end
49
39
 
50
- @included_patterns = T.let([File.join("**", "*.rb")], T::Array[String])
51
- @excluded_magic_comments = T.let(
52
- [
53
- "frozen_string_literal:",
54
- "typed:",
55
- "compiled:",
56
- "encoding:",
57
- "shareable_constant_value:",
58
- "warn_indent:",
59
- "rubocop:",
60
- "nodoc:",
61
- "doc:",
62
- "coding:",
63
- "warn_past_scope:",
64
- ],
65
- T::Array[String],
66
- )
67
- end
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(",")}}/**/*") }
40
+ # We start the included patterns with only the non excluded directories so that we can avoid paying the price of
41
+ # traversing large directories that don't include Ruby files like `node_modules`
42
+ @included_patterns = ["{#{top_level_directories.join(",")}}/**/*.rb", "*.rb"] #: Array[String]
43
+ @excluded_magic_comments = [
44
+ "frozen_string_literal:",
45
+ "typed:",
46
+ "compiled:",
47
+ "encoding:",
48
+ "shareable_constant_value:",
49
+ "warn_indent:",
50
+ "rubocop:",
51
+ "nodoc:",
52
+ "doc:",
53
+ "coding:",
54
+ "warn_past_scope:",
55
+ ] #: Array[String]
82
56
  end
83
57
 
84
- sig { returns(T::Array[URI::Generic]) }
58
+ #: -> Array[URI::Generic]
85
59
  def indexable_uris
86
60
  excluded_gems = @excluded_gems - @included_gems
87
61
  locked_gems = Bundler.locked_gems&.specs
@@ -91,51 +65,19 @@ module RubyIndexer
91
65
 
92
66
  flags = File::FNM_PATHNAME | File::FNM_EXTGLOB
93
67
 
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
- uris = T.let([], T::Array[URI::Generic])
111
-
112
- # Handle top level files separately. The path below is an optimization to prevent descending down directories that
113
- # are going to be excluded anyway, so we need to handle top level scripts separately
114
- Dir.glob(File.join(@workspace_path, "*.rb"), flags).each do |path|
115
- uris << URI::Generic.from_path(path: path)
116
- end
117
-
118
- # Add user specified patterns
119
- @included_patterns.each do |pattern|
120
- load_path_entry = T.let(nil, T.nilable(String))
68
+ uris = @included_patterns.flat_map do |pattern|
69
+ load_path_entry = nil #: String?
121
70
 
122
- included_paths.each do |included_path, relative_path|
123
- relative_pattern = pattern.delete_prefix(File.join(relative_path, "/"))
124
-
125
- next unless pattern.start_with?("**") || pattern.start_with?(relative_path)
126
-
127
- Dir.glob(File.join(included_path, relative_pattern), flags).each do |path|
128
- path = File.expand_path(path)
129
- # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
130
- # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This
131
- # happens on repositories that define multiple gems, like Rails. All frameworks are defined inside the
132
- # current workspace directory, but each one of them belongs to a different $LOAD_PATH entry
133
- if load_path_entry.nil? || !path.start_with?(load_path_entry)
134
- load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
135
- end
136
-
137
- uris << URI::Generic.from_path(path: path, load_path_entry: load_path_entry)
71
+ Dir.glob(File.join(@workspace_path, pattern), flags).map! do |path|
72
+ # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
73
+ # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens
74
+ # on repositories that define multiple gems, like Rails. All frameworks are defined inside the current
75
+ # workspace directory, but each one of them belongs to a different $LOAD_PATH entry
76
+ if load_path_entry.nil? || !path.start_with?(load_path_entry)
77
+ load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
138
78
  end
79
+
80
+ URI::Generic.from_path(path: path, load_path_entry: load_path_entry)
139
81
  end
140
82
  end
141
83
 
@@ -150,10 +92,12 @@ module RubyIndexer
150
92
  end
151
93
 
152
94
  # Remove user specified patterns
95
+ bundle_path = Bundler.settings["path"]&.gsub(/[\\]+/, "/")
153
96
  uris.reject! do |indexable|
154
- excluded_patterns.any? do |pattern|
155
- File.fnmatch?(pattern, T.must(indexable.full_path), File::FNM_PATHNAME | File::FNM_EXTGLOB)
156
- end
97
+ path = indexable.full_path #: as !nil
98
+ next false if test_files_ignored_from_exclusion?(path, bundle_path)
99
+
100
+ excluded_patterns.any? { |pattern| File.fnmatch?(pattern, path, flags) }
157
101
  end
158
102
 
159
103
  # Add default gems to the list of files to be indexed
@@ -222,12 +166,12 @@ module RubyIndexer
222
166
  uris
223
167
  end
224
168
 
225
- sig { returns(Regexp) }
169
+ #: -> Regexp
226
170
  def magic_comment_regex
227
- @magic_comment_regex ||= T.let(/^#\s*#{@excluded_magic_comments.join("|")}/, T.nilable(Regexp))
171
+ @magic_comment_regex ||= /^#\s*#{@excluded_magic_comments.join("|")}/ #: Regexp?
228
172
  end
229
173
 
230
- sig { params(config: T::Hash[String, T.untyped]).void }
174
+ #: (Hash[String, untyped] config) -> void
231
175
  def apply_config(config)
232
176
  validate_config!(config)
233
177
 
@@ -240,7 +184,7 @@ module RubyIndexer
240
184
 
241
185
  private
242
186
 
243
- sig { params(config: T::Hash[String, T.untyped]).void }
187
+ #: (Hash[String, untyped] config) -> void
244
188
  def validate_config!(config)
245
189
  errors = config.filter_map do |key, value|
246
190
  type = CONFIGURATION_SCHEMA[key]
@@ -255,7 +199,7 @@ module RubyIndexer
255
199
  raise ArgumentError, errors.join("\n") if errors.any?
256
200
  end
257
201
 
258
- sig { returns(T::Array[String]) }
202
+ #: -> Array[String]
259
203
  def initial_excluded_gems
260
204
  excluded, others = Bundler.definition.dependencies.partition do |dependency|
261
205
  dependency.groups == [:development]
@@ -305,5 +249,27 @@ module RubyIndexer
305
249
  rescue Bundler::GemfileNotFound
306
250
  []
307
251
  end
252
+
253
+ # Checks if the test file is never supposed to be ignored from indexing despite matching exclusion patterns, like
254
+ # `test_helper.rb` or `test_case.rb`. Also takes into consideration the possibility of finding these files under
255
+ # fixtures or inside gem source code if the bundle path points to a directory inside the workspace
256
+ #: (String path, String? bundle_path) -> bool
257
+ def test_files_ignored_from_exclusion?(path, bundle_path)
258
+ ["test_case.rb", "test_helper.rb"].include?(File.basename(path)) &&
259
+ !File.fnmatch?("**/fixtures/**/*", path, File::FNM_PATHNAME | File::FNM_EXTGLOB) &&
260
+ (!bundle_path || !path.start_with?(bundle_path))
261
+ end
262
+
263
+ #: -> Array[String]
264
+ def top_level_directories
265
+ excluded_directories = ["tmp", "node_modules", "sorbet"]
266
+
267
+ Dir.glob("#{Dir.pwd}/*").filter_map do |path|
268
+ dir_name = File.basename(path)
269
+ next unless File.directory?(path) && !excluded_directories.include?(dir_name)
270
+
271
+ dir_name
272
+ end
273
+ end
308
274
  end
309
275
  end