ruby-lsp 0.14.5 → 0.14.6

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: '046081e0caf521f948f3952bd7a356ef6f0beb80c5a9ff72930a6d0c9fd8d9f4'
4
- data.tar.gz: e65b5224342c412a0b28198af0e56b17a421eaacc7748a17d2ed30890c0a2d86
3
+ metadata.gz: 7c364aee0d80e6187f66e8eb6d50d7cdd21ffb129e0a0b17c9dc7bd5d8a18cad
4
+ data.tar.gz: 65964ec1d89f10adf9bcbf113efed6d43a977b669abe51182e90dd9870032623
5
5
  SHA512:
6
- metadata.gz: 682219fbef2c6e1483c0e6b0c36bf2ce8c3bd15aed88e53ee1a05623f5569c7e52227300b1a6fb4968a6ba2b0844a7e985a0b134af9e9498036199673d660117
7
- data.tar.gz: a13eb33a1a41514819f4d0612a5d50eb170a0adb36158b799cebffae34781a5d41c1b07922f0bec8310cf6439ea8356305224bcf1de7423b96ff6e28c9e744c8
6
+ metadata.gz: 971af8a3c02903597143a8ec49915eead6bc0f65a0175e4a443e381d671dd4a88f776a26f62af1bd69ee7dc3f478b36bef122febbfaed7656907eb1a6a7a3354
7
+ data.tar.gz: 15148c1c3765390f9bb92dbafe7043ac15b22de6b5c415c424a063f4742026d92831d84bcde2013ab742c4a889dc16d666d4ec5d18863d0db8acf793ce87f0a6
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.5
1
+ 0.14.6
@@ -152,6 +152,8 @@ module RubyIndexer
152
152
  handle_attribute(node, reader: false, writer: true)
153
153
  when :attr_accessor
154
154
  handle_attribute(node, reader: true, writer: true)
155
+ when :include
156
+ handle_include(node)
155
157
  end
156
158
  end
157
159
 
@@ -350,5 +352,24 @@ module RubyIndexer
350
352
  @index << Entry::Accessor.new("#{name}=", @file_path, loc, comments, @current_owner) if writer
351
353
  end
352
354
  end
355
+
356
+ sig { params(node: Prism::CallNode).void }
357
+ def handle_include(node)
358
+ return unless @current_owner
359
+
360
+ arguments = node.arguments&.arguments
361
+ return unless arguments
362
+
363
+ names = arguments.filter_map do |node|
364
+ if node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
365
+ node.full_name
366
+ end
367
+ rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
368
+ # TO DO: add MissingNodesInConstantPathError when released in Prism
369
+ # If a constant path reference is dynamic or missing parts, we can't
370
+ # index it
371
+ end
372
+ @current_owner.included_modules.concat(names)
373
+ end
353
374
  end
354
375
  end
@@ -20,7 +20,7 @@ module RubyIndexer
20
20
  def initialize
21
21
  @excluded_gems = T.let(initial_excluded_gems, T::Array[String])
22
22
  @included_gems = T.let([], T::Array[String])
23
- @excluded_patterns = T.let([File.join("**", "*_test.rb")], T::Array[String])
23
+ @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("**", "tmp", "**", "*")], T::Array[String])
24
24
  path = Bundler.settings["path"]
25
25
  @excluded_patterns << File.join(File.expand_path(path, Dir.pwd), "**", "*.rb") if path
26
26
 
@@ -40,6 +40,22 @@ module RubyIndexer
40
40
 
41
41
  abstract!
42
42
 
43
+ sig { returns(T::Array[String]) }
44
+ attr_accessor :included_modules
45
+
46
+ sig do
47
+ params(
48
+ name: String,
49
+ file_path: String,
50
+ location: Prism::Location,
51
+ comments: T::Array[String],
52
+ ).void
53
+ end
54
+ def initialize(name, file_path, location, comments)
55
+ super(name, file_path, location, comments)
56
+ @included_modules = T.let([], T::Array[String])
57
+ end
58
+
43
59
  sig { returns(String) }
44
60
  def short_name
45
61
  T.must(@name.split("::").last)
@@ -191,8 +191,9 @@ module RubyIndexer
191
191
 
