ruby-lsp 0.16.6 → 0.17.0

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