ruby-lsp 0.23.20 → 0.23.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63ffbd4bd2ac09693c25c61a71ec89528578c3eaeaf35bf547e5ee748085f3d6
4
- data.tar.gz: f3390b3e9f7f86c3dcf98130c04d18a40720ab21a6b521035859bafdac67ae60
3
+ metadata.gz: 336c4740e2cc0c03bec77c7854065bc9115ed14ab88949d41ae8a8da7d58b1da
4
+ data.tar.gz: 0b1ccf86525c8db95beb310c23977353520e0951f7c627199db567c50b8b29f8
5
5
  SHA512:
6
- metadata.gz: 0232de2d936eaa8baa579293884fdb47c69d70be87b834b655292111b50ab9609f0d52d2a627a679483919116a6215df25c0966fc725b9c5a11a5b0160f12550
7
- data.tar.gz: 3c744d0e29846cc5bf4406a06675fb789d1d6efe959cf982fba2505ca58b151ea45b4c800aceedfd2b242d75e792f557d99ffea5108afdc3894c76d0885ea5ae
6
+ metadata.gz: ffb2a4605bf6c3b7e82dde8ca73cd624c22aa16a2cf91569b7b38d06a7d876a932023efa4a4dea1871ca0e5965a923be97b9e870f2fd9f0b36d70d354213e9ea
7
+ data.tar.gz: e488ffda15f9d7a0549294d70f607c09620144ff3a1fea3441d834c3c2db06cf691997005c357cffcf596b4e41e83599111c48da07d92ea26bde293ae6edde0a
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.23.20
1
+ 0.23.21
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Append to RUBYOPT the necessary requires to hook our custom test reporters so that results are automatically
5
+ # reflected in the test explorer
6
+ rubyopt = [
7
+ *ENV["RUBYOPT"],
8
+ "-rbundler/setup",
9
+ "-r#{File.expand_path("../lib/ruby_lsp/test_reporters/minitest_reporter", __dir__)}",
10
+ "-r#{File.expand_path("../lib/ruby_lsp/test_reporters/test_unit_reporter", __dir__)}",
11
+ ].join(" ")
12
+
13
+ # Replace this process with whatever command was passed. We only want to set RUBYOPT.
14
+ # The way you use this executable is by prefixing your test command with `ruby-lsp-test-exec`, like so:
15
+ # ruby-lsp-test-exec bundle exec ruby -Itest test/example_test.rb
16
+ # ruby-lsp-test-exec bundle exec ruby -Ispec spec/example_spec.rb
17
+ # ruby-lsp-test-exec bundle exec rspec spec/example_spec.rb
18
+ exec({ "RUBYOPT" => rubyopt }, *ARGV)
@@ -4,12 +4,24 @@
4
4
  module RubyLsp
5
5
  module Listeners
6
6
  class SpecStyle < TestDiscovery
7
+ class Group
8
+ #: String
9
+ attr_reader :id
10
+
11
+ #: (String) -> void
12
+ def initialize(id)
13
+ @id = id
14
+ end
15
+ end
16
+
17
+ class ClassGroup < Group; end
18
+ class DescribeGroup < Group; end
19
+
7
20
  #: (ResponseBuilders::TestCollection, GlobalState, Prism::Dispatcher, URI::Generic) -> void
8
21
  def initialize(response_builder, global_state, dispatcher, uri)
9
22
  super
10
23
 
11
- @describe_block_nesting = [] #: Array[String]
12
- @spec_class_stack = [] #: Array[bool]
24
+ @spec_group_id_stack = [] #: Array[Group?]
13
25
 