192
192
  require_path = indexable_path.require_path
193
193
  @require_paths_tree.insert(require_path, indexable_path) if require_path
194
- rescue Errno::EISDIR
195
- # If `path` is a directory, just ignore it and continue indexing
194
+ rescue Errno::EISDIR, Errno::ENOENT
195
+ # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
196
+ # it
196
197
  end
197
198
 
198
199
  # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
@@ -281,5 +281,51 @@ module RubyIndexer
281
281
  final_thing = T.must(@index["FinalThing"].first)
282
282
  assert_equal("Something::Baz", final_thing.parent_class)
283
283
  end
284
+
285
+ def test_keeping_track_of_included_modules
286
+ index(<<~RUBY)
287
+ class Foo
288
+ # valid syntaxes that we can index
289
+ include A1
290
+ self.include A2
291
+ include A3, A4
292
+ self.include A5, A6
293
+
294
+ # valid syntaxes that we cannot index because of their dynamic nature
295
+ include some_variable_or_method_call
296
+ self.include some_variable_or_method_call
297
+
298
+ def something
299
+ include A7 # We should not index this because of this dynamic nature
300
+ end
301
+
302
+ # Valid inner class syntax definition with its own modules included
303
+ class Qux
304
+ include Corge
305
+ self.include Corge
306
+ include Baz
307
+
308
+ include some_variable_or_method_call
309
+ end
310
+ end
311
+
312
+ class ConstantPathReferences
313
+ include Foo::Bar
314
+ self.include Foo::Bar2
315
+
316
+ include dynamic::Bar
317
+ include Foo::
318
+ end
319
+ RUBY
320
+
321
+ foo = T.must(@index["Foo"][0])
322
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules)
323
+
324
+ qux = T.must(@index["Foo::Qux"][0])
325
+ assert_equal(["Corge", "Corge", "Baz"], qux.included_modules)
326
+
327
+ constant_path_references = T.must(@index["ConstantPathReferences"][0])
328
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
329
+ end
284
330
  end
285
331
  end
@@ -308,5 +308,10 @@ module RubyIndexer
308
308
 
309
309
  refute_empty(@index.instance_variable_get(:@entries))
310
310
  end
311
+
312
+ def test_index_single_does_not_fail_for_non_existing_file
313
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"))
314
+ assert_empty(@index.instance_variable_get(:@entries))
315
+ end
311
316
  end
312
317
  end
@@ -235,15 +235,17 @@ module RubyLsp
235
235
 
236
236
  sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
237
237
  def workspace_dependencies
238
- definition = Bundler.definition
239
- dep_keys = definition.locked_deps.keys.to_set
240
- definition.specs.map do |spec|
241
- {
242
- name: spec.name,
243
- version: spec.version,
244
- path: spec.full_gem_path,
245
- dependency: dep_keys.include?(spec.name),
246
- }
238
+ Bundler.with_original_env do
239
+ definition = Bundler.definition
240
+ dep_keys = definition.locked_deps.keys.to_set
241
+ definition.specs.map do |spec|
242
+ {
243
+ name: spec.name,
244
+ version: spec.version,
245
+ path: spec.full_gem_path,
246
+ dependency: dep_keys.include?(spec.name),
247
+ }
248
+ end
247
249
  end
248
250
  rescue Bundler::GemfileNotFound
249
251
  []
@@ -22,6 +22,7 @@ module RubyLsp
22
22
  SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])
23
23
  DESCRIBE_KEYWORD = T.let(:describe, Symbol)
24
24
  IT_KEYWORD = T.let(:it, Symbol)
25
+ DYNAMIC_REFERENCE_MARKER = T.let("<dynamic_reference>", String)
25
26
 
26
27
  sig do
