ruby-lsp 0.16.6 → 0.17.0

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +21 -4
  5. data/exe/ruby-lsp-check +1 -3
  6. data/exe/ruby-lsp-doctor +1 -4
  7. data/lib/core_ext/uri.rb +3 -0
  8. data/lib/ruby_indexer/lib/ruby_indexer/{collector.rb → declaration_listener.rb} +258 -140
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +101 -12
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +187 -12
  11. data/lib/ruby_indexer/ruby_indexer.rb +1 -1
  12. data/lib/ruby_indexer/test/classes_and_modules_test.rb +106 -10
  13. data/lib/ruby_indexer/test/configuration_test.rb +4 -5
  14. data/lib/ruby_indexer/test/constant_test.rb +11 -8
  15. data/lib/ruby_indexer/test/index_test.rb +528 -0
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
  17. data/lib/ruby_indexer/test/method_test.rb +93 -0
  18. data/lib/ruby_indexer/test/test_case.rb +3 -1
  19. data/lib/ruby_lsp/addon.rb +8 -8
  20. data/lib/ruby_lsp/document.rb +3 -3
  21. data/lib/ruby_lsp/internal.rb +1 -0
  22. data/lib/ruby_lsp/listeners/code_lens.rb +11 -0
  23. data/lib/ruby_lsp/listeners/completion.rb +144 -51
  24. data/lib/ruby_lsp/listeners/definition.rb +77 -12
  25. data/lib/ruby_lsp/listeners/document_highlight.rb +1 -1
  26. data/lib/ruby_lsp/listeners/document_link.rb +1 -1
  27. data/lib/ruby_lsp/listeners/hover.rb +60 -6
  28. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +59 -3
  29. data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
  30. data/lib/ruby_lsp/node_context.rb +28 -0
  31. data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
  32. data/lib/ruby_lsp/requests/code_actions.rb +16 -15
  33. data/lib/ruby_lsp/requests/completion.rb +22 -13
  34. data/lib/ruby_lsp/requests/completion_resolve.rb +26 -10
  35. data/lib/ruby_lsp/requests/definition.rb +21 -5
  36. data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
  37. data/lib/ruby_lsp/requests/hover.rb +5 -6
  38. data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
  39. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  40. data/lib/ruby_lsp/requests/support/common.rb +20 -1
  41. data/lib/ruby_lsp/requests/workspace_symbol.rb +3 -1
  42. data/lib/ruby_lsp/server.rb +10 -4
  43. metadata +10 -8
@@ -3,6 +3,14 @@
3
3
 
4
4
  module RubyIndexer
5
5
  class Entry
6
+ class Visibility < T::Enum
7
+ enums do
8
+ PUBLIC = new(:public)
9
+ PROTECTED = new(:protected)
10
+ PRIVATE = new(:private)
11
+ end
12
+ end
13
+
6
14
  extend T::Sig
7
15
 
8
16
  sig { returns(String) }
@@ -17,7 +25,7 @@ module RubyIndexer
17
25
  sig { returns(T::Array[String]) }
18
26
  attr_reader :comments
19
27
 
20
- sig { returns(Symbol) }
28
+ sig { returns(Visibility) }
21
29
  attr_accessor :visibility
22
30
 
23
31
  sig do
@@ -32,7 +40,7 @@ module RubyIndexer
32
40
  @name = name
33
41
  @file_path = file_path
34
42
  @comments = comments
35
- @visibility = T.let(:public, Symbol)
43
+ @visibility = T.let(Visibility::PUBLIC, Visibility)
36
44
 
37
45
  @location = T.let(
38
46
  if location.is_a?(Prism::Location)
@@ -49,11 +57,35 @@ module RubyIndexer
49
57
  )
50
58
  end
51
59
 
60
+ sig { returns(T::Boolean) }
61
+ def private?
62
+ visibility == Visibility::PRIVATE
63
+ end
64
+
52
65
  sig { returns(String) }
53
66
  def file_name
54
67
  File.basename(@file_path)
55
68
  end
56
69
 