14
26
  dispatcher.register(
15
27
  self,
@@ -22,21 +34,21 @@ module RubyLsp
22
34
 
23
35
  #: (Prism::ClassNode) -> void
24
36
  def on_class_node_enter(node)
25
- with_test_ancestor_tracking(node) do |_, ancestors|
26
- is_spec = ancestors.include?("Minitest::Spec")
27
- @spec_class_stack.push(is_spec)
37
+ with_test_ancestor_tracking(node) do |name, ancestors|
38
+ @spec_group_id_stack << (ancestors.include?("Minitest::Spec") ? ClassGroup.new(name) : nil)
28
39
  end
29
40
  end
30
41
 
31
42
  #: (Prism::ClassNode) -> void
32
43
  def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
33
44
  super
34
-
35
- @spec_class_stack.pop
45
+ @spec_group_id_stack.pop
36
46
  end
37
47
 
38
48
  #: (Prism::CallNode) -> void
39
49
  def on_call_node_enter(node)
50
+ return unless in_spec_context?
51
+
40
52
  case node.name
41
53
  when :describe
42
54
  handle_describe(node)
@@ -49,84 +61,63 @@ module RubyLsp
49
61
  def on_call_node_leave(node)
50
62
  return unless node.name == :describe && !node.receiver
51
63
 
52
- @describe_block_nesting.pop
64
+ @spec_group_id_stack.pop
53
65
  end
54
66
 
55
67
  private
56
68
 
57
69
  #: (Prism::CallNode) -> void
58
70
  def handle_describe(node)
71
+ # Describes will include the nesting of all classes and all outer describes as part of its ID, unlike classes
72
+ # that ignore describes
59
73
  return if node.block.nil?
60
74
 
61
75
  description = extract_description(node)
62
76
  return unless description
63
77
 
64
- return unless in_spec_context?
65
-
66
- if @nesting.empty? && @describe_block_nesting.empty?
67
- test_item = Requests::Support::TestItem.new(
68
- description,
69
- description,
70
- @uri,
71
- range_from_node(node),
72
- framework: :minitest,
73
- )
74
- @response_builder.add(test_item)
75
- @response_builder.add_code_lens(test_item)
78
+ parent = latest_group
79
+ id = case parent
80
+ when Requests::Support::TestItem
81
+ "#{parent.id}::#{description}"
76
82
  else
77
- add_to_parent_test_group(description, node)
83
+ description
78
84
  end
79
85
 
80
- @describe_block_nesting << description
86
+ test_item = Requests::Support::TestItem.new(
87
+ id,
88
+ description,
89
+ @uri,
90
+ range_from_node(node),
91
+ framework: :minitest,
92
+ )
93
+
94
+ parent.add(test_item)
95
+ @response_builder.add_code_lens(test_item)
96
+ @spec_group_id_stack << DescribeGroup.new(id)
81
97
  end
82
98
 
83
99
  #: (Prism::CallNode) -> void
84
100
  def handle_example(node)
85
- return unless in_spec_context?
86
-
87
- return if @describe_block_nesting.empty? && @nesting.empty?
88
-
89
- description = extract_description(node)
90
- return unless description
101
+ # Minitest formats the descriptions into test method names by using the count of examples with the description
102
+ # We are not guaranteed to discover examples in the exact order using static analysis, so we use the line number
103
+ # instead. Note that anonymous examples mixed with meta-programming will not be handled correctly
104
+ description = extract_description(node) || "anonymous"
105
+ line = node.location.start_line - 1
106
+ parent = latest_group
107
+ return unless parent.is_a?(Requests::Support::TestItem)
91
108
 
92
- add_to_parent_test_group(description, node)
93
- end
94
-
95
- #: (String, Prism::CallNode) -> void
96
- def add_to_parent_test_group(description, node)
97
- parent_test_group = find_parent_test_group
98
- return unless parent_test_group
109
+ id = "#{parent.id}##{format("test_%04d_%s", line, description)}"
99
110
 
100
111
  test_item = Requests::Support::TestItem.new(
101
- description,
112
+ id,
102
113
  description,
103
114
  @uri,
104
115
  range_from_node(node),
105
116
  framework: :minitest,
106
117
  )
107
- parent_test_group.add(test_item)
108
- @response_builder.add_code_lens(test_item)
109
- end
110
-
111
- #: -> Requests::Support::TestItem?
112
- def find_parent_test_group
113
- root_group_name, nested_describe_groups = if @nesting.empty?
114
- [@describe_block_nesting.first, @describe_block_nesting[1..]]
115
- else
116
- [RubyIndexer::Index.actual_nesting(@nesting, nil).join("::"), @describe_block_nesting]
117
- end
118
- return unless root_group_name
119
-
120
- test_group = @response_builder[root_group_name] #: Requests::Support::TestItem?
121
- return unless test_group
122
-
123
- return test_group unless nested_describe_groups
124
118
 
125
- nested_describe_groups.each do |description|
126
- test_group = test_group[description]
127
- end
128
-
129
- test_group
119
+ parent.add(test_item)
120
+ @response_builder.add_code_lens(test_item)
130
121
  end
131
122
 
132
123
  #: (Prism::CallNode) -> String?
@@ -144,11 +135,36 @@ module RubyLsp
144
135
  end
145
136
  end
146
137
 
138
+ #: -> (Requests::Support::TestItem | ResponseBuilders::TestCollection)
139
+ def latest_group
140
+ return @response_builder if @spec_group_id_stack.compact.empty?
141
+
142
+ first_class_index = @spec_group_id_stack.rindex { |i| i.is_a?(ClassGroup) } || 0
143
+ first_class = @spec_group_id_stack[0] #: as !nil
144
+ item = @response_builder[first_class.id] #: as !nil
145
+
146
+ # Descend into child items from the beginning all the way to the latest class group, ignoring describes
147
+ @spec_group_id_stack[1..first_class_index] #: as !nil
148
+ .each do |group|
149
+ next unless group.is_a?(ClassGroup)
150
+
151
+ item = item[group.id] #: as !nil
152
+ end
153
+
154
+ # From the class forward, we must take describes into account
155
+ @spec_group_id_stack[first_class_index + 1..] #: as !nil
156
+ .each do |group|
157
+ next unless group
158
+
159
+ item = item[group.id] #: as !nil
160
+ end
161
+
162
+ item
163
+ end
164
+
147
165
  #: -> bool
148
166
  def in_spec_context?
149
- return true if @nesting.empty?
150
-
151
- @spec_class_stack.last #: as !nil
167
+ @nesting.empty? || @spec_group_id_stack.any? { |id| id }
152
168
  end
153
169
  end
154
170
  end
@@ -34,7 +34,7 @@ module RubyLsp
34
34
  if tags.include?("test_dir")
35
35
  if children.empty?
36
36
  full_files.concat(Dir.glob(
37
- "#{path}/**/{*_test,test_*}.rb",
37
+ "#{path}/**/{*_test,test_*,*_spec}.rb",
38
38
  File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
39
39
  ))
40
40
  end
@@ -74,7 +74,9 @@ module RubyLsp
74
74
  end
75
75
 
76
76
  unless full_files.empty?
77
- commands << "#{BASE_COMMAND} -Itest -e \"ARGV.each { |f| require f }\" #{full_files.join(" ")}"
77
+ specs, tests = full_files.partition { |path| spec?(path) }
78
+ commands << "#{BASE_COMMAND} -Itest -e \"ARGV.each { |f| require f }\" #{tests.join(" ")}" if tests.any?
79
+ commands << "#{BASE_COMMAND} -Ispec -e \"ARGV.each { |f| require f }\" #{specs.join(" ")}" if specs.any?
78
80
  end
79
81
 
80
82
  commands
@@ -82,6 +84,11 @@ module RubyLsp
82
84
 
83
85
  private
84
86
 
87
+ #: (String) -> bool
88
+ def spec?(path)
89
+ File.fnmatch?("**/spec/**/*_spec.rb", path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
90
+ end
91
+
85
92
  #: (String, Hash[String, Hash[Symbol, untyped]]) -> String
86
93
  def handle_minitest_groups(file_path, groups_and_examples)
87
94
  regexes = groups_and_examples.flat_map do |group, info|
@@ -105,7 +112,8 @@ module RubyLsp
105
112
  "(#{regexes.join("|")})"
106
113
  end
107
114
 
108
- "#{BASE_COMMAND} -Itest #{file_path} --name \"/#{regex}/\""
115
+ load_path = spec?(file_path) ? "-Ispec" : "-Itest"
116
+ "#{BASE_COMMAND} #{load_path} #{file_path} --name \"/#{regex}/\""
109
117
  end
110
118
 
111
119
  #: (String, Hash[String, Hash[Symbol, untyped]]) -> Array[String]
@@ -148,6 +156,7 @@ module RubyLsp
148
156
  super
149
157
 
150
158
  @framework = :minitest #: Symbol
159
+ @parent_stack = [@response_builder] #: Array[(Requests::Support::TestItem | ResponseBuilders::TestCollection)?]
151
160
 
152
161
  dispatcher.register(
153
162
  self,
@@ -173,12 +182,21 @@ module RubyLsp
173
182
  framework: @framework,
174
183
  )
175
184
 
176
- @response_builder.add(test_item)
185
+ last_test_group.add(test_item)
177
186
  @response_builder.add_code_lens(test_item)
187
+ @parent_stack << test_item
188
+ else
189
+ @parent_stack << nil
178
190
  end
179
191
  end
180
192
  end
181
193
 
194
+ #: (Prism::ClassNode node) -> void
195
+ def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
196
+ @parent_stack.pop
197
+ super
198
+ end
199
+
182
200
  #: (Prism::DefNode node) -> void
183
201
  def on_def_node_enter(node)
184
202
  return if @visibility_stack.last != :public
@@ -187,12 +205,8 @@ module RubyLsp
187
205
  return unless name.start_with?("test_")
188
206
 
189
207
  current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::")
190
-
191
- # If we're finding a test method, but for the wrong framework, then the group test item will not have been
192
- # previously pushed and thus we return early and avoid adding items for a framework this listener is not
193
- # interested in
194
- test_item = @response_builder[current_group_name]
195
- return unless test_item
208
+ parent = @parent_stack.last
209
+ return unless parent.is_a?(Requests::Support::TestItem)
196
210
 
197
211
  example_item = Requests::Support::TestItem.new(
198
212
  "#{current_group_name}##{name}",
@@ -201,7 +215,7 @@ module RubyLsp
201
215
  range_from_node(node),
202
216
  framework: @framework,
203
217
  )
204
- test_item.add(example_item)
218
+ parent.add(example_item)
205
219
  @response_builder.add_code_lens(example_item)
206
220
  end
207
221
 
@@ -224,6 +238,12 @@ module RubyLsp
224
238
 
225
239
  private
226
240
 
241
+ #: -> (Requests::Support::TestItem | ResponseBuilders::TestCollection)
242
+ def last_test_group
243
+ index = @parent_stack.rindex { |i| i } #: as !nil
244
+ @parent_stack[index] #: as Requests::Support::TestItem | ResponseBuilders::TestCollection
245
+ end
246
+
227
247
  #: (Array[String] attached_ancestors, String fully_qualified_name) -> bool
228
248
  def non_declarative_minitest?(attached_ancestors, fully_qualified_name)
229
249
  return false unless attached_ancestors.include?("Minitest::Test")
@@ -6,6 +6,7 @@ require "json"
6
6
  require "socket"
7
7
  require "singleton"
8
8
  require "tmpdir"
9
+ require_relative "../../ruby_indexer/lib/ruby_indexer/uri"
9
10
 
10
11
  module RubyLsp
11
12
  class LspReporter
@@ -19,15 +20,20 @@ module RubyLsp
19
20
  dir_path = File.join(Dir.tmpdir, "ruby-lsp")
20
21
  FileUtils.mkdir_p(dir_path)
21
22
 
22
- port_path = File.join(dir_path, "test_reporter_port")
23
+ # Remove in 1 month once updates have rolled out
24
+ legacy_port_path = File.join(dir_path, "test_reporter_port")
25
+ port_db_path = File.join(dir_path, "test_reporter_port_db.json")
23
26
  port = ENV["RUBY_LSP_REPORTER_PORT"]
24
27
 
25
28
  @io = begin
26
29
  # The environment variable is only used for tests. The extension always writes to the temporary file
27
30
  if port
28
31
  TCPSocket.new("localhost", port)
29
- elsif File.exist?(port_path)
30
- TCPSocket.new("localhost", File.read(port_path))
32
+ elsif File.exist?(port_db_path)
33
+ db = JSON.load_file(port_db_path)
34
+ TCPSocket.new("localhost", db[Dir.pwd])
35
+ elsif File.exist?(legacy_port_path)
36
+ TCPSocket.new("localhost", File.read(legacy_port_path))
31
37
  else
32
38
  # For tests that don't spawn the TCP server
33
39
  require "stringio"
@@ -8,7 +8,6 @@ rescue LoadError
8
8
  end
9
9
 
10
10
  require_relative "lsp_reporter"
11
- require "ruby_indexer/lib/ruby_indexer/uri"
12
11
 
13
12
  module RubyLsp
14
13
  # An override of the default progress reporter in Minitest to add color to the output
@@ -54,15 +53,19 @@ module RubyLsp
54
53
  uri, line = LspReporter.instance.uri_and_line_for(test_class.instance_method(method_name))
55
54
  return unless uri
56
55
 
57
- LspReporter.instance.start_test(id: "#{test_class.name}##{method_name}", uri: uri, line: line)
56
+ id = "#{test_class.name}##{handle_spec_test_id(method_name, line)}"
57
+ LspReporter.instance.start_test(id: id, uri: uri, line: line)
58
58
  end
59
59
 
60
60
  #: (Minitest::Result result) -> void
61
61
  def record(result)
62
- id = "#{result.klass}##{result.name}"
63
- file_path, _line = result.source_location
62
+ file_path, line = result.source_location
64
63
  return unless file_path
65
64
 
65
+ zero_based_line = line ? line - 1 : nil
66
+ name = handle_spec_test_id(result.name, zero_based_line)
67
+ id = "#{result.klass}##{name}"
68
+
66
69
  uri = URI::Generic.from_path(path: File.expand_path(file_path))
67
70
 
68
71
  if result.error?
@@ -82,6 +85,11 @@ module RubyLsp
82
85
  def report
83
86
  LspReporter.instance.shutdown
84
87
  end
88
+
89
+ #: (String, Integer?) -> String
90
+ def handle_spec_test_id(method_name, line)
91
+ method_name.gsub(/(?<=test_)\d{4}(?=_)/, format("%04d", line.to_s))
92
+ end
85
93
  end
86
94
  end
87
95
 
@@ -10,7 +10,6 @@ rescue LoadError
10
10
  end
11
11
 
12
12
  require_relative "lsp_reporter"
13
- require "ruby_indexer/lib/ruby_indexer/uri"
14
13
 
15
14
  module RubyLsp
16
15
  class TestUnitReporter < Test::Unit::UI::Console::TestRunner
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.20
4
+ version: 0.23.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
@@ -84,6 +84,7 @@ executables:
84
84
  - ruby-lsp
85
85
  - ruby-lsp-check
86
86
  - ruby-lsp-launcher
87
+ - ruby-lsp-test-exec
87
88
  extensions: []
88
89
  extra_rdoc_files: []
89
90
  files:
@@ -93,6 +94,7 @@ files:
93
94
  - exe/ruby-lsp
94
95
  - exe/ruby-lsp-check
95
96
  - exe/ruby-lsp-launcher
97
+ - exe/ruby-lsp-test-exec
96
98
  - lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb
97
99
  - lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb
98
100
  - lib/ruby-lsp.rb
@@ -228,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
228
230
  - !ruby/object:Gem::Version
229
231
  version: '0'
230
232
  requirements: []
231
- rubygems_version: 3.6.8
233
+ rubygems_version: 3.6.9
232
234
  specification_version: 4
233
235
  summary: An opinionated language server for Ruby
234
236
  test_files: []