27
28
  params(
@@ -44,6 +45,8 @@ module RubyLsp
44
45
  self,
45
46
  :on_class_node_enter,
46
47
  :on_class_node_leave,
48
+ :on_module_node_enter,
49
+ :on_module_node_leave,
47
50
  :on_def_node_enter,
48
51
  :on_call_node_enter,
49
52
  :on_call_node_leave,
@@ -60,7 +63,7 @@ module RubyLsp
60
63
  add_test_code_lens(
61
64
  node,
62
65
  name: class_name,
63
- command: generate_test_command(group_name: class_name),
66
+ command: generate_test_command(group_stack: @group_stack),
64
67
  kind: :group,
65
68
  )
66
69
  end
@@ -88,13 +91,27 @@ module RubyLsp
88
91
  add_test_code_lens(
89
92
  node,
90
93
  name: method_name,
91
- command: generate_test_command(method_name: method_name, group_name: class_name),
94
+ command: generate_test_command(method_name: method_name, group_stack: @group_stack),
92
95
  kind: :example,
93
96
  )
94
97
  end
95
98
  end
96
99
  end
97
100
 
101
+ sig { params(node: Prism::ModuleNode).void }
102
+ def on_module_node_enter(node)
103
+ if (path = namespace_constant_name(node))
104
+ @group_stack.push(path)
105
+ else
106
+ @group_stack.push(DYNAMIC_REFERENCE_MARKER)
107
+ end
108
+ end
109
+
110
+ sig { params(node: Prism::ModuleNode).void }
111
+ def on_module_node_leave(node)
112
+ @group_stack.pop
113
+ end
114
+
98
115
  sig { params(node: Prism::CallNode).void }
99
116
  def on_call_node_enter(node)
100
117
  name = node.name
@@ -181,18 +198,45 @@ module RubyLsp
181
198
  )
182
199
  end
183
200
 
184
- sig { params(group_name: String, method_name: T.nilable(String)).returns(String) }
185
- def generate_test_command(group_name:, method_name: nil)
201
+ sig do
202
+ params(
203
+ group_stack: T::Array[String],
204
+ spec_name: T.nilable(String),
205
+ method_name: T.nilable(String),
206
+ ).returns(String)
207
+ end
208
+ def generate_test_command(group_stack: [], spec_name: nil, method_name: nil)
186
209
  command = BASE_COMMAND + T.must(@path)
187
210
 
188
211
  case DependencyDetector.instance.detected_test_library
189
212
  when "minitest"
190
- command += if method_name
191
- " --name " + "/#{Shellwords.escape(group_name + "#" + method_name)}/"
213
+ last_dynamic_reference_index = group_stack.rindex(DYNAMIC_REFERENCE_MARKER)
214
+ command += if last_dynamic_reference_index
215
+ # In cases where the test path looks like `foo::Bar`
216
+ # the best we can do is match everything to the right of it.
217
+ # Tests are classes, dynamic references are only a thing for modules,
218
+ # so there must be something to the left of the available path.
219
+ group_stack = T.must(group_stack[last_dynamic_reference_index + 1..])
220
+ if method_name
221
+ " --name " + "/::#{Shellwords.escape(group_stack.join("::") + "#" + method_name)}$/"
222
+ else
223
+ # When clicking on a CodeLens for `Test`, `(#|::)` will match all tests
224
+ # that are registered on the class itself (matches after `#`) and all tests
225
+ # that are nested inside of that class in other modules/classes (matches after `::`)
226
+ " --name " + "\"/::#{Shellwords.escape(group_stack.join("::"))}(#|::)/\""
227
+ end
228
+ elsif method_name
229
+ # We know the entire path, do an exact match
230
+ " --name " + Shellwords.escape(group_stack.join("::") + "#" + method_name)
231
+ elsif spec_name
232
+ " --name " + "/#{Shellwords.escape(spec_name)}/"
192
233
  else
193
- " --name " + "/#{Shellwords.escape(group_name)}/"
234
+ # Execute all tests of the selected class and tests in
235
+ # modules/classes nested inside of that class
236
+ " --name " + "\"/^#{Shellwords.escape(group_stack.join("::"))}(#|::)/\""
194
237
  end
195
238
  when "test-unit"
239
+ group_name = T.must(group_stack.last)
196
240
  command += " --testcase " + "/#{Shellwords.escape(group_name)}/"
197
241
 
198
242
  if method_name
@@ -214,8 +258,8 @@ module RubyLsp
214
258
  name = case first_argument
215
259
  when Prism::StringNode
216
260
  first_argument.content
217
- when Prism::ConstantReadNode
218
- first_argument.full_name
261
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
262
+ constant_name(first_argument)
219
263
  end
220
264
 
221
265
  return unless name
