ruby-lsp 0.14.5 → 0.14.6

Sign up to get free protection for your applications and to get access to all the features.
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