ruby-lsp 0.23.11 → 0.23.16

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp-launcher +20 -11
  5. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +1 -1
  6. data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +3 -5
  7. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +82 -116
  8. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +123 -169
  9. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +9 -7
  10. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +92 -202
  11. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +116 -222
  12. data/lib/ruby_indexer/lib/ruby_indexer/location.rb +4 -27
  13. data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +18 -19
  14. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +22 -45
  15. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +47 -61
  16. data/lib/ruby_indexer/lib/ruby_indexer/uri.rb +17 -19
  17. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +5 -9
  18. data/lib/ruby_indexer/test/class_variables_test.rb +14 -14
  19. data/lib/ruby_indexer/test/classes_and_modules_test.rb +65 -40
  20. data/lib/ruby_indexer/test/configuration_test.rb +48 -7
  21. data/lib/ruby_indexer/test/constant_test.rb +34 -34
  22. data/lib/ruby_indexer/test/enhancements_test.rb +1 -1
  23. data/lib/ruby_indexer/test/index_test.rb +139 -135
  24. data/lib/ruby_indexer/test/instance_variables_test.rb +37 -37
  25. data/lib/ruby_indexer/test/method_test.rb +143 -117
  26. data/lib/ruby_indexer/test/prefix_tree_test.rb +13 -13
  27. data/lib/ruby_indexer/test/rbs_indexer_test.rb +65 -71
  28. data/lib/ruby_indexer/test/test_case.rb +2 -2
  29. data/lib/ruby_indexer/test/uri_test.rb +15 -2
  30. data/lib/ruby_lsp/addon.rb +44 -71
  31. data/lib/ruby_lsp/base_server.rb +29 -32
  32. data/lib/ruby_lsp/client_capabilities.rb +10 -12
  33. data/lib/ruby_lsp/document.rb +39 -45
  34. data/lib/ruby_lsp/erb_document.rb +36 -40
  35. data/lib/ruby_lsp/global_state.rb +52 -57
  36. data/lib/ruby_lsp/internal.rb +2 -0
  37. data/lib/ruby_lsp/listeners/code_lens.rb +82 -89
  38. data/lib/ruby_lsp/listeners/completion.rb +60 -66
  39. data/lib/ruby_lsp/listeners/definition.rb +38 -52
  40. data/lib/ruby_lsp/listeners/document_highlight.rb +123 -150
  41. data/lib/ruby_lsp/listeners/document_link.rb +46 -63
  42. data/lib/ruby_lsp/listeners/document_symbol.rb +38 -52
  43. data/lib/ruby_lsp/listeners/folding_ranges.rb +40 -43
  44. data/lib/ruby_lsp/listeners/hover.rb +83 -102
  45. data/lib/ruby_lsp/listeners/inlay_hints.rb +4 -11
  46. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +54 -56
  47. data/lib/ruby_lsp/listeners/signature_help.rb +11 -26
  48. data/lib/ruby_lsp/listeners/spec_style.rb +155 -0
  49. data/lib/ruby_lsp/listeners/test_discovery.rb +89 -0
  50. data/lib/ruby_lsp/listeners/test_style.rb +160 -88
  51. data/lib/ruby_lsp/node_context.rb +12 -39
  52. data/lib/ruby_lsp/rbs_document.rb +8 -6
  53. data/lib/ruby_lsp/requests/code_action_resolve.rb +24 -25
  54. data/lib/ruby_lsp/requests/code_actions.rb +14 -26
  55. data/lib/ruby_lsp/requests/code_lens.rb +6 -17
  56. data/lib/ruby_lsp/requests/completion.rb +7 -20
  57. data/lib/ruby_lsp/requests/completion_resolve.rb +6 -6
  58. data/lib/ruby_lsp/requests/definition.rb +8 -17
  59. data/lib/ruby_lsp/requests/diagnostics.rb +8 -11
  60. data/lib/ruby_lsp/requests/discover_tests.rb +18 -5
  61. data/lib/ruby_lsp/requests/document_highlight.rb +5 -15
  62. data/lib/ruby_lsp/requests/document_link.rb +6 -17
  63. data/lib/ruby_lsp/requests/document_symbol.rb +5 -8
  64. data/lib/ruby_lsp/requests/folding_ranges.rb +7 -15
  65. data/lib/ruby_lsp/requests/formatting.rb +6 -9
  66. data/lib/ruby_lsp/requests/go_to_relevant_file.rb +87 -0
  67. data/lib/ruby_lsp/requests/hover.rb +10 -20
  68. data/lib/ruby_lsp/requests/inlay_hints.rb +6 -17
  69. data/lib/ruby_lsp/requests/on_type_formatting.rb +32 -40
  70. data/lib/ruby_lsp/requests/prepare_rename.rb +4 -9
  71. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +5 -15
  72. data/lib/ruby_lsp/requests/range_formatting.rb +5 -6
  73. data/lib/ruby_lsp/requests/references.rb +8 -37
  74. data/lib/ruby_lsp/requests/rename.rb +19 -42
  75. data/lib/ruby_lsp/requests/request.rb +7 -19
  76. data/lib/ruby_lsp/requests/selection_ranges.rb +6 -6
  77. data/lib/ruby_lsp/requests/semantic_highlighting.rb +16 -35
  78. data/lib/ruby_lsp/requests/show_syntax_tree.rb +7 -8
  79. data/lib/ruby_lsp/requests/signature_help.rb +8 -26
  80. data/lib/ruby_lsp/requests/support/annotation.rb +4 -10
  81. data/lib/ruby_lsp/requests/support/common.rb +16 -51
  82. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +27 -35
  83. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +11 -14
  84. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +22 -34
  85. data/lib/ruby_lsp/requests/support/selection_range.rb +1 -3
  86. data/lib/ruby_lsp/requests/support/sorbet.rb +29 -38
  87. data/lib/ruby_lsp/requests/support/source_uri.rb +20 -32
  88. data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +12 -19
  89. data/lib/ruby_lsp/requests/support/test_item.rb +10 -14
  90. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +5 -6
  91. data/lib/ruby_lsp/requests/workspace_symbol.rb +4 -4
  92. data/lib/ruby_lsp/response_builders/collection_response_builder.rb +5 -5
  93. data/lib/ruby_lsp/response_builders/document_symbol.rb +13 -18
  94. data/lib/ruby_lsp/response_builders/hover.rb +11 -14
  95. data/lib/ruby_lsp/response_builders/response_builder.rb +1 -1
  96. data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +60 -88
  97. data/lib/ruby_lsp/response_builders/signature_help.rb +5 -6
  98. data/lib/ruby_lsp/response_builders/test_collection.rb +6 -10
  99. data/lib/ruby_lsp/ruby_document.rb +24 -62
  100. data/lib/ruby_lsp/scope.rb +7 -11
  101. data/lib/ruby_lsp/scripts/compose_bundle.rb +6 -4
  102. data/lib/ruby_lsp/server.rb +147 -79
  103. data/lib/ruby_lsp/setup_bundler.rb +65 -60
  104. data/lib/ruby_lsp/static_docs.rb +11 -7
  105. data/lib/ruby_lsp/store.rb +24 -42
  106. data/lib/ruby_lsp/test_helper.rb +2 -12
  107. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +164 -0
  108. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +105 -0
  109. data/lib/ruby_lsp/test_reporters/test_unit_reporter.rb +94 -0
  110. data/lib/ruby_lsp/type_inferrer.rb +13 -14
  111. data/lib/ruby_lsp/utils.rb +49 -83
  112. metadata +9 -3