@@ -223,7 +267,7 @@ module RubyLsp
223
267
  add_test_code_lens(
224
268
  node,
225
269
  name: name,
226
- command: generate_test_command(group_name: name),
270
+ command: generate_test_command(spec_name: name),
227
271
  kind: kind,
228
272
  )
229
273
  end
@@ -49,7 +49,13 @@ module RubyLsp
49
49
  node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
50
50
  )
51
51
 
52
- target = parent if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
52
+ if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
53
+ target = determine_target(
54
+ target,
55
+ parent,
56
+ position,
57
+ )
58
+ end
53
59
 
54
60
  Listeners::Definition.new(@response_builder, document.uri, nesting, index, dispatcher, typechecker_enabled)
55
61
 
@@ -50,7 +50,11 @@ module RubyLsp
50
50
  if (Listeners::Hover::ALLOWED_TARGETS.include?(parent.class) &&
51
51
  !Listeners::Hover::ALLOWED_TARGETS.include?(@target.class)) ||
52
52
  (parent.is_a?(Prism::ConstantPathNode) && @target.is_a?(Prism::ConstantReadNode))
53
- @target = parent
53
+ @target = determine_target(
54
+ T.must(@target),
55
+ T.must(parent),
56
+ position,
57
+ )
54
58
  end
55
59
 
56
60
  # Don't need to instantiate any listeners if there's no target
@@ -12,6 +12,57 @@ module RubyLsp
12
12
 
13
13
  sig { abstract.returns(T.anything) }
14
14
  def perform; end
15
+
16
+ private
17
+
18
+ # Checks if a location covers a position
19
+ sig { params(location: Prism::Location, position: T.untyped).returns(T::Boolean) }
20
+ def cover?(location, position)
21
+ start_covered =
22
+ location.start_line - 1 < position[:line] ||
23
+ (
24
+ location.start_line - 1 == position[:line] &&
25
+ location.start_column <= position[:character]
26
+ )
27
+ end_covered =
28
+ location.end_line - 1 > position[:line] ||
29
+ (
30
+ location.end_line - 1 == position[:line] &&
31
+ location.end_column >= position[:character]
32
+ )
33
+ start_covered && end_covered
34
+ end
35
+
36
+ # Based on a constant node target, a constant path node parent and a position, this method will find the exact
37
+ # portion of the constant path that matches the requested position, for higher precision in hover and
38
+ # definition. For example:
39
+ #
40
+ # ```ruby
41
+ # Foo::Bar::Baz
42
+ # # ^ Going to definition here should go to Foo::Bar::Baz
43
+ # # ^ Going to definition here should go to Foo::Bar
44
+ # #^ Going to definition here should go to Foo
45
+ # ```
46
+ sig do
47
+ params(
48
+ target: Prism::Node,
49
+ parent: Prism::Node,
50
+ position: T::Hash[Symbol, Integer],
51
+ ).returns(Prism::Node)
52
+ end
53
+ def determine_target(target, parent, position)
54
+ return target unless parent.is_a?(Prism::ConstantPathNode)
55
+
56
+ target = T.let(parent, Prism::Node)
57
+ parent = T.let(T.cast(target, Prism::ConstantPathNode).parent, T.nilable(Prism::Node))
58
+
59
+ while parent && cover?(parent.location, position)
60
+ target = parent
61
+ parent = target.is_a?(Prism::ConstantPathNode) ? target.parent : nil
62
+ end
63
+
64
+ target
65
+ end
15
66
  end
16
67
  end
17
68
  end
@@ -147,6 +147,15 @@ module RubyLsp
147
147
  rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
148
148
  nil
149
149
  end
150
+
151
+ sig { params(node: T.any(Prism::ModuleNode, Prism::ClassNode)).returns(T.nilable(String)) }
152
+ def namespace_constant_name(node)
153
+ path = node.constant_path
154
+ case path
155
+ when Prism::ConstantPathNode, Prism::ConstantReadNode, Prism::ConstantPathTargetNode
156
+ constant_name(path)
157
+ end
158
+ end
150
159
  end
151
160
  end
152
161
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.5
4
+ version: 0.14.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-07 00:00:00.000000000 Z
11
+ date: 2024-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol