ruby-lsp 0.23.14 → 0.23.15

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp-launcher +9 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +1 -1
  5. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +6 -3
  6. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +4 -2
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +59 -29
  8. data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +5 -4
  9. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +5 -1
  10. data/lib/ruby_indexer/test/class_variables_test.rb +14 -14
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +65 -40
  12. data/lib/ruby_indexer/test/configuration_test.rb +6 -4
  13. data/lib/ruby_indexer/test/constant_test.rb +34 -34
  14. data/lib/ruby_indexer/test/enhancements_test.rb +1 -1
  15. data/lib/ruby_indexer/test/index_test.rb +139 -135
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +37 -37
  17. data/lib/ruby_indexer/test/method_test.rb +118 -118
  18. data/lib/ruby_indexer/test/prefix_tree_test.rb +13 -13
  19. data/lib/ruby_indexer/test/rbs_indexer_test.rb +64 -70
  20. data/lib/ruby_indexer/test/test_case.rb +2 -2
  21. data/lib/ruby_lsp/erb_document.rb +12 -4
  22. data/lib/ruby_lsp/global_state.rb +1 -1
  23. data/lib/ruby_lsp/listeners/code_lens.rb +3 -3
  24. data/lib/ruby_lsp/listeners/completion.rb +24 -11
  25. data/lib/ruby_lsp/listeners/definition.rb +1 -1
  26. data/lib/ruby_lsp/listeners/document_link.rb +3 -1
  27. data/lib/ruby_lsp/listeners/document_symbol.rb +3 -3
  28. data/lib/ruby_lsp/listeners/folding_ranges.rb +8 -4
  29. data/lib/ruby_lsp/listeners/hover.rb +2 -2
  30. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +12 -5
  31. data/lib/ruby_lsp/listeners/signature_help.rb +5 -1
  32. data/lib/ruby_lsp/listeners/spec_style.rb +1 -1
  33. data/lib/ruby_lsp/listeners/test_style.rb +3 -3
  34. data/lib/ruby_lsp/requests/code_action_resolve.rb +4 -3
  35. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  36. data/lib/ruby_lsp/requests/hover.rb +2 -2
  37. data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -2
  38. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +1 -2
  39. data/lib/ruby_lsp/requests/references.rb +2 -1
  40. data/lib/ruby_lsp/requests/rename.rb +8 -5
  41. data/lib/ruby_lsp/requests/semantic_highlighting.rb +4 -4
  42. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -1
  43. data/lib/ruby_lsp/requests/support/common.rb +3 -1
  44. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
  45. data/lib/ruby_lsp/requests/support/source_uri.rb +1 -1
  46. data/lib/ruby_lsp/response_builders/document_symbol.rb +3 -2
  47. data/lib/ruby_lsp/response_builders/hover.rb +1 -1
  48. data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +1 -1
  49. data/lib/ruby_lsp/scripts/compose_bundle.rb +6 -4
  50. data/lib/ruby_lsp/server.rb +12 -4
  51. data/lib/ruby_lsp/setup_bundler.rb +5 -2
  52. data/lib/ruby_lsp/static_docs.rb +8 -1
  53. data/lib/ruby_lsp/store.rb +3 -2
  54. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +152 -0
  55. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +105 -0
  56. data/lib/ruby_lsp/test_reporters/test_unit_reporter.rb +94 -0
  57. data/lib/ruby_lsp/type_inferrer.rb +4 -1
  58. metadata +6 -6
  59. data/lib/ruby_lsp/ruby_lsp_reporter_plugin.rb +0 -109
  60. data/lib/ruby_lsp/test_reporter.rb +0 -207
  61. data/lib/ruby_lsp/test_unit_test_runner.rb +0 -98
@@ -299,7 +299,7 @@ module RubyLsp
299
299
  methods = @index.resolve_method(message, type.name, inherited_only: inherited_only)
300
300
  return unless methods