70
+ class ModuleOperation
71
+ extend T::Sig
72
+ extend T::Helpers
73
+
74
+ abstract!
75
+
76
+ sig { returns(String) }
77
+ attr_reader :module_name
78
+
79
+ sig { params(module_name: String).void }
80
+ def initialize(module_name)
81
+ @module_name = module_name
82
+ end
83
+ end
84
+
85
+ class Include < ModuleOperation; end
86
+ class Prepend < ModuleOperation; end
87
+ class Extend < ModuleOperation; end
88
+
57
89
  class Namespace < Entry
58
90
  extend T::Sig
59
91
  extend T::Helpers
@@ -61,13 +93,40 @@ module RubyIndexer
61
93
  abstract!
62
94
 
63
95
  sig { returns(T::Array[String]) }
64
- def included_modules
65
- @included_modules ||= T.let([], T.nilable(T::Array[String]))
96
+ attr_reader :nesting
97
+
98
+ sig do
99
+ params(
100
+ nesting: T::Array[String],
101
+ file_path: String,
102
+ location: T.any(Prism::Location, RubyIndexer::Location),
103
+ comments: T::Array[String],
104
+ ).void
105
+ end
106
+ def initialize(nesting, file_path, location, comments)
107
+ @name = T.let(nesting.join("::"), String)
108
+ # The original nesting where this namespace was discovered
109
+ @nesting = nesting
110
+
111
+ super(@name, file_path, location, comments)
66
112
  end
67
113
 
68
114
  sig { returns(T::Array[String]) }
69
- def prepended_modules
70
- @prepended_modules ||= T.let([], T.nilable(T::Array[String]))
115
+ def mixin_operation_module_names
116
+ mixin_operations.map(&:module_name)
117
+ end
118
+
119
+ # Stores all explicit prepend, include and extend operations in the exact order they were discovered in the source
120
+ # code. Maintaining the order is essential to linearize ancestors the right way when a module is both included
121
+ # and prepended
122
+ sig { returns(T::Array[ModuleOperation]) }
123
+ def mixin_operations
124
+ @mixin_operations ||= T.let([], T.nilable(T::Array[ModuleOperation]))
125
+ end
126
+
127
+ sig { returns(Integer) }
128
+ def ancestor_hash
129
+ mixin_operation_module_names.hash
71
130
  end
72
131
  end
73
132
 
@@ -84,17 +143,23 @@ module RubyIndexer
84
143
 
85
144
  sig do
86
145
  params(
87
- name: String,
146
+ nesting: T::Array[String],
88
147
  file_path: String,
89
148
  location: T.any(Prism::Location, RubyIndexer::Location),
90
149
  comments: T::Array[String],
91
150
  parent_class: T.nilable(String),
92
151
  ).void
93
152
  end
94
- def initialize(name, file_path, location, comments, parent_class)
95
- super(name, file_path, location, comments)
153
+ def initialize(nesting, file_path, location, comments, parent_class)
154
+ super(nesting, file_path, location, comments)
155
+
96
156
  @parent_class = T.let(parent_class, T.nilable(String))
97
157
  end
158
+
159
+ sig { override.returns(Integer) }
160
+ def ancestor_hash
161
+ [mixin_operation_module_names, @parent_class].hash
162
+ end
98
163
  end
99
164
 
100
165
  class Constant < Entry
@@ -188,11 +253,13 @@ module RubyIndexer
188
253
  file_path: String,
189
254
  location: T.any(Prism::Location, RubyIndexer::Location),
190
255
  comments: T::Array[String],
256
+ visibility: Visibility,
191
257
  owner: T.nilable(Entry::Namespace),
192
258
  ).void
193
259
  end
194
- def initialize(name, file_path, location, comments, owner)
260
+ def initialize(name, file_path, location, comments, visibility, owner) # rubocop:disable Metrics/ParameterLists
195
261
  super(name, file_path, location, comments)
262
+ @visibility = visibility
196
263
  @owner = owner
197
264
  end
198
265
 
@@ -227,11 +294,12 @@ module RubyIndexer
227
294
  location: T.any(Prism::Location, RubyIndexer::Location),
228
295
  comments: T::Array[String],
229
296
  parameters_node: T.nilable(Prism::ParametersNode),
297
+ visibility: Visibility,
230
298
  owner: T.nilable(Entry::Namespace),
231
299
  ).void
232
300
  end