@@ -25,50 +25,47 @@ module RubyLsp
25
25
  class BundleNotLocked < StandardError; end
26
26
  class BundleInstallFailure < StandardError; end
27
27
 
28
- FOUR_HOURS = T.let(4 * 60 * 60, Integer)
28
+ FOUR_HOURS = 4 * 60 * 60 #: Integer
29
29
 
30
- sig { params(project_path: String, options: T.untyped).void }
30
+ #: (String project_path, **untyped options) -> void
31
31
  def initialize(project_path, **options)
32
32
  @project_path = project_path
33
- @branch = T.let(options[:branch], T.nilable(String))
34
- @launcher = T.let(options[:launcher], T.nilable(T::Boolean))
33
+ @branch = options[:branch] #: String?
34
+ @launcher = options[:launcher] #: bool?
35
35
  patch_thor_to_print_progress_to_stderr! if @launcher
36
36
 
37
37
  # Regular bundle paths
38
- @gemfile = T.let(
39
- begin
40
- Bundler.default_gemfile
41
- rescue Bundler::GemfileNotFound
42
- nil
43
- end,
44
- T.nilable(Pathname),
45
- )
46
- @lockfile = T.let(@gemfile ? Bundler.default_lockfile : nil, T.nilable(Pathname))
38
+ @gemfile = begin
39
+ Bundler.default_gemfile
40
+ rescue Bundler::GemfileNotFound
41
+ nil
42
+ end #: Pathname?
43
+ @lockfile = @gemfile ? Bundler.default_lockfile : nil #: Pathname?
47
44
 
48
- @gemfile_hash = T.let(@gemfile ? Digest::SHA256.hexdigest(@gemfile.read) : nil, T.nilable(String))
49
- @lockfile_hash = T.let(@lockfile&.exist? ? Digest::SHA256.hexdigest(@lockfile.read) : nil, T.nilable(String))
45
+ @gemfile_hash = @gemfile ? Digest::SHA256.hexdigest(@gemfile.read) : nil #: String?
46
+ @lockfile_hash = @lockfile&.exist? ? Digest::SHA256.hexdigest(@lockfile.read) : nil #: String?
50
47
 
51
- @gemfile_name = T.let(@gemfile&.basename&.to_s || "Gemfile", String)
48
+ @gemfile_name = @gemfile&.basename&.to_s || "Gemfile" #: String
52
49
 
53
50
  # Custom bundle paths
54
- @custom_dir = T.let(Pathname.new(".ruby-lsp").expand_path(@project_path), Pathname)
55
- @custom_gemfile = T.let(@custom_dir + @gemfile_name, Pathname)
56
- @custom_lockfile = T.let(@custom_dir + (@lockfile&.basename || "Gemfile.lock"), Pathname)
57
- @lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
58
- @last_updated_path = T.let(@custom_dir + "last_updated", Pathname)
59
- @error_path = T.let(@custom_dir + "install_error", Pathname)
60
- @already_composed_path = T.let(@custom_dir + "bundle_is_composed", Pathname)
51
+ @custom_dir = Pathname.new(".ruby-lsp").expand_path(@project_path) #: Pathname
52
+ @custom_gemfile = @custom_dir + @gemfile_name #: Pathname
53
+ @custom_lockfile = @custom_dir + (@lockfile&.basename || "Gemfile.lock") #: Pathname
54
+ @lockfile_hash_path = @custom_dir + "main_lockfile_hash" #: Pathname
55
+ @last_updated_path = @custom_dir + "last_updated" #: Pathname
56
+ @error_path = @custom_dir + "install_error" #: Pathname
57
+ @already_composed_path = @custom_dir + "bundle_is_composed" #: Pathname
61
58
 
62
59
  dependencies, bundler_version = load_dependencies
63
- @dependencies = T.let(dependencies, T::Hash[String, T.untyped])
64
- @bundler_version = T.let(bundler_version, T.nilable(Gem::Version))
65
- @rails_app = T.let(rails_app?, T::Boolean)
66
- @retry = T.let(false, T::Boolean)
60
+ @dependencies = dependencies #: Hash[String, untyped]
61
+ @bundler_version = bundler_version #: Gem::Version?
62
+ @rails_app = rails_app? #: bool
63
+ @retry = false #: bool
67
64
  end
68
65
 
69
66
  # Sets up the composed bundle and returns the `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_APP_CONFIG` that should be
70
67
  # used for running the server
71
- sig { returns(T::Hash[String, String]) }
68
+ #: -> Hash[String, String]
72
69
  def setup!
73
70
  raise BundleNotLocked if !@launcher && @gemfile&.exist? && !@lockfile&.exist?
74
71
 
@@ -128,26 +125,23 @@ module RubyLsp
128
125
 
129
126
  private
130
127
 
131
- sig { returns(T::Hash[String, T.untyped]) }
128
+ #: -> Hash[String, untyped]
132
129
  def composed_bundle_dependencies
133
- @composed_bundle_dependencies ||= T.let(
134
- begin
135
- original_bundle_gemfile = ENV["BUNDLE_GEMFILE"]
136
-
137
- if @custom_lockfile.exist?
138
- ENV["BUNDLE_GEMFILE"] = @custom_gemfile.to_s
139
- Bundler::LockfileParser.new(@custom_lockfile.read).dependencies
140
- else
141
- {}
142
- end
143
- ensure
144
- ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile
145
- end,
146
- T.nilable(T::Hash[String, T.untyped]),
147
- )
130
+ @composed_bundle_dependencies ||= begin
131
+ original_bundle_gemfile = ENV["BUNDLE_GEMFILE"]
132
+
133
+ if @custom_lockfile.exist?
134
+ ENV["BUNDLE_GEMFILE"] = @custom_gemfile.to_s
135
+ Bundler::LockfileParser.new(@custom_lockfile.read).dependencies
136
+ else
137
+ {}
138
+ end
139
+ ensure
140
+ ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile
141
+ end #: Hash[String, untyped]?
148
142
  end
149
143
 
150
- sig { void }
144
+ #: -> void
151
145
  def write_custom_gemfile
152
146
  parts = [
153
147
  "# This custom gemfile is automatically generated by the Ruby LSP.",
@@ -188,7 +182,7 @@ module RubyLsp
188
182
  @custom_gemfile.write(content) unless @custom_gemfile.exist? && @custom_gemfile.read == content
189
183
  end
190
184
 
191
- sig { returns([T::Hash[String, T.untyped], T.nilable(Gem::Version)]) }
185
+ #: -> [Hash[String, untyped], Gem::Version?]
192
186
  def load_dependencies
193
187
  return [{}, nil] unless @lockfile&.exist?
194
188
 
@@ -208,7 +202,7 @@ module RubyLsp
208
202
  [dependencies, lockfile_parser.bundler_version]
209
203
  end
210
204
 
211
- sig { params(bundle_gemfile: T.nilable(Pathname)).returns(T::Hash[String, String]) }
205
+ #: (?Pathname? bundle_gemfile) -> Hash[String, String]
212
206
  def run_bundle_install(bundle_gemfile = @gemfile)
213
207
  env = bundler_settings_as_env
214
208
  env["BUNDLE_GEMFILE"] = bundle_gemfile.to_s
@@ -259,17 +253,18 @@ module RubyLsp
259
253
  env
260
254
  end
261
255
 
262
- sig { params(env: T::Hash[String, String], force_install: T::Boolean).returns(T::Hash[String, String]) }
256
+ #: (Hash[String, String] env, ?force_install: bool) -> Hash[String, String]
263
257
  def run_bundle_install_directly(env, force_install: false)
264
258
  RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
265
259
 
266
260
  # The ENV can only be merged after checking if an update is required because we depend on the original value of
267
261
  # ENV["BUNDLE_GEMFILE"], which gets overridden after the merge
268
262
  should_update = should_bundle_update?
269
- T.unsafe(ENV).merge!(env)
263
+ ENV #: as untyped
264
+ .merge!(env)
270
265
 
271
266
  unless should_update && !force_install
272
- Bundler::CLI::Install.new({}).run
267
+ Bundler::CLI::Install.new({ "no-cache" => true }).run
273
268
  correct_relative_remote_paths if @custom_lockfile.exist?
274
269
  return env
275
270
  end
@@ -288,7 +283,7 @@ module RubyLsp
288
283
  @retry ? env : run_bundle_install_directly(env, force_install: true)
289
284
  end
290
285
 
291
- sig { params(env: T::Hash[String, String]).returns(T::Hash[String, String]) }
286
+ #: (Hash[String, String] env) -> Hash[String, String]
292
287
  def run_bundle_install_through_command(env)
293
288
  # If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
294
289
  # to upgrade them or else we'll produce undesired source control changes. If the composed bundle was just created
@@ -345,7 +340,7 @@ module RubyLsp
345
340
  end
346
341
 
347
342
  # Gather all Bundler settings (global and local) and return them as a hash that can be used as the environment
348
- sig { returns(T::Hash[String, String]) }
343
+ #: -> Hash[String, String]
349
344
  def bundler_settings_as_env
350
345
  local_config_path = File.join(@project_path, ".bundle")
351
346
 
@@ -357,10 +352,17 @@ module RubyLsp
357
352
  Bundler::Settings.new
358
353
  end
359
354
 
355
+ # List of Bundler settings that don't make sense for the composed bundle and are better controlled manually by the
356
+ # user
357
+ ignored_settings = ["bin", "cache_all", "cache_all_platforms"]
358
+
360
359
  # Map all settings to their environment variable names with `key_for` and their values. For example, the if the
361
360
  # setting name `e` is `path` with a value of `vendor/bundle`, then it will return `"BUNDLE_PATH" =>
362
361
  # "vendor/bundle"`
363
- settings.all.to_h do |e|
362
+ settings
363
+ .all
364
+ .reject { |setting| ignored_settings.include?(setting) }
365
+ .to_h do |e|
364
366
  key = settings.key_for(e)
365
367
  value = Array(settings[e]).join(":").tr(" ", ":")
366
368
 
@@ -368,7 +370,7 @@ module RubyLsp
368
370
  end
369
371
  end
370
372
 
371
- sig { void }
373
+ #: -> void
372
374
  def install_bundler_if_needed
373
375
  # Try to find the bundler version specified in the lockfile in installed gems. If not found, install it
374
376
  requirement = Gem::Requirement.new(@bundler_version.to_s)
@@ -377,7 +379,7 @@ module RubyLsp
377
379
  Gem.install("bundler", @bundler_version.to_s)
378
380
  end
379
381
 
380
- sig { returns(T::Boolean) }
382
+ #: -> bool
381
383
  def should_bundle_update?
382
384
  # If `ruby-lsp`, `ruby-lsp-rails` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it
383
385
  # will produce version control changes
@@ -400,16 +402,19 @@ module RubyLsp
400
402
 
401
403
  # When a lockfile has remote references based on relative file paths, we need to ensure that they are pointing to
402
404
  # the correct place since after copying the relative path is no longer valid
403
- sig { void }
405
+ #: -> void
404
406
  def correct_relative_remote_paths
405
407
  content = @custom_lockfile.read
406
408
  content.gsub!(/remote: (.*)/) do |match|
407
- path = T.must(Regexp.last_match)[1]
409
+ last_match = Regexp.last_match #: as !nil
410
+ path = last_match[1]
408
411
 
409
412
  # We should only apply the correction if the remote is a relative path. It might also be a URI, like
410
413
  # `https://rubygems.org` or an absolute path, in which case we shouldn't do anything
411
414
  if path && !URI(path).scheme
412
- "remote: #{File.expand_path(path, T.must(@gemfile).dirname)}"
415
+ bundle_dir = @gemfile #: as !nil
416
+ .dirname
417
+ "remote: #{File.expand_path(path, bundle_dir)}"
413
418
  else
414
419
  match
415
420
  end
@@ -422,7 +427,7 @@ module RubyLsp
422
427
  end
423
428
 
424
429
  # Detects if the project is a Rails app by looking if the superclass of the main class is `Rails::Application`
425
- sig { returns(T::Boolean) }
430
+ #: -> bool
426
431
  def rails_app?
427
432
  config = Pathname.new("config/application.rb").expand_path
428
433
  application_contents = config.read(external_encoding: Encoding::UTF_8) if config.exist?
@@ -431,7 +436,7 @@ module RubyLsp
431
436
  /class .* < (::)?Rails::Application/.match?(application_contents)
432
437
  end
433
438
 
434
- sig { void }
439
+ #: -> void
435
440
  def patch_thor_to_print_progress_to_stderr!
436
441
  return unless defined?(Bundler::Thor::Shell::Basic)
437
442
 
@@ -3,13 +3,17 @@
3
3
 
4
4
  module RubyLsp
5
5
  # The path to the `static_docs` directory, where we keep long-form static documentation
6
- STATIC_DOCS_PATH = T.let(File.join(File.dirname(File.dirname(T.must(__dir__))), "static_docs"), String)
6
+ STATIC_DOCS_PATH = File.join(
7
+ File.dirname(
8
+ File.dirname(
9
+ __dir__, #: as !nil
10
+ ),
11
+ ),
12
+ "static_docs",
13
+ ) #: String
7
14
 
8
15
  # A map of keyword => short documentation to be displayed on hover or completion
9
- KEYWORD_DOCS = T.let(
10
- {
11
- "yield" => "Invokes the passed block with the given arguments",
12
- }.freeze,
13
- T::Hash[String, String],
14
- )
16
+ KEYWORD_DOCS = {
17
+ "yield" => "Invokes the passed block with the given arguments",
18
+ }.freeze #: Hash[String, String]
15
19
  end
@@ -3,34 +3,29 @@
3
3
 
4
4
  module RubyLsp
5
5
  class Store
6
- extend T::Sig
7
-
8
6
  class NonExistingDocumentError < StandardError; end
9
7
 
10
- sig { returns(T::Hash[Symbol, RequestConfig]) }
8
+ #: Hash[Symbol, RequestConfig]
11
9
  attr_accessor :features_configuration
12
10
 
13
- sig { returns(String) }
11
+ #: String
14
12
  attr_accessor :client_name
15
13
 
16
- sig { params(global_state: GlobalState).void }
14
+ #: (GlobalState global_state) -> void
17
15
  def initialize(global_state)
18
16
  @global_state = global_state
19
- @state = T.let({}, T::Hash[String, Document[T.untyped]])
20
- @features_configuration = T.let(
21
- {
22
- inlayHint: RequestConfig.new({
23
- enableAll: false,
24
- implicitRescue: false,
25
- implicitHashValue: false,
26
- }),
27
- },
28
- T::Hash[Symbol, RequestConfig],
29
- )
30
- @client_name = T.let("Unknown", String)
17
+ @state = {} #: Hash[String, Document[untyped]]
18
+ @features_configuration = {
19
+ inlayHint: RequestConfig.new({
20
+ enableAll: false,
21
+ implicitRescue: false,
22
+ implicitHashValue: false,
23
+ }),
24
+ } #: Hash[Symbol, RequestConfig]
25
+ @client_name = "Unknown" #: String
31
26
  end
32
27
 
33
- sig { params(uri: URI::Generic).returns(Document[T.untyped]) }
28
+ #: (URI::Generic uri) -> Document[untyped]
34
29
  def get(uri)
35
30
  document = @state[uri.to_s]
36
31
  return document unless document.nil?
@@ -51,19 +46,12 @@ module RubyLsp
51
46
  end
52
47
 
53
48
  set(uri: uri, source: File.binread(path), version: 0, language_id: language_id)
54
- T.must(@state[uri.to_s])
49
+ @state[uri.to_s] #: as !nil
55
50
  rescue Errno::ENOENT
56
51
  raise NonExistingDocumentError, uri.to_s
57
52
  end
58
53
 
59
- sig do
60
- params(
61
- uri: URI::Generic,
62
- source: String,
63
- version: Integer,
64
- language_id: Document::LanguageId,
65
- ).returns(Document[T.untyped])
66
- end
54
+ #: (uri: URI::Generic, source: String, version: Integer, language_id: Document::LanguageId) -> Document[untyped]
67
55
  def set(uri:, source:, version:, language_id:)
68
56
  @state[uri.to_s] = case language_id
69
57
  when Document::LanguageId::ERB
@@ -75,46 +63,40 @@ module RubyLsp
75
63
  end
76
64
  end
77
65
 
78
- sig { params(uri: URI::Generic, edits: T::Array[T::Hash[Symbol, T.untyped]], version: Integer).void }
66
+ #: (uri: URI::Generic, edits: Array[Hash[Symbol, untyped]], version: Integer) -> void
79
67
  def push_edits(uri:, edits:, version:)
80
- T.must(@state[uri.to_s]).push_edits(edits, version: version)
68
+ @state[uri.to_s] #: as !nil
69
+ .push_edits(edits, version: version)
81
70
  end
82
71
 
83
- sig { void }
72
+ #: -> void
84
73
  def clear
85
74
  @state.clear
86
75
  end
87
76
 
88
- sig { returns(T::Boolean) }
77
+ #: -> bool
89
78
  def empty?
90
79
  @state.empty?
91
80
  end
92
81
 
93
- sig { params(uri: URI::Generic).void }
82
+ #: (URI::Generic uri) -> void
94
83
  def delete(uri)
95
84
  @state.delete(uri.to_s)
96
85
  end
97
86
 
98
- sig { params(uri: URI::Generic).returns(T::Boolean) }
87
+ #: (URI::Generic uri) -> bool
99
88
  def key?(uri)
100
89
  @state.key?(uri.to_s)
101
90
  end
102
91
 
103
- sig { params(block: T.proc.params(uri: String, document: Document[T.untyped]).void).void }
92
+ #: { (String uri, Document[untyped] document) -> void } -> void
104
93
  def each(&block)
105
94
  @state.each do |uri, document|
106
95
  block.call(uri, document)
107
96
  end
108
97
  end
109
98
 
110
- sig do
111
- type_parameters(:T)
112
- .params(
113
- uri: URI::Generic,
114
- request_name: String,
115
- block: T.proc.params(document: Document[T.untyped]).returns(T.type_parameter(:T)),
116
- ).returns(T.type_parameter(:T))
117
- end
99
+ #: [T] (URI::Generic uri, String request_name) { (Document[untyped] document) -> T } -> T
118
100
  def cache_fetch(uri, request_name, &block)
119
101
  get(uri).cache_fetch(request_name, &block)
120
102
  end
@@ -7,21 +7,11 @@ module RubyLsp
7
7
  module TestHelper
8
8
  class TestError < StandardError; end
9
9
 
10
- extend T::Sig
11
10
  extend T::Helpers
12
11
 
13
12
  requires_ancestor { Kernel }
14
13
 
15
- sig do
16
- type_parameters(:T)
17
- .params(
18
- source: T.nilable(String),
19
- uri: URI::Generic,
20
- stub_no_typechecker: T::Boolean,
21
- load_addons: T::Boolean,
22
- block: T.proc.params(server: RubyLsp::Server, uri: URI::Generic).returns(T.type_parameter(:T)),
23
- ).returns(T.type_parameter(:T))
24
- end
14
+ #: [T] (?String? source, ?URI::Generic uri, ?stub_no_typechecker: bool, ?load_addons: bool) { (RubyLsp::Server server, URI::Generic uri) -> T } -> T
25
15
  def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typechecker: false, load_addons: true,
26
16
  &block)
27
17
  server = RubyLsp::Server.new(test_mode: true)
@@ -58,7 +48,7 @@ module RubyLsp
58
48
  end
59
49
  end
60
50
 
61
- sig { params(server: RubyLsp::Server).returns(RubyLsp::Result) }
51
+ #: (RubyLsp::Server server) -> RubyLsp::Result
62
52
  def pop_result(server)
63
53
  result = server.pop_response
64
54
  result = server.pop_response until result.is_a?(RubyLsp::Result) || result.is_a?(RubyLsp::Error)
@@ -0,0 +1,164 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "socket"
6
+ require "singleton"
7
+
8
+ module RubyLsp
9
+ class LspReporter
10
+ include Singleton
11
+
12
+ #: -> void
13
+ def initialize
14
+ port = ENV["RUBY_LSP_REPORTER_PORT"]
15
+ @io = if port
16
+ TCPSocket.new("localhost", port)
17
+ else
18
+ # For tests that don't spawn the TCP server
19
+ require "stringio"
20
+ StringIO.new
21
+ end #: IO | StringIO
22
+ end
23
+
24
+ #: -> void
25
+ def shutdown
26
+ # When running in coverage mode, we don't want to inform the extension that we finished immediately after running
27
+ # tests. We only do it after we finish processing coverage results, by invoking `internal_shutdown`
28
+ return if ENV["RUBY_LSP_TEST_RUNNER"] == "coverage"
29
+
30
+ internal_shutdown
31
+ end
32
+
33
+ # This method is intended to be used by the RubyLsp::LspReporter class itself only. If you're writing a custom test
34
+ # reporter, use `shutdown` instead
35
+ #: -> void
36
+ def internal_shutdown
37
+ send_message("finish")
38
+ @io.close
39
+ end
40
+
41
+ #: (id: String, uri: URI::Generic) -> void
42
+ def start_test(id:, uri:)
43
+ send_message("start", id: id, uri: uri.to_s)
44
+ end
45
+
46
+ #: (id: String, uri: URI::Generic) -> void
47
+ def record_pass(id:, uri:)
48
+ send_message("pass", id: id, uri: uri.to_s)
49
+ end
50
+
51
+ #: (id: String, message: String, uri: URI::Generic) -> void
52
+ def record_fail(id:, message:, uri:)
53
+ send_message("fail", id: id, message: message, uri: uri.to_s)
54
+ end
55
+
56
+ #: (id: String, uri: URI::Generic) -> void
57
+ def record_skip(id:, uri:)
58
+ send_message("skip", id: id, uri: uri.to_s)
59
+ end
60
+
61
+ #: (id: String, message: String?, uri: URI::Generic) -> void
62
+ def record_error(id:, message:, uri:)
63
+ send_message("error", id: id, message: message, uri: uri.to_s)
64
+ end
65
+
66
+ # Gather the results returned by Coverage.result and format like the VS Code test explorer expects
67
+ #
68
+ # Coverage result format:
69
+ #
70
+ # Lines are reported in order as an array where each number is the number of times it was executed. For example,
71
+ # the following says that line 0 was executed 1 time and line 1 executed 3 times: [1, 3].
72
+ # Nil values represent lines for which coverage is not available, like empty lines, comments or keywords like
73
+ # `else`
74
+ #
75
+ # Branches are a hash containing the name of the branch and the location where it is found in tuples with the
76
+ # following elements: [NAME, ID, START_LINE, START_COLUMN, END_LINE, END_COLUMN] as the keys and the value is the
77
+ # number of times it was executed
78
+ #
79
+ # Methods are a similar hash [ClassName, :method_name, START_LINE, START_COLUMN, END_LINE, END_COLUMN] => NUMBER
80
+ # OF EXECUTIONS
81
+ #
82
+ # Example:
83
+ # {
84
+ # "file_path" => {
85
+ # "lines" => [1, 2, 3, nil],
86
+ # "branches" => {
87
+ # ["&.", 0, 6, 21, 6, 65] => { [:then, 1, 6, 21, 6, 65] => 0, [:else, 5, 7, 0, 7, 87] => 1 }
88
+ # },
89
+ # "methods" => {
90
+ # ["Foo", :bar, 6, 21, 6, 65] => 0
91
+ # }
92
+ # }
93
+ #: -> Hash[String, StatementCoverage]
94
+ def gather_coverage_results
95
+ # Ignore coverage results inside dependencies
96
+ bundle_path = Bundler.bundle_path.to_s
97
+ default_gems_path = File.dirname(RbConfig::CONFIG["rubylibdir"])
98
+
99
+ result = Coverage.result.reject do |file_path, _coverage_info|
100
+ file_path.start_with?(bundle_path, default_gems_path, "eval")
101
+ end
102
+
103
+ result.to_h do |file_path, coverage_info|
104
+ # Format the branch coverage information as VS Code expects it and then group it based on the start line of
105
+ # the conditional that causes the branching. We need to match each line coverage data with the branches that
106
+ # spawn from that line
107
+ branch_by_line = coverage_info[:branches]
108
+ .flat_map do |branch, data|
109
+ branch_name, _branch_id, branch_start_line, _branch_start_col, _branch_end_line, _branch_end_col = branch
110
+
111
+ data.map do |then_or_else, execution_count|
112
+ name, _id, start_line, start_column, end_line, end_column = then_or_else
113
+
114
+ {
115
+ groupingLine: branch_start_line,
116
+ executed: execution_count,
117
+ location: {
118
+ start: { line: start_line, character: start_column },
119
+ end: { line: end_line, character: end_column },
120
+ },
121
+ label: "#{branch_name} #{name}",
122
+ }
123
+ end
124
+ end
125
+ .group_by { |branch| branch[:groupingLine] }
126
+
127
+ # Format the line coverage information, gathering any branch coverage data associated with that line
128
+ data = coverage_info[:lines].filter_map.with_index do |execution_count, line_index|
129
+ next if execution_count.nil?
130
+
131
+ {
132
+ executed: execution_count,
133
+ location: { line: line_index, character: 0 },
134
+ branches: branch_by_line[line_index] || [],
135
+ }
136
+ end
137
+
138
+ # The expected format is URI => { executed: number_of_times_executed, location: { ... }, branches: [ ... ] }
139
+ [URI::Generic.from_path(path: File.expand_path(file_path)).to_s, data]
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ #: (method_name: String?, params: untyped) -> void
146
+ def send_message(method_name, **params)
147
+ json_message = { method: method_name, params: params }.to_json
148
+ @io.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
149
+ end
150
+ end
151
+ end
152
+
153
+ if ENV["RUBY_LSP_TEST_RUNNER"] == "coverage"
154
+ # Auto start coverage when running tests under that profile. This avoids the user from having to configure coverage
155
+ # manually for their project or adding extra dependencies
156
+ require "coverage"
157
+ Coverage.start(:all)
158
+
159
+ at_exit do
160
+ coverage_results = RubyLsp::LspReporter.instance.gather_coverage_results
161
+ File.write(File.join(".ruby-lsp", "coverage_result.json"), coverage_results.to_json)
162
+ RubyLsp::LspReporter.instance.internal_shutdown
163
+ end
164
+ end