301
301
 
302
- first_method = T.must(methods.first)
302
+ first_method = methods.first #: as !nil
303
303
 
304
304
  title = "#{message}#{first_method.decorated_parameters}"
305
305
  title << first_method.formatted_signatures
@@ -365,7 +365,7 @@ module RubyLsp
365
365
 
366
366
  # We should only show hover for private constants if the constant is defined in the same namespace as the
367
367
  # reference
368
- first_entry = T.must(entries.first)
368
+ first_entry = entries.first #: as !nil
369
369
  return if first_entry.private? && first_entry.name != "#{@node_context.fully_qualified_name}::#{name}"
370
370
 
371
371
  categorized_markdown_from_index_entries(name, entries).each do |category, content|
@@ -67,12 +67,18 @@ module RubyLsp
67
67
  return if special_method?(message)
68
68
 
69
69
  if Requests::Support::Sorbet.annotation?(node)
70
- @response_builder.add_token(T.must(node.message_loc), :type)
70
+ @response_builder.add_token(
71
+ node.message_loc, #: as !nil
72
+ :type,
73
+ )
71
74
  elsif !node.receiver && !node.opening_loc
72
75
  # If the node has a receiver, then the syntax is not ambiguous and semantic highlighting is not necessary to
73
76
  # determine that the token is a method call. The only ambiguous case is method calls with implicit self
74
77
  # receiver and no parenthesis, which may be confused with local variables
75
- @response_builder.add_token(T.must(node.message_loc), :method)
78
+ @response_builder.add_token(
79
+ node.message_loc, #: as !nil
80
+ :method,
81
+ )
76
82
  end
77
83
  end
78
84
 
@@ -98,7 +104,7 @@ module RubyLsp
98
104
 
99
105
  #: (Prism::DefNode node) -> void
100
106
  def on_def_node_leave(node)
101
- @current_scope = T.must(@current_scope.parent)
107
+ @current_scope = @current_scope.parent #: as !nil
102
108
  end
103
109
 
104
110
  #: (Prism::BlockNode node) -> void
@@ -108,7 +114,7 @@ module RubyLsp
108
114
 
109
115
  #: (Prism::BlockNode node) -> void
110
116
  def on_block_node_leave(node)
111
- @current_scope = T.must(@current_scope.parent)
117
+ @current_scope = @current_scope.parent #: as !nil
112
118
  end
113
119
 
114
120
  #: (Prism::BlockLocalVariableNode node) -> void
@@ -301,7 +307,8 @@ module RubyLsp
301
307
  # For each capture name we find in the regexp, look for a local in the current_scope
302
308
  Regexp.new(content, Regexp::FIXEDENCODING).names.each do |name|
303
309
  # The +3 is to compensate for the "(?<" part of the capture name
304
- capture_name_offset = T.must(content.index("(?<#{name}>")) + 3
310
+ capture_name_index = content.index("(?<#{name}>") #: as !nil
311
+ capture_name_offset = capture_name_index + 3
305
312
  local_var_loc = loc.copy(start_offset: loc.start_offset + capture_name_offset, length: name.length)
306
313
 
307
314
  local = @current_scope.lookup(name)
@@ -69,7 +69,11 @@ module RubyLsp
69
69
  signature.matches?(arguments)
70
70
  end || 0
71
71
 
72
- parameter_length = [T.must(signatures[active_sig_index]).parameters.length - 1, 0].max
72
+ parameter_length = [
73
+ signatures[active_sig_index] #: as !nil
74
+ .parameters.length - 1,
75
+ 0,
76
+ ].max
73
77
  active_parameter = (arguments.length - 1).clamp(0, parameter_length)
74
78
 
75
79
  # If there are arguments, then we need to check if there's a trailing comma after the end of the last argument
@@ -148,7 +148,7 @@ module RubyLsp
148
148
  def in_spec_context?
149
149
  return true if @nesting.empty?
150
150
 
151
- T.must(@spec_class_stack.last)
151
+ @spec_class_stack.last #: as !nil
152
152
  end
153
153
  end
154
154
  end
@@ -22,7 +22,7 @@ module RubyLsp
22
22
  queue = items.dup
23
23
 
24
24
  until queue.empty?
25
- item = T.must(queue.shift)
25
+ item = queue.shift #: as !nil
26
26
  tags = Set.new(item[:tags])
27
27
  next unless tags.include?("framework:minitest") || tags.include?("framework:test_unit")
28
28
 
@@ -133,8 +133,8 @@ module RubyLsp
133
133
 
134
134
  include Requests::Support::Common
135
135
 
136
- MINITEST_REPORTER_PATH = File.expand_path("../ruby_lsp_reporter_plugin.rb", __dir__) #: String
137
- TEST_UNIT_REPORTER_PATH = File.expand_path("../test_unit_test_runner.rb", __dir__) #: String
136
+ MINITEST_REPORTER_PATH = File.expand_path("../test_reporters/minitest_reporter.rb", __dir__) #: String
137
+ TEST_UNIT_REPORTER_PATH = File.expand_path("../test_reporters/test_unit_reporter.rb", __dir__) #: String
138
138
  ACCESS_MODIFIERS = [:public, :private, :protected].freeze
139
139
  BASE_COMMAND = begin
140
140
  Bundler.with_original_env { Bundler.default_lockfile }
@@ -97,7 +97,7 @@ module RubyLsp
97
97
  return Error::EmptySelection if source_range[:start] == source_range[:end]
98
98
 
99
99
  start_index, end_index = @document.find_index_by_position(source_range[:start], source_range[:end])
100
- extracted_source = T.must(@document.source[start_index...end_index])
100
+ extracted_source = @document.source[start_index...end_index] #: as !nil
101
101
 
102
102
  # Find the closest statements node, so that we place the refactor in a valid position
103
103
  node_context = RubyDocument
@@ -153,7 +153,8 @@ module RubyLsp
153
153
  indentation_line = lines[indentation_line_number]
154
154
  return Error::InvalidTargetRange unless indentation_line
155
155
 
156
- indentation = T.must(indentation_line[/\A */]).size
156
+ indentation = indentation_line[/\A */] #: as !nil
157
+ .size
157
158
 
158
159
  target_range = {
159
160
  start: { line: target_line, character: indentation },
@@ -195,7 +196,7 @@ module RubyLsp
195
196
  return Error::EmptySelection if source_range[:start] == source_range[:end]
196
197
 
197
198
  start_index, end_index = @document.find_index_by_position(source_range[:start], source_range[:end])
198
- extracted_source = T.must(@document.source[start_index...end_index])
199
+ extracted_source = @document.source[start_index...end_index] #: as !nil
199
200
 
200
201
  # Find the closest method declaration node, so that we place the refactor in a valid position
201
202
  node_context = RubyDocument.locate(
@@ -54,7 +54,7 @@ module RubyLsp
54
54
  end
55
55
  end
56
56
 
57
- first_entry = T.must(entries.first)
57
+ first_entry = entries.first #: as !nil
58
58
 
59
59
  if first_entry.is_a?(RubyIndexer::Entry::Member)
60
60
  label = +"#{label}#{first_entry.decorated_parameters}"
@@ -37,8 +37,8 @@ module RubyLsp
37
37
 
38
38
  if should_refine_target?(parent, target)
39
39
  target = determine_target(
40
- T.must(target),
41
- T.must(parent),
40
+ target, #: as !nil
41
+ parent, #: as !nil
42
42
  position,
43
43
  )
44
44
  elsif position_outside_target?(position, target)
@@ -51,7 +51,9 @@ module RubyLsp
51
51
  # But if it's a RBS signature starting with `#:`, we'll ignore it
52
52
  # so users can immediately continue typing the method definition
53
53
  if (comment_match = @previous_line.match(/^#(?!:)(\s*)/))
54
- handle_comment_line(T.must(comment_match[1]))
54
+ handle_comment_line(
55
+ comment_match[1], #: as !nil
56
+ )
55
57
  elsif @document.syntax_error?
56
58
  match = /(<<((-|~)?))(?<quote>['"`]?)(?<delimiter>\w+)\k<quote>/.match(@previous_line)
57
59
  heredoc_delimiter = match && match.named_captures["delimiter"]
@@ -76,7 +78,7 @@ module RubyLsp
76
78
  current_line = @lines[@position[:line]]
77
79
  return unless /((?<=do)|(?<={))\s+\|/.match?(current_line)
78
80
 
79
- line = T.must(current_line)
81
+ line = current_line #: as !nil
80
82
 
81
83
  # If the user inserts the closing pipe manually to the end of the block argument, we need to avoid adding
82
84
  # an additional one and remove the previous one. This also helps to remove the user who accidentally
@@ -49,8 +49,7 @@ module RubyLsp
49
49
 
50
50
  # While the spec allows for multiple entries, VSCode seems to only support one
51
51
  # We'll just return the first one for now
52
- first_entry = T.must(entries.first)
53
-
52
+ first_entry = entries.first #: as !nil
54
53
  range = range_from_location(first_entry.location)
55
54
 
56
55
  [
@@ -106,7 +106,8 @@ module RubyLsp
106
106
  entries = @global_state.index.resolve(name, node_context.nesting)
107
107
  return unless entries
108
108
 
109
- fully_qualified_name = T.must(entries.first).name
109
+ fully_qualified_name = entries.first #: as !nil
110
+ .name
110
111
  RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
111
112
  when
112
113
  Prism::InstanceVariableAndWriteNode,
@@ -63,10 +63,11 @@ module RubyLsp
63
63
  return unless entries
64
64
 
65
65
  if (conflict_entries = @global_state.index.resolve(@new_name, node_context.nesting))
66
- raise InvalidNameError, "The new name is already in use by #{T.must(conflict_entries.first).name}"
66
+ raise InvalidNameError, "The new name is already in use by #{conflict_entries.first&.name}"
67
67
  end
68
68
 
69
- fully_qualified_name = T.must(entries.first).name
69
+ fully_qualified_name = entries.first #: as !nil
70
+ .name
70
71
  reference_target = RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
71
72
  changes = collect_text_edits(reference_target, name)
72
73
 
@@ -97,9 +98,9 @@ module RubyLsp
97
98
  # rename the files for the user.
98
99
  #
99
100
  # We also look for an associated test file and rename it too
100
- short_name = T.must(fully_qualified_name.split("::").last)
101
+ short_name = fully_qualified_name.split("::").last #: as !nil
101
102
 
102
- T.must(@global_state.index[fully_qualified_name]).each do |entry|
103
+ @global_state.index[fully_qualified_name]&.each do |entry|
103
104
  # Do not rename files that are not part of the workspace
104
105
  uri = entry.uri
105
106
  file_path = uri.full_path
@@ -112,7 +113,9 @@ module RubyLsp
112
113
  file_name = file_from_constant_name(short_name)
113
114
 
114
115
  if "#{file_name}.rb" == entry.file_name
115
- new_file_name = file_from_constant_name(T.must(@new_name.split("::").last))
116
+ new_file_name = file_from_constant_name(
117
+ @new_name.split("::").last, #: as !nil
118
+ )
116
119
 
117
120
  new_uri = URI::Generic.from_path(path: File.join(
118
121
  File.dirname(file_path),
@@ -43,8 +43,8 @@ module RubyLsp
43
43
  # Filter the tokens based on the first different position. This must happen at this stage, before we try to
44
44
  # find the next position from the end or else we risk confusing sets of token that may have different lengths,
45
45
  # but end with the exact same token
46
- old_tokens = T.must(previous_tokens[first_different_position...])
47
- new_tokens = T.must(current_tokens[first_different_position...])
46
+ old_tokens = previous_tokens[first_different_position...] #: as !nil
47
+ new_tokens = current_tokens[first_different_position...] #: as !nil
48
48
 
49
49
  # Then search from the end to find the first token that doesn't match. Since the user is normally editing the
50
50
  # middle of the file, this will minimize the number of edits since the end of the token array has not changed
@@ -53,8 +53,8 @@ module RubyLsp
53
53
  end || 0
54
54
 
55
55
  # Filter the old and new tokens to only the section that will be replaced/inserted/deleted
56
- old_tokens = T.must(old_tokens[...old_tokens.length - first_different_token_from_end])
57
- new_tokens = T.must(new_tokens[...new_tokens.length - first_different_token_from_end])
56
+ old_tokens = old_tokens[...old_tokens.length - first_different_token_from_end] #: as !nil
57
+ new_tokens = new_tokens[...new_tokens.length - first_different_token_from_end] #: as !nil
58
58
 
59
59
  # And we send back a single edit, replacing an entire section for the new tokens
60
60
  Interface::SemanticTokensDelta.new(
@@ -29,7 +29,7 @@ module RubyLsp
29
29
 
30
30
  #: -> String
31
31
  def ast_for_range
32
- range = T.must(@range)
32
+ range = @range #: as !nil
33
33
  start_char, end_char = @document.find_index_by_position(range[:start], range[:end])
34
34
 
35
35
  queue = @tree.statements.body.dup
@@ -54,7 +54,9 @@ module RubyLsp
54
54
  #: (String file_path) -> bool?
55
55
  def not_in_dependencies?(file_path)
56
56
  BUNDLE_PATH &&
57
- !file_path.start_with?(T.must(BUNDLE_PATH)) &&
57
+ !file_path.start_with?(
58
+ BUNDLE_PATH, #: as !nil
59
+ ) &&
58
60
  !file_path.start_with?(RbConfig::CONFIG["rubylibdir"])
59
61
  end
60
62
 
@@ -21,7 +21,7 @@ module RubyLsp
21
21
  # @override
22
22
  #: (URI::Generic uri, RubyDocument document) -> String?
23
23
  def run_formatting(uri, document)
24
- filename = T.must(uri.to_standardized_path || uri.opaque)
24
+ filename = uri.to_standardized_path || uri.opaque #: as !nil
25
25
 
26
26
  # Invoke RuboCop with just this file in `paths`
27
27
  @format_runner.run(filename, document.source)
@@ -38,7 +38,7 @@ module RubyLsp
38
38
  # @override
39
39
  #: (URI::Generic uri, RubyDocument document) -> Array[Interface::Diagnostic]?
40
40
  def run_diagnostic(uri, document)
41
- filename = T.must(uri.to_standardized_path || uri.opaque)
41
+ filename = uri.to_standardized_path || uri.opaque #: as !nil
42
42
  # Invoke RuboCop with just this file in `paths`
43
43
  @diagnostic_runner.run(filename, document.source)
44
44
 
@@ -71,7 +71,7 @@ module URI
71
71
  if URI.respond_to?(:register_scheme)
72
72
  URI.register_scheme("SOURCE", self)
73
73
  else
74
- @@schemes = @@schemes # rubocop:disable Style/ClassVars #: Hash[String, untyped]
74
+ @@schemes = @@schemes #: Hash[String, untyped] # rubocop:disable Style/ClassVars
75
75
  @@schemes["SOURCE"] = self
76
76
  end
77
77
  end
@@ -38,13 +38,14 @@ module RubyLsp
38
38
 
39
39
  #: -> (SymbolHierarchyRoot | Interface::DocumentSymbol)
40
40
  def last
41
- T.must(@stack.last)
41
+ @stack.last #: as !nil
42
42
  end
43
43
 
44
44
  # @override
45
45
  #: -> Array[Interface::DocumentSymbol]
46
46
  def response
47
- T.must(@stack.first).children
47
+ @stack.first #: as !nil
48
+ .children
48
49
  end
49
50
  end
50
51
  end
@@ -35,7 +35,7 @@ module RubyLsp
35
35
  # @override
36
36
  #: -> ResponseType
37
37
  def response
38
- result = T.must(@response[:title])
38
+ result = @response[:title] #: as !nil
39
39
  result << "\n" << @response[:links] if @response[:links]
40
40
  result << "\n" << @response[:documentation] if @response[:documentation]
41
41
 
@@ -64,7 +64,7 @@ module RubyLsp
64
64
  start_line: location.start_line,
65
65
  start_code_unit_column: location.cached_start_code_units_column(@code_units_cache),
66
66
  length: length,
67
- type: T.must(TOKEN_TYPES[type]),
67
+ type: TOKEN_TYPES[type], #: as !nil
68
68
  modifier: modifiers_indices,
69
69
  ),
70
70
  )
@@ -13,8 +13,10 @@ def compose(raw_initialize)
13
13
  workspace_path ||= Dir.pwd
14
14
 
15
15
  env = RubyLsp::SetupBundler.new(workspace_path, launcher: true).setup!
16
- File.write(
17
- File.join(".ruby-lsp", "bundle_env"),
18
- env.map { |k, v| "#{k}=#{v}" }.join("\n"),
19
- )
16
+
17
+ File.open(File.join(".ruby-lsp", "bundle_env"), "w") do |f|
18
+ f.flock(File::LOCK_EX)
19
+ f.write(env.map { |k, v| "#{k}=#{v}" }.join("\n"))
20
+ f.flush
21
+ end
20
22
  end
@@ -223,7 +223,8 @@ module RubyLsp
223
223
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
224
224
 
225
225
  configured_hints = options.dig(:initializationOptions, :featuresConfiguration, :inlayHint)
226
- T.must(@store.features_configuration.dig(:inlayHint)).configuration.merge!(configured_hints) if configured_hints
226
+ @store.features_configuration.dig(:inlayHint) #: as !nil
227
+ .configuration.merge!(configured_hints) if configured_hints
227
228
 
228
229
  enabled_features = case configured_features
229
230
  when Array
@@ -294,6 +295,7 @@ module RubyLsp
294
295
  addon_detection: true,
295
296
  compose_bundle: true,
296
297
  go_to_relevant_file: true,
298
+ full_test_discovery: true,
297
299
  },
298
300
  ),
299
301
  serverInfo: {
@@ -488,7 +490,11 @@ module RubyLsp
488
490
  document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
489
491
  document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
490
492
  code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
491
- inlay_hint = Requests::InlayHints.new(document, T.must(@store.features_configuration.dig(:inlayHint)), dispatcher)
493
+ inlay_hint = Requests::InlayHints.new(
494
+ document,
495
+ @store.features_configuration.dig(:inlayHint), #: as !nil
496
+ dispatcher,
497
+ )
492
498
 
493
499
  if document.is_a?(RubyDocument) && document.should_index?
494
500
  # Re-index the file as it is modified. This mode of indexing updates entries only. Require path trees are only
@@ -815,7 +821,7 @@ module RubyLsp
815
821
  return
816
822
  end
817
823
 
818
- hints_configurations = T.must(@store.features_configuration.dig(:inlayHint))
824
+ hints_configurations = @store.features_configuration.dig(:inlayHint) #: as !nil
819
825
  dispatcher = Prism::Dispatcher.new
820
826
 
821
827
  unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
@@ -1393,7 +1399,9 @@ module RubyLsp
1393
1399
  Open3.capture3(
1394
1400
  Gem.ruby,
1395
1401
  "-I",
1396
- File.dirname(T.must(__dir__)),
1402
+ File.dirname(
1403
+ __dir__, #: as !nil
1404
+ ),
1397
1405
  File.expand_path("../../exe/ruby-lsp-launcher", __dir__),
1398
1406
  @global_state.workspace_uri.to_s,
1399
1407
  chdir: @global_state.workspace_path,
@@ -405,12 +405,15 @@ module RubyLsp
405
405
  def correct_relative_remote_paths
406
406
  content = @custom_lockfile.read
407
407
  content.gsub!(/remote: (.*)/) do |match|
408
- path = T.must(Regexp.last_match)[1]
408
+ last_match = Regexp.last_match #: as !nil
409
+ path = last_match[1]
409
410
 
410
411
  # We should only apply the correction if the remote is a relative path. It might also be a URI, like
411
412
  # `https://rubygems.org` or an absolute path, in which case we shouldn't do anything
412
413
  if path && !URI(path).scheme
413
- "remote: #{File.expand_path(path, T.must(@gemfile).dirname)}"
414
+ bundle_dir = @gemfile #: as !nil
415
+ .dirname
416
+ "remote: #{File.expand_path(path, bundle_dir)}"
414
417
  else
415
418
  match
416
419
  end
@@ -3,7 +3,14 @@
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 = 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
16
  KEYWORD_DOCS = {
@@ -46,7 +46,7 @@ module RubyLsp
46
46
  end
47
47
 
48
48
  set(uri: uri, source: File.binread(path), version: 0, language_id: language_id)
49
- T.must(@state[uri.to_s])
49
+ @state[uri.to_s] #: as !nil
50
50
  rescue Errno::ENOENT
51
51
  raise NonExistingDocumentError, uri.to_s
52
52
  end
@@ -65,7 +65,8 @@ module RubyLsp
65
65
 
66
66
  #: (uri: URI::Generic, edits: Array[Hash[Symbol, untyped]], version: Integer) -> void
67
67
  def push_edits(uri:, edits:, version:)
68
- T.must(@state[uri.to_s]).push_edits(edits, version: version)
68
+ @state[uri.to_s] #: as !nil
69
+ .push_edits(edits, version: version)
69
70
  end
70
71
 
71
72
  #: -> void
@@ -0,0 +1,152 @@
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
+ send_message("finish")
27
+ @io.close
28
+ end
29
+
30
+ #: (id: String, uri: URI::Generic) -> void
31
+ def start_test(id:, uri:)
32
+ send_message("start", id: id, uri: uri.to_s)
33
+ end
34
+
35
+ #: (id: String, uri: URI::Generic) -> void
36
+ def record_pass(id:, uri:)
37
+ send_message("pass", id: id, uri: uri.to_s)
38
+ end
39
+
40
+ #: (id: String, message: String, uri: URI::Generic) -> void
41
+ def record_fail(id:, message:, uri:)
42
+ send_message("fail", id: id, message: message, uri: uri.to_s)
43
+ end
44
+
45
+ #: (id: String, uri: URI::Generic) -> void
46
+ def record_skip(id:, uri:)
47
+ send_message("skip", id: id, uri: uri.to_s)
48
+ end
49
+
50
+ #: (id: String, message: String?, uri: URI::Generic) -> void
51
+ def record_error(id:, message:, uri:)
52
+ send_message("error", id: id, message: message, uri: uri.to_s)
53
+ end
54
+
55
+ # Gather the results returned by Coverage.result and format like the VS Code test explorer expects
56
+ #
57
+ # Coverage result format:
58
+ #
59
+ # Lines are reported in order as an array where each number is the number of times it was executed. For example,
60
+ # the following says that line 0 was executed 1 time and line 1 executed 3 times: [1, 3].
61
+ # Nil values represent lines for which coverage is not available, like empty lines, comments or keywords like
62
+ # `else`
63
+ #
64
+ # Branches are a hash containing the name of the branch and the location where it is found in tuples with the
65
+ # following elements: [NAME, ID, START_LINE, START_COLUMN, END_LINE, END_COLUMN] as the keys and the value is the
66
+ # number of times it was executed
67
+ #
68
+ # Methods are a similar hash [ClassName, :method_name, START_LINE, START_COLUMN, END_LINE, END_COLUMN] => NUMBER
69
+ # OF EXECUTIONS
70
+ #
71
+ # Example:
72
+ # {
73
+ # "file_path" => {
74
+ # "lines" => [1, 2, 3, nil],
75
+ # "branches" => {
76
+ # ["&.", 0, 6, 21, 6, 65] => { [:then, 1, 6, 21, 6, 65] => 0, [:else, 5, 7, 0, 7, 87] => 1 }
77
+ # },
78
+ # "methods" => {
79
+ # ["Foo", :bar, 6, 21, 6, 65] => 0
80
+ # }
81
+ # }
82
+ #: -> Hash[String, StatementCoverage]
83
+ def gather_coverage_results
84
+ # Ignore coverage results inside dependencies
85
+ bundle_path = Bundler.bundle_path.to_s
86
+ default_gems_path = File.dirname(RbConfig::CONFIG["rubylibdir"])
87
+
88
+ result = Coverage.result.reject do |file_path, _coverage_info|
89
+ file_path.start_with?(bundle_path, default_gems_path, "eval")
90
+ end
91
+
92
+ result.to_h do |file_path, coverage_info|
93
+ # Format the branch coverage information as VS Code expects it and then group it based on the start line of
94
+ # the conditional that causes the branching. We need to match each line coverage data with the branches that
95
+ # spawn from that line
96
+ branch_by_line = coverage_info[:branches]
97
+ .flat_map do |branch, data|
98
+ branch_name, _branch_id, branch_start_line, _branch_start_col, _branch_end_line, _branch_end_col = branch
99
+
100
+ data.map do |then_or_else, execution_count|
101
+ name, _id, start_line, start_column, end_line, end_column = then_or_else
102
+
103
+ {
104
+ groupingLine: branch_start_line,
105
+ executed: execution_count,
106
+ location: {
107
+ start: { line: start_line, character: start_column },
108
+ end: { line: end_line, character: end_column },
109
+ },
110
+ label: "#{branch_name} #{name}",
111
+ }
112
+ end
113
+ end
114
+ .group_by { |branch| branch[:groupingLine] }
115
+
116
+ # Format the line coverage information, gathering any branch coverage data associated with that line
117
+ data = coverage_info[:lines].filter_map.with_index do |execution_count, line_index|
118
+ next if execution_count.nil?
119
+
120
+ {
121
+ executed: execution_count,
122
+ location: { line: line_index, character: 0 },
123
+ branches: branch_by_line[line_index] || [],
124
+ }
125
+ end
126
+
127
+ # The expected format is URI => { executed: number_of_times_executed, location: { ... }, branches: [ ... ] }
128
+ [URI::Generic.from_path(path: File.expand_path(file_path)).to_s, data]
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ #: (method_name: String?, params: untyped) -> void
135
+ def send_message(method_name, **params)
136
+ json_message = { method: method_name, params: params }.to_json
137
+ @io.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
138
+ end
139
+ end
140
+ end
141
+
142
+ if ENV["RUBY_LSP_TEST_RUNNER"] == "coverage"
143
+ # Auto start coverage when running tests under that profile. This avoids the user from having to configure coverage
144
+ # manually for their project or adding extra dependencies
145
+ require "coverage"
146
+ Coverage.start(:all)
147
+
148
+ at_exit do
149
+ coverage_results = RubyLsp::LspReporter.instance.gather_coverage_results
150
+ File.write(File.join(".ruby-lsp", "coverage_result.json"), coverage_results.to_json)
151
+ end
152
+ end