233
- def initialize(name, file_path, location, comments, parameters_node, owner) # rubocop:disable Metrics/ParameterLists
234
- super(name, file_path, location, comments, owner)
301
+ def initialize(name, file_path, location, comments, parameters_node, visibility, owner) # rubocop:disable Metrics/ParameterLists
302
+ super(name, file_path, location, comments, visibility, owner)
235
303
 
236
304
  @parameters = T.let(list_params(parameters_node), T::Array[Parameter])
237
305
  end
@@ -377,8 +445,29 @@ module RubyIndexer
377
445
  def initialize(target, unresolved_alias)
378
446
  super(unresolved_alias.name, unresolved_alias.file_path, unresolved_alias.location, unresolved_alias.comments)
379
447
 
448
+ @visibility = unresolved_alias.visibility
380
449
  @target = target
381
450
  end
382
451
  end
452
+
453
+ # Represents an instance variable e.g.: @a = 1
454
+ class InstanceVariable < Entry
455
+ sig { returns(T.nilable(Entry::Namespace)) }
456
+ attr_reader :owner
457
+
458
+ sig do
459
+ params(
460
+ name: String,
461
+ file_path: String,
462
+ location: T.any(Prism::Location, RubyIndexer::Location),
463
+ comments: T::Array[String],
464
+ owner: T.nilable(Entry::Namespace),
465
+ ).void
466
+ end
467
+ def initialize(name, file_path, location, comments, owner)
468
+ super(name, file_path, location, comments)
469
+ @owner = owner
470
+ end
471
+ end
383
472
  end
384
473
  end
@@ -6,6 +6,7 @@ module RubyIndexer
6
6
  extend T::Sig
7
7
 
8
8
  class UnresolvableAliasError < StandardError; end
9
+ class NonExistingNamespaceError < StandardError; end
9
10
 
10
11
  # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
11
12
  ENTRY_SIMILARITY_THRESHOLD = 0.7
@@ -31,6 +32,9 @@ module RubyIndexer
31
32
 
32
33
  # Holds all require paths for every indexed item so that we can provide autocomplete for requires
33
34
  @require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
35
+
36
+ # Holds the linearized ancestors list for every namespace
37
+ @ancestors = T.let({}, T::Hash[String, T::Array[String]])
34
38
  end
35
39
 
36
40
  sig { params(indexable: IndexablePath).void }
@@ -126,6 +130,16 @@ module RubyIndexer
126
130
  results.flat_map(&:first)
127
131
  end
128
132
 
133
+ sig { params(name: String, receiver_name: String).returns(T::Array[Entry]) }
134
+ def method_completion_candidates(name, receiver_name)
135
+ ancestors = linearized_ancestors_of(receiver_name)
136
+ candidates = prefix_search(name).flatten
137
+ candidates.select! do |entry|
138
+ entry.is_a?(RubyIndexer::Entry::Member) && ancestors.any?(entry.owner&.name)
139
+ end
140
+ candidates
141
+ end
142
+
129
143
  # Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
130
144
  # the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
131
145
  # 1. Foo::Bar::Baz
@@ -185,9 +199,11 @@ module RubyIndexer
185
199
  sig { params(indexable_path: IndexablePath, source: T.nilable(String)).void }
186
200
  def index_single(indexable_path, source = nil)
187
201
  content = source || File.read(indexable_path.full_path)
202
+ dispatcher = Prism::Dispatcher.new
203
+
188
204
  result = Prism.parse(content)
189
- collector = Collector.new(self, result, indexable_path.full_path)
190
- collector.collect(result.value)
205
+ DeclarationListener.new(self, dispatcher, result, indexable_path.full_path)
206
+ dispatcher.dispatch(result.value)
191
207
 
192
208
  require_path = indexable_path.require_path
193
209
  @require_paths_tree.insert(require_path, indexable_path) if require_path
@@ -243,16 +259,175 @@ module RubyIndexer
243
259
  sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
244
260
  def resolve_method(method_name, receiver_name)
245
261
  method_entries = self[method_name]
246
- owner_entries = self[receiver_name]
247
- return unless owner_entries && method_entries
248
-
249
- owner_name = T.must(owner_entries.first).name
250
- T.cast(
251
- method_entries.grep(Entry::Member).select do |entry|
252
- T.cast(entry, Entry::Member).owner&.name == owner_name
253
- end,
254
- T::Array[Entry::Member],
255
- )
262
+ ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
263
+ return unless method_entries
264
+
265
+ ancestors.each do |ancestor|
266
+ found = method_entries.select do |entry|
267
+ next unless entry.is_a?(Entry::Member)
268
+
269
+ entry.owner&.name == ancestor
270
+ end
271
+
272
+ return T.cast(found, T::Array[Entry::Member]) if found.any?
273
+ end
274
+
275
+ nil
276
+ rescue NonExistingNamespaceError
277
+ nil
278
+ end
279
+
280
+ # Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method
281
+ # or constant declarations.
282
+ #
283
+ # When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize
284
+ # everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a
285
+ # module that prepends another module, then the prepend module appears before the included module.
286
+ #
287
+ # The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass]
288
+ sig { params(fully_qualified_name: String).returns(T::Array[String]) }
289
+ def linearized_ancestors_of(fully_qualified_name)
290
+ # If we already computed the ancestors for this namespace, return it straight away
291
+ cached_ancestors = @ancestors[fully_qualified_name]
292
+ return cached_ancestors if cached_ancestors
293
+
294
+ ancestors = [fully_qualified_name]
295
+
296
+ # Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
297
+ # this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
298
+ # the cache will reflect the final result
299
+ @ancestors[fully_qualified_name] = ancestors
300
+
301
+ # If we don't have an entry for `name`, raise
302
+ entries = resolve(fully_qualified_name, [])
303
+ raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
304
+
305
+ # If none of the entries for `name` are namespaces, raise
306
+ namespaces = entries.filter_map do |entry|
307
+ case entry
308
+ when Entry::Namespace
309
+ entry
310
+ when Entry::Alias
311
+ self[entry.target]&.grep(Entry::Namespace)
312
+ end
313
+ end.flatten
314
+
315
+ raise NonExistingNamespaceError,
316
+ "None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
317
+
318
+ mixin_operations = namespaces.flat_map(&:mixin_operations)
319
+ main_namespace_index = 0
320
+
321
+ # The original nesting where we discovered this namespace, so that we resolve the correct names of the
322
+ # included/prepended/extended modules and parent classes
323
+ nesting = T.must(namespaces.first).nesting
324
+
325
+ mixin_operations.each do |operation|
326
+ resolved_module = resolve(operation.module_name, nesting)
327
+ next unless resolved_module
328
+
329
+ module_fully_qualified_name = T.must(resolved_module.first).name
330
+
331
+ case operation
332
+ when Entry::Prepend
333
+ # When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
334
+ # the actual namespace twice. However, it does not check if it has been included because you are allowed to
335
+ # prepend the same module after it has already been included
336
+ linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
337
+
338
+ # When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
339
+ # example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
340
+ # be inserted after `"A`
341
+ uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
342
+ insert_position = linearized_prepends.length - uniq_prepends.length
343
+
344
+ T.unsafe(ancestors).insert(
345
+ insert_position,
346
+ *(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
347
+ )
348
+
349
+ main_namespace_index += linearized_prepends.length
350
+ when Entry::Include
351
+ # When including a module, Ruby will always prevent duplicate entries in case the module has already been
352
+ # prepended or included
353
+ linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
354
+ T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
355
+ end
356
+ end
357
+
358
+ # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
359
+ # from two diffent classes in different files, we simply ignore it
360
+ superclass = T.cast(namespaces.find { |n| n.is_a?(Entry::Class) && n.parent_class }, T.nilable(Entry::Class))
361
+
362
+ if superclass
363
+ # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
364
+ # error. We need to ensure that this isn't the case
365
+ parent_class = T.must(superclass.parent_class)
366
+
367
+ resolved_parent_class = resolve(parent_class, nesting)
368
+ parent_class_name = resolved_parent_class&.first&.name
369
+
370
+ if parent_class_name && fully_qualified_name != parent_class_name
371
+ ancestors.concat(linearized_ancestors_of(parent_class_name))
372
+ end
373
+ end
374
+
375
+ ancestors
376
+ end
377
+
378
+ # Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner
379
+ # and find inherited instance variables as well
380
+ sig { params(variable_name: String, owner_name: String).returns(T.nilable(T::Array[Entry::InstanceVariable])) }
381
+ def resolve_instance_variable(variable_name, owner_name)
382
+ entries = T.cast(self[variable_name], T.nilable(T::Array[Entry::InstanceVariable]))
383
+ return unless entries
384
+
385
+ ancestors = linearized_ancestors_of(owner_name)
386
+ return if ancestors.empty?
387
+
388
+ entries.select { |e| ancestors.include?(e.owner&.name) }
389
+ end
390
+
391
+ # Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
392
+ # include the `@` prefix
393
+ sig { params(name: String, owner_name: String).returns(T::Array[Entry::InstanceVariable]) }
394
+ def instance_variable_completion_candidates(name, owner_name)
395
+ entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
396
+ ancestors = linearized_ancestors_of(owner_name)
397
+
398
+ variables = entries.uniq(&:name)
399
+ variables.select! { |e| ancestors.any?(e.owner&.name) }
400
+ variables
401
+ end
402
+
403
+ # Synchronizes a change made to the given indexable path. This method will ensure that new declarations are indexed,
404
+ # removed declarations removed and that the ancestor linearization cache is cleared if necessary
405
+ sig { params(indexable: IndexablePath).void }
406
+ def handle_change(indexable)
407
+ original_entries = @files_to_entries[indexable.full_path]
408
+
409
+ delete(indexable)
410
+ index_single(indexable)
411
+
412
+ updated_entries = @files_to_entries[indexable.full_path]
413
+
414
+ return unless original_entries && updated_entries
415
+
416
+ # A change in one ancestor may impact several different others, which could be including that ancestor through
417
+ # indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
418
+ # ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
419
+ # ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
420
+ original_map = T.cast(
421
+ original_entries.select { |e| e.is_a?(Entry::Namespace) },
422
+ T::Array[Entry::Namespace],
423
+ ).to_h { |e| [e.name, e.ancestor_hash] }
424
+
425
+ updated_map = T.cast(
426
+ updated_entries.select { |e| e.is_a?(Entry::Namespace) },
427
+ T::Array[Entry::Namespace],
428
+ ).to_h { |e| [e.name, e.ancestor_hash] }
429
+
430
+ @ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
256
431
  end
257
432
 
258
433
  private
@@ -5,7 +5,7 @@ require "yaml"
5
5
  require "did_you_mean"
6
6
 
7
7
  require "ruby_indexer/lib/ruby_indexer/indexable_path"
8
- require "ruby_indexer/lib/ruby_indexer/collector"
8
+ require "ruby_indexer/lib/ruby_indexer/declaration_listener"
9
9
  require "ruby_indexer/lib/ruby_indexer/index"
10
10
  require "ruby_indexer/lib/ruby_indexer/entry"
11
11
  require "ruby_indexer/lib/ruby_indexer/configuration"
@@ -14,6 +14,15 @@ module RubyIndexer
14
14
  assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
15
15
  end
16
16
 
17
+ def test_conditional_class
18
+ index(<<~RUBY)
19
+ class Foo
20
+ end if condition
21
+ RUBY
22
+
23
+ assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
24
+ end
25
+
17
26
  def test_class_with_statements
18
27
  index(<<~RUBY)
19
28
  class Foo
@@ -60,7 +69,23 @@ module RubyIndexer
60
69
  end
61
70
  RUBY
62
71
 
72
+ assert_entry("self::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
73
+ end
74
+
75
+ def test_dynamically_namespaced_class_doesnt_affect_other_classes
76
+ index(<<~RUBY)
77
+ class Foo
78
+ class self::Bar
79
+ end
80
+
81
+ class Bar
82
+ end
83
+ end
84
+ RUBY
85
+
63
86
  refute_entry("self::Bar")
87
+ assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:6-3")
88
+ assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:4-2:5-5")
64
89
  end
65
90
 
66
91
  def test_empty_statements_module
@@ -72,6 +97,15 @@ module RubyIndexer
72
97
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
73
98
  end
74
99
 
100
+ def test_conditional_module
101
+ index(<<~RUBY)
102
+ module Foo
103
+ end if condition
104
+ RUBY
105
+
106
+ assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
107
+ end
108
+
75
109
  def test_module_with_statements
76
110
  index(<<~RUBY)
77
111
  module Foo
@@ -106,7 +140,23 @@ module RubyIndexer
106
140
  end
107
141
  RUBY
108
142
 
109
- refute_entry("self::Bar")
143
+ assert_entry("self::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
144
+ end
145
+
146
+ def test_dynamically_namespaced_module_doesnt_affect_other_modules
147
+ index(<<~RUBY)
148
+ module Foo
149
+ class self::Bar
150
+ end
151
+
152
+ module Bar
153
+ end
154
+ end
155
+ RUBY
156
+
157
+ assert_entry("Foo::self::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
158
+ assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:6-3")
159
+ assert_entry("Foo::Bar", Entry::Module, "/fake/path/foo.rb:4-2:5-5")
110
160
  end
111
161
 
112
162
  def test_nested_modules_and_classes
@@ -240,13 +290,13 @@ module RubyIndexer
240
290
  RUBY
241
291
 
242
292
  b_const = @index["A::B"].first
243
- assert_equal(:private, b_const.visibility)
293
+ assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
244
294
 
245
295
  c_const = @index["A::C"].first
246
- assert_equal(:private, c_const.visibility)
296
+ assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
247
297
 
248
298
  d_const = @index["A::D"].first
249
- assert_equal(:public, d_const.visibility)
299
+ assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
250
300
  end
251
301
 
252
302
  def test_keeping_track_of_super_classes
@@ -319,13 +369,13 @@ module RubyIndexer
319
369
  RUBY
320
370
 
321
371
  foo = T.must(@index["Foo"][0])
322
- assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules)
372
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
323
373
 
324
374
  qux = T.must(@index["Foo::Qux"][0])
325
- assert_equal(["Corge", "Corge", "Baz"], qux.included_modules)
375
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
326
376
 
327
377
  constant_path_references = T.must(@index["ConstantPathReferences"][0])
328
- assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
378
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
329
379
  end
330
380
 
331
381
  def test_keeping_track_of_prepended_modules
@@ -365,13 +415,59 @@ module RubyIndexer
365
415
  RUBY
366
416
 
367
417
  foo = T.must(@index["Foo"][0])
368
- assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.prepended_modules)
418
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
419
+
420
+ qux = T.must(@index["Foo::Qux"][0])
421
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
422
+
423
+ constant_path_references = T.must(@index["ConstantPathReferences"][0])
424
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
425
+ end
426
+
427
+ def test_keeping_track_of_extended_modules
428
+ index(<<~RUBY)
429
+ class Foo
430
+ # valid syntaxes that we can index
431
+ extend A1
432
+ self.extend A2
433
+ extend A3, A4
434
+ self.extend A5, A6
435
+
436
+ # valid syntaxes that we cannot index because of their dynamic nature
437
+ extend some_variable_or_method_call
438
+ self.extend some_variable_or_method_call
439
+
440
+ def something
441
+ extend A7 # We should not index this because of this dynamic nature
442
+ end
443
+
444
+ # Valid inner class syntax definition with its own modules prepended
445
+ class Qux
446
+ extend Corge
447
+ self.extend Corge
448
+ extend Baz
449
+
450
+ extend some_variable_or_method_call
451
+ end
452
+ end
453
+
454
+ class ConstantPathReferences
455
+ extend Foo::Bar
456
+ self.extend Foo::Bar2
457
+
458
+ extend dynamic::Bar
459
+ extend Foo::
460
+ end
461
+ RUBY
462
+
463
+ foo = T.must(@index["Foo"][0])
464
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
369
465
 
370
466
  qux = T.must(@index["Foo::Qux"][0])
371
- assert_equal(["Corge", "Corge", "Baz"], qux.prepended_modules)
467
+ assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
372
468
 
373
469
  constant_path_references = T.must(@index["ConstantPathReferences"][0])
374
- assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.prepended_modules)
470
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
375
471
  end
376
472
  end
377
473
  end
@@ -68,12 +68,11 @@ module RubyIndexer
68
68
  end
69
69
 
70
70
  def test_indexables_avoids_duplicates_if_bundle_path_is_inside_project
71
- Bundler.settings.set_global("path", "vendor/bundle")
72
- config = Configuration.new
71
+ Bundler.settings.temporary(path: "vendor/bundle") do
72
+ config = Configuration.new
73
73
 
74
- assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
75
- ensure
76
- Bundler.settings.set_global("path", nil)
74
+ assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
75
+ end
77
76
  end
78
77
 
79
78
  def test_indexables_does_not_include_gems_own_installed_files