ruby-lsp 0.12.2 → 0.13.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp-check +20 -4
  5. data/exe/ruby-lsp-doctor +2 -2
  6. data/lib/ruby_indexer/lib/ruby_indexer/{visitor.rb → collector.rb} +144 -61
  7. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +9 -4
  8. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +89 -12
  9. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +22 -4
  10. data/lib/ruby_indexer/ruby_indexer.rb +1 -1
  11. data/lib/ruby_indexer/test/configuration_test.rb +10 -0
  12. data/lib/ruby_indexer/test/index_test.rb +64 -0
  13. data/lib/ruby_indexer/test/method_test.rb +80 -0
  14. data/lib/ruby_lsp/addon.rb +9 -13
  15. data/lib/ruby_lsp/document.rb +7 -9
  16. data/lib/ruby_lsp/executor.rb +54 -51
  17. data/lib/ruby_lsp/internal.rb +4 -0
  18. data/lib/ruby_lsp/listener.rb +4 -5
  19. data/lib/ruby_lsp/requests/code_action_resolve.rb +8 -4
  20. data/lib/ruby_lsp/requests/code_lens.rb +16 -7
  21. data/lib/ruby_lsp/requests/completion.rb +60 -8
  22. data/lib/ruby_lsp/requests/definition.rb +55 -29
  23. data/lib/ruby_lsp/requests/diagnostics.rb +0 -5
  24. data/lib/ruby_lsp/requests/document_highlight.rb +20 -11
  25. data/lib/ruby_lsp/requests/document_link.rb +2 -3
  26. data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
  27. data/lib/ruby_lsp/requests/folding_ranges.rb +12 -15
  28. data/lib/ruby_lsp/requests/formatting.rb +0 -5
  29. data/lib/ruby_lsp/requests/hover.rb +23 -4
  30. data/lib/ruby_lsp/requests/inlay_hints.rb +42 -4
  31. data/lib/ruby_lsp/requests/on_type_formatting.rb +18 -4
  32. data/lib/ruby_lsp/requests/semantic_highlighting.rb +41 -16
  33. data/lib/ruby_lsp/requests/support/common.rb +22 -2
  34. data/lib/ruby_lsp/requests/support/dependency_detector.rb +0 -1
  35. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +3 -8
  36. data/lib/ruby_lsp/requests/workspace_symbol.rb +6 -11
  37. data/lib/ruby_lsp/ruby_document.rb +14 -0
  38. data/lib/ruby_lsp/setup_bundler.rb +2 -0
  39. data/lib/ruby_lsp/store.rb +5 -3
  40. data/lib/ruby_lsp/utils.rb +8 -3
  41. metadata +8 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f60569fbb67f1c78fcffed58dbf540cb4e68fe10200841e54cd8a68859cf0de
4
- data.tar.gz: 07c4419c763e590e1fbed7fe14a2660c1bf6ddbd92ca791948a8c2235208f6e9
3
+ metadata.gz: d6bace664e1540c1333df9dabfb2e7bdd8bda0f910033b47b188e4a82ddacb15
4
+ data.tar.gz: be9e5da0492b26b5cb6e07074d01d7874104b70286c1578342b9639158fed147
5
5
  SHA512:
6
- metadata.gz: 5284be197a27351a845cbcfc1735a0e9546472d60f9600ffd4ee55ba3ab0998438110c1ec59f684e0cf2fbaa7ff99f89878919ae3954022372e50116182c59dd
7
- data.tar.gz: f706c17b0cd47c1001bffb42f6c9a1c43efbd390e548ffe7b51fb3c58bdb433c868a7f907a3aa32175d7742ccbb3c1c29606a0c7c979246bfe887500e6531676
6
+ metadata.gz: 7d8e61433686271a68dafe8db79765ed509a115ab6f06060d7669d279f48218fe2eada2c12e3b45f155d27a41774771681cdab92700d27f426e75ed89287ca03
7
+ data.tar.gz: 611c050b751050934d0fd93e9e52089fe63fc4d947be75a440dfa0465933814d2682268a10135d036c2cee83fa739156d2b11e42e679ecbed7de62acfee9d621
data/README.md CHANGED
@@ -52,6 +52,7 @@ The Ruby LSP provides an addon system that allows other gems to enhance the base
52
52
  features. This is the mechanism that powers addons like
53
53
 
54
54
  - [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails)
55
+ - [Ruby LSP RSpec](https://github.com/st0012/ruby-lsp-rspec)
55
56
 
56
57
  For instructions on how to create addons, see the [addons documentation](ADDONS.md).
57
58
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.12.2
1
+ 0.13.0
data/exe/ruby-lsp-check CHANGED
@@ -17,8 +17,6 @@ end
17
17
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
18
18
  require "ruby_lsp/internal"
19
19
 
20
- RubyLsp::Addon.load_addons
21
-
22
20
  T::Utils.run_all_sig_blocks
23
21
 
24
22
  files = Dir.glob("#{Dir.pwd}/**/*.rb")
@@ -28,6 +26,7 @@ puts "Verifying that all automatic LSP requests execute successfully. This may t
28
26
  errors = {}
29
27
  store = RubyLsp::Store.new
30
28
  message_queue = Thread::Queue.new
29
+ RubyLsp::Addon.load_addons(message_queue)
31
30
  executor = RubyLsp::Executor.new(store, message_queue)
32
31
 
33
32
  files.each_with_index do |file, index|
@@ -50,13 +49,30 @@ end
50
49
  puts "\n"
51
50
  message_queue.close
52
51
 
52
+ # Indexing
53
+ puts "Verifying that indexing executes successfully. This may take a while..."
54
+
55
+ index = RubyIndexer::Index.new
56
+ indexables = RubyIndexer.configuration.indexables
57
+
58
+ indexables.each_with_index do |indexable, i|
59
+ result = Prism.parse(File.read(indexable.full_path))
60
+ collector = RubyIndexer::Collector.new(index, result, indexable.full_path)
61
+ collector.collect(result.value)
62
+ rescue => e
63
+ errors[indexable.full_path] = e
64
+ ensure
65
+ print("\033[M\033[0KIndexed #{i + 1}/#{indexables.length}") unless ENV["CI"]
66
+ end
67
+ puts "\n"
68
+
53
69
  if errors.empty?
54
- puts "All automatic LSP requests executed successfully"
70
+ puts "All operations completed successfully!"
55
71
  exit
56
72
  end
57
73
 
58
74
  puts <<~ERRORS
59
- Errors while executing requests:
75
+ Errors while executing:
60
76
 
61
77
  #{errors.map { |file, error| "#{file}: #{error.message}" }.join("\n")}
62
78
  ERRORS
data/exe/ruby-lsp-doctor CHANGED
@@ -10,6 +10,6 @@ RubyIndexer.configuration.indexables.each do |indexable|
10
10
  puts "indexing: #{indexable.full_path}"
11
11
  content = File.read(indexable.full_path)
12
12
  result = Prism.parse(content)
13
- visitor = RubyIndexer::IndexVisitor.new(index, result, indexable.full_path)
14
- result.value.accept(visitor)
13
+ collector = RubyIndexer::Collector.new(index, result, indexable.full_path)
14
+ collector.collect(result.value)
15
15
  end
@@ -2,9 +2,11 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyIndexer
5
- class IndexVisitor < Prism::Visitor
5
+ class Collector
6
6
  extend T::Sig
7
7
 
8
+ LEAVE_EVENT = T.let(Object.new.freeze, Object)
9
+
8
10
  sig { params(index: Index, parse_result: Prism::ParseResult, file_path: String).void }
9
11
  def initialize(index, parse_result, file_path)
10
12
  @index = index
@@ -16,26 +18,67 @@ module RubyIndexer
16
18
  end,
17
19
  T::Hash[Integer, Prism::Comment],
18
20
  )
21
+ @queue = T.let([], T::Array[Object])
22
+ @current_owner = T.let(nil, T.nilable(Entry::Namespace))
19
23
 
20
24
  super()
21
25
  end
22
26
 
23
- sig { override.params(node: Prism::ClassNode).void }
24
- def visit_class_node(node)
25
- add_class_entry(node)
27
+ sig { params(node: Prism::Node).void }
28
+ def collect(node)
29
+ @queue = [node]
30
+
31
+ until @queue.empty?
32
+ node_or_event = @queue.shift
33
+
34
+ case node_or_event
35
+ when Prism::ProgramNode
36
+ @queue << node_or_event.statements
37
+ when Prism::StatementsNode
38
+ T.unsafe(@queue).prepend(*node_or_event.body)
39
+ when Prism::ClassNode
40
+ add_class_entry(node_or_event)
41
+ when Prism::ModuleNode
42
+ add_module_entry(node_or_event)
43
+ when Prism::MultiWriteNode
44
+ handle_multi_write_node(node_or_event)
45
+ when Prism::ConstantPathWriteNode
46
+ handle_constant_path_write_node(node_or_event)
47
+ when Prism::ConstantPathOrWriteNode
48
+ handle_constant_path_or_write_node(node_or_event)
49
+ when Prism::ConstantPathOperatorWriteNode
50
+ handle_constant_path_operator_write_node(node_or_event)
51
+ when Prism::ConstantPathAndWriteNode
52
+ handle_constant_path_and_write_node(node_or_event)
53
+ when Prism::ConstantWriteNode
54
+ handle_constant_write_node(node_or_event)
55
+ when Prism::ConstantOrWriteNode
56
+ name = fully_qualify_name(node_or_event.name.to_s)
57
+ add_constant(node_or_event, name)
58
+ when Prism::ConstantAndWriteNode
59
+ name = fully_qualify_name(node_or_event.name.to_s)
60
+ add_constant(node_or_event, name)
61
+ when Prism::ConstantOperatorWriteNode
62
+ name = fully_qualify_name(node_or_event.name.to_s)
63
+ add_constant(node_or_event, name)
64
+ when Prism::CallNode
65
+ handle_call_node(node_or_event)
66
+ when Prism::DefNode
67
+ handle_def_node(node_or_event)
68
+ when LEAVE_EVENT
69
+ @stack.pop
70
+ end
71
+ end
26
72
  end
27
73
 
28
- sig { override.params(node: Prism::ModuleNode).void }
29
- def visit_module_node(node)
30
- add_module_entry(node)
31
- end
74
+ private
32
75
 
33
- sig { override.params(node: Prism::MultiWriteNode).void }
34
- def visit_multi_write_node(node)
76
+ sig { params(node: Prism::MultiWriteNode).void }
77
+ def handle_multi_write_node(node)
35
78
  value = node.value
36
79
  values = value.is_a?(Prism::ArrayNode) && value.opening_loc ? value.elements : []
37
80
 
38
- node.targets.each_with_index do |target, i|
81
+ [*node.lefts, *node.rest, *node.rights].each_with_index do |target, i|
39
82
  current_value = values[i]
40
83
  # The moment we find a splat on the right hand side of the assignment, we can no longer figure out which value
41
84
  # gets assigned to what
@@ -50,8 +93,8 @@ module RubyIndexer
50
93
  end
51
94
  end
52
95
 
53
- sig { override.params(node: Prism::ConstantPathWriteNode).void }
54
- def visit_constant_path_write_node(node)
96
+ sig { params(node: Prism::ConstantPathWriteNode).void }
97
+ def handle_constant_path_write_node(node)
55
98
  # ignore variable constants like `var::FOO` or `self.class::FOO`
56
99
  target = node.target
57
100
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
@@ -60,8 +103,8 @@ module RubyIndexer
60
103
  add_constant(node, name)
61
104
  end
62
105
 
63
- sig { override.params(node: Prism::ConstantPathOrWriteNode).void }
64
- def visit_constant_path_or_write_node(node)
106
+ sig { params(node: Prism::ConstantPathOrWriteNode).void }
107
+ def handle_constant_path_or_write_node(node)
65
108
  # ignore variable constants like `var::FOO` or `self.class::FOO`
66
109
  target = node.target
67
110
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
@@ -70,8 +113,8 @@ module RubyIndexer
70
113
  add_constant(node, name)
71
114
  end
72
115
 
73
- sig { override.params(node: Prism::ConstantPathOperatorWriteNode).void }
74
- def visit_constant_path_operator_write_node(node)
116
+ sig { params(node: Prism::ConstantPathOperatorWriteNode).void }
117
+ def handle_constant_path_operator_write_node(node)
75
118
  # ignore variable constants like `var::FOO` or `self.class::FOO`
76
119
  target = node.target
77
120
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
@@ -80,8 +123,8 @@ module RubyIndexer
80
123
  add_constant(node, name)
81
124
  end
82
125
 
83
- sig { override.params(node: Prism::ConstantPathAndWriteNode).void }
84
- def visit_constant_path_and_write_node(node)
126
+ sig { params(node: Prism::ConstantPathAndWriteNode).void }
127
+ def handle_constant_path_and_write_node(node)
85
128
  # ignore variable constants like `var::FOO` or `self.class::FOO`
86
129
  target = node.target
87
130
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
@@ -90,50 +133,54 @@ module RubyIndexer
90
133
  add_constant(node, name)
91
134
  end
92
135
 
93
- sig { override.params(node: Prism::ConstantWriteNode).void }
94
- def visit_constant_write_node(node)
136
+ sig { params(node: Prism::ConstantWriteNode).void }
137
+ def handle_constant_write_node(node)
95
138
  name = fully_qualify_name(node.name.to_s)
96
139
  add_constant(node, name)
97
140
  end
98
141
 
99
- sig { override.params(node: Prism::ConstantOrWriteNode).void }
100
- def visit_constant_or_write_node(node)
101
- name = fully_qualify_name(node.name.to_s)
102
- add_constant(node, name)
103
- end
104
-
105
- sig { override.params(node: Prism::ConstantAndWriteNode).void }
106
- def visit_constant_and_write_node(node)
107
- name = fully_qualify_name(node.name.to_s)
108
- add_constant(node, name)
109
- end
110
-
111
- sig { override.params(node: Prism::ConstantOperatorWriteNode).void }
112
- def visit_constant_operator_write_node(node)
113
- name = fully_qualify_name(node.name.to_s)
114
- add_constant(node, name)
115
- end
116
-
117
- sig { override.params(node: Prism::CallNode).void }
118
- def visit_call_node(node)
119
- message = node.message
120
- handle_private_constant(node) if message == "private_constant"
142
+ sig { params(node: Prism::CallNode).void }
143
+ def handle_call_node(node)
144
+ message = node.name
145
+
146
+ case message
147
+ when :private_constant
148
+ handle_private_constant(node)
149
+ when :attr_reader
150
+ handle_attribute(node, reader: true, writer: false)
151
+ when :attr_writer
152
+ handle_attribute(node, reader: false, writer: true)
153
+ when :attr_accessor
154
+ handle_attribute(node, reader: true, writer: true)
155
+ end
121
156
  end
122
157
 
123
- sig { override.params(node: Prism::DefNode).void }
124
- def visit_def_node(node)
158
+ sig { params(node: Prism::DefNode).void }
159
+ def handle_def_node(node)
125
160
  method_name = node.name.to_s
126
161
  comments = collect_comments(node)
127
162
  case node.receiver
128
163
  when nil
129
- @index << Entry::InstanceMethod.new(method_name, @file_path, node.location, comments, node.parameters)
164
+ @index << Entry::InstanceMethod.new(
165
+ method_name,
166
+ @file_path,
167
+ node.location,
168
+ comments,
169
+ node.parameters,
170
+ @current_owner,
171
+ )
130
172
  when Prism::SelfNode
131
- @index << Entry::SingletonMethod.new(method_name, @file_path, node.location, comments, node.parameters)
173
+ @index << Entry::SingletonMethod.new(
174
+ method_name,
175
+ @file_path,
176
+ node.location,
177
+ comments,
178
+ node.parameters,
179
+ @current_owner,
180
+ )
132
181
  end
133
182
  end
134
183
 
135
- private
136
-
137
184
  sig { params(node: Prism::CallNode).void }
138
185
  def handle_private_constant(node)
139
186
  arguments = node.arguments&.arguments
@@ -189,12 +236,12 @@ module RubyIndexer
189
236
 
190
237
  # If the right hand side is another constant assignment, we need to visit it because that constant has to be
191
238
  # indexed too
192
- visit(value)
239
+ @queue.prepend(value)
193
240
  Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name, @file_path, node.location, comments)
194
241
  when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
195
242
  Prism::ConstantPathAndWriteNode
196
243
 
197
- visit(value)
244
+ @queue.prepend(value)
198
245
  Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name, @file_path, node.location, comments)
199
246
  else
200
247
  Entry::Constant.new(name, @file_path, node.location, comments)
@@ -204,20 +251,26 @@ module RubyIndexer
204
251
  sig { params(node: Prism::ModuleNode).void }
205
252
  def add_module_entry(node)
206
253
  name = node.constant_path.location.slice
207
- return visit_child_nodes(node) unless /^[A-Z:]/.match?(name)
254
+ unless /^[A-Z:]/.match?(name)
255
+ @queue << node.body
256
+ return
257
+ end
208
258
 
209
259
  comments = collect_comments(node)
210
-
211
- @index << Entry::Module.new(fully_qualify_name(name), @file_path, node.location, comments)
260
+ @current_owner = Entry::Module.new(fully_qualify_name(name), @file_path, node.location, comments)
261
+ @index << @current_owner
212
262
  @stack << name
213
- visit_child_nodes(node)
214
- @stack.pop
263
+ @queue.prepend(node.body, LEAVE_EVENT)
215
264
  end
216
265
 
217
266
  sig { params(node: Prism::ClassNode).void }
218
267
  def add_class_entry(node)
219
268
  name = node.constant_path.location.slice
220
- return visit_child_nodes(node) unless /^[A-Z:]/.match?(name)
269
+
270
+ unless /^[A-Z:]/.match?(name)
271
+ @queue << node.body
272
+ return
273
+ end
221
274
 
222
275
  comments = collect_comments(node)
223
276
 
@@ -227,10 +280,16 @@ module RubyIndexer
227
280
  superclass.slice
228
281
  end
229
282
 
230
- @index << Entry::Class.new(fully_qualify_name(name), @file_path, node.location, comments, parent_class)
283
+ @current_owner = Entry::Class.new(
284
+ fully_qualify_name(name),
285
+ @file_path,
286
+ node.location,
287
+ comments,
288
+ parent_class,
289
+ )
290
+ @index << @current_owner
231
291
  @stack << name
232
- visit(node.body)
233
- @stack.pop
292
+ @queue.prepend(node.body, LEAVE_EVENT)
234
293
  end
235
294
 
236
295
  sig { params(node: Prism::Node).returns(T::Array[String]) }
@@ -249,7 +308,7 @@ module RubyIndexer
249
308
 
250
309
  comment_content.delete_prefix!("#")
251
310
  comment_content.delete_prefix!(" ")
252
- comments.unshift(comment_content)
311
+ comments.prepend(comment_content)
253
312
  end
254
313
 
255
314
  comments
@@ -263,5 +322,29 @@ module RubyIndexer
263
322
  "#{@stack.join("::")}::#{name}"
264
323
  end.delete_prefix("::")
265
324
  end
325
+
326
+ sig { params(node: Prism::CallNode, reader: T::Boolean, writer: T::Boolean).void }
327
+ def handle_attribute(node, reader:, writer:)
328
+ arguments = node.arguments&.arguments
329
+ return unless arguments
330
+
331
+ receiver = node.receiver
332
+ return unless receiver.nil? || receiver.is_a?(Prism::SelfNode)
333
+
334
+ comments = collect_comments(node)
335
+ arguments.each do |argument|
336
+ name, loc = case argument
337
+ when Prism::SymbolNode
338
+ [argument.value, argument.value_loc]
339
+ when Prism::StringNode
340
+ [argument.content, argument.content_loc]
341
+ end
342
+
343
+ next unless name && loc
344
+
345
+ @index << Entry::Accessor.new(name, @file_path, loc, comments, @current_owner) if reader
346
+ @index << Entry::Accessor.new("#{name}=", @file_path, loc, comments, @current_owner) if writer
347
+ end
348
+ end
266
349
  end
267
350
  end
@@ -71,8 +71,13 @@ module RubyIndexer
71
71
 
72
72
  Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
73
73
  # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
74
- # entry is expensive, we memoize it for the entire pattern
75
- load_path_entry ||= $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
74
+ # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens
75
+ # on repositories that define multiple gems, like Rails. All frameworks are defined inside the Dir.pwd, but
76
+ # each one of them belongs to a different $LOAD_PATH entry
77
+ if load_path_entry.nil? || !path.start_with?(load_path_entry)
78
+ load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
79
+ end
80
+
76
81
  IndexablePath.new(load_path_entry, path)
77
82
  end
78
83
  end
@@ -115,7 +120,7 @@ module RubyIndexer
115
120
  IndexablePath.new(RbConfig::CONFIG["rubylibdir"], path)
116
121
  end,
117
122
  )
118
- else
123
+ elsif pathname.extname == ".rb"
119
124
  # If the default_path is a Ruby file, we index it
120
125
  indexables << IndexablePath.new(RbConfig::CONFIG["rubylibdir"], default_path)
121
126
  end
@@ -144,7 +149,7 @@ module RubyIndexer
144
149
  # just ignore if they're missing
145
150
  end
146
151
 
147
- indexables.uniq!
152
+ indexables.uniq!(&:full_path)
148
153
  indexables
149
154
  end
150
155
 
@@ -90,15 +90,67 @@ module RubyIndexer
90
90
  end
91
91
  end
92
92
 
93
+ # A required method parameter, e.g. `def foo(a)`
93
94
  class RequiredParameter < Parameter
94
95
  end
95
96
 
96
- class Method < Entry
97
+ # An optional method parameter, e.g. `def foo(a = 123)`
98
+ class OptionalParameter < Parameter
99
+ end
100
+
101
+ # An required keyword method parameter, e.g. `def foo(a:)`
102
+ class KeywordParameter < Parameter
103
+ end
104
+
105
+ # An optional keyword method parameter, e.g. `def foo(a: 123)`
106
+ class OptionalKeywordParameter < Parameter
107
+ end
108
+
109
+ class Member < Entry
110
+ extend T::Sig
111
+ extend T::Helpers
112
+
113
+ abstract!
114
+
115
+ sig { returns(T.nilable(Entry::Namespace)) }
116
+ attr_reader :owner
117
+
118
+ sig do
119
+ params(
120
+ name: String,
121
+ file_path: String,
122
+ location: Prism::Location,
123
+ comments: T::Array[String],
124
+ owner: T.nilable(Entry::Namespace),
125
+ ).void
126
+ end
127
+ def initialize(name, file_path, location, comments, owner)
128
+ super(name, file_path, location, comments)
129
+ @owner = owner
130
+ end
131
+
132
+ sig { abstract.returns(T::Array[Parameter]) }
133
+ def parameters; end
134
+ end
135
+
136
+ class Accessor < Member
137
+ extend T::Sig
138
+
139
+ sig { override.returns(T::Array[Parameter]) }
140
+ def parameters
141
+ params = []
142
+ params << RequiredParameter.new(name: name.delete_suffix("=").to_sym) if name.end_with?("=")
143
+ params
144
+ end
145
+ end
146
+
147
+ class Method < Member
97
148
  extend T::Sig
98
149
  extend T::Helpers
150
+
99
151
  abstract!
100
152
 
101
- sig { returns(T::Array[Parameter]) }
153
+ sig { override.returns(T::Array[Parameter]) }
102
154
  attr_reader :parameters
103
155
 
104
156
  sig do
@@ -108,10 +160,12 @@ module RubyIndexer
108
160
  location: Prism::Location,
109
161
  comments: T::Array[String],
110
162
  parameters_node: T.nilable(Prism::ParametersNode),
163
+ owner: T.nilable(Entry::Namespace),
111
164
  ).void
112
165
  end
113
- def initialize(name, file_path, location, comments, parameters_node)
114
- super(name, file_path, location, comments)
166
+ def initialize(name, file_path, location, comments, parameters_node, owner) # rubocop:disable Metrics/ParameterLists
167
+ super(name, file_path, location, comments, owner)
168
+
115
169
  @parameters = T.let(list_params(parameters_node), T::Array[Parameter])
116
170
  end
117
171
 
@@ -121,23 +175,46 @@ module RubyIndexer
121
175
  def list_params(parameters_node)
122
176
  return [] unless parameters_node
123
177
 
124
- parameters_node.requireds.filter_map do |required|
178
+ parameters = []
179
+
180
+ parameters_node.requireds.each do |required|
125
181
  name = parameter_name(required)
126
182
  next unless name
127
183
 
128
- RequiredParameter.new(name: name)
184
+ parameters << RequiredParameter.new(name: name)
129
185
  end
130
- end
131
186
 
132
- sig do
133
- params(node: Prism::Node).returns(T.nilable(Symbol))
187
+ parameters_node.optionals.each do |optional|
188
+ name = parameter_name(optional)
189
+ next unless name
190
+
191
+ parameters << OptionalParameter.new(name: name)
192
+ end
193
+
194
+ parameters_node.keywords.each do |keyword|
195
+ name = parameter_name(keyword)
196
+ next unless name
197
+
198
+ case keyword
199
+ when Prism::RequiredKeywordParameterNode
200
+ parameters << KeywordParameter.new(name: name)
201
+ when Prism::OptionalKeywordParameterNode
202
+ parameters << OptionalKeywordParameter.new(name: name)
203
+ end
204
+ end
205
+
206
+ parameters
134
207
  end
208
+
209
+ sig { params(node: Prism::Node).returns(T.nilable(Symbol)) }
135
210
  def parameter_name(node)
136
211
  case node
137
- when Prism::RequiredParameterNode
212
+ when Prism::RequiredParameterNode, Prism::OptionalParameterNode,
213
+ Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode
138
214
  node.name
139
- when Prism::RequiredDestructuredParameterNode
140
- names = node.parameters.map { |parameter_node| parameter_name(parameter_node) }
215
+ when Prism::MultiTargetNode
216
+ names = [*node.lefts, *node.rest, *node.rights].map { |parameter_node| parameter_name(parameter_node) }
217
+
141
218
  names_with_commas = names.join(", ")
142
219
  :"(#{names_with_commas})"
143
220
  end
@@ -93,8 +93,14 @@ module RubyIndexer
93
93
  # [#<Entry::Class name="Foo::Baz">],
94
94
  # ]
95
95
  # ```
96
- sig { params(query: String, nesting: T::Array[String]).returns(T::Array[T::Array[Entry]]) }
97
- def prefix_search(query, nesting)
96
+ sig { params(query: String, nesting: T.nilable(T::Array[String])).returns(T::Array[T::Array[Entry]]) }
97
+ def prefix_search(query, nesting = nil)
98
+ unless nesting
99
+ results = @entries_tree.search(query)
100
+ results.uniq!
101
+ return results
102
+ end
103
+
98
104
  results = nesting.length.downto(0).flat_map do |i|
99
105
  prefix = T.must(nesting[0...i]).join("::")
100
106
  namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}"
@@ -180,8 +186,8 @@ module RubyIndexer
180
186
  def index_single(indexable_path, source = nil)
181
187
  content = source || File.read(indexable_path.full_path)
182
188
  result = Prism.parse(content)
183
- visitor = IndexVisitor.new(self, result, indexable_path.full_path)
184
- result.value.accept(visitor)
189
+ collector = Collector.new(self, result, indexable_path.full_path)
190
+ collector.collect(result.value)
185
191
 
186
192
  require_path = indexable_path.require_path
187
193
  @require_paths_tree.insert(require_path, indexable_path) if require_path
@@ -231,6 +237,18 @@ module RubyIndexer
231
237
  real_parts.join("::")
232
238
  end
233
239
 
240
+ # Attempts to find a given method for a resolved fully qualified receiver name. Returns `nil` if the method does not
241
+ # exist on that receiver
242
+ sig { params(method_name: String, receiver_name: String).returns(T.nilable(Entry::Method)) }
243
+ def resolve_method(method_name, receiver_name)
244
+ method_entries = T.cast(self[method_name], T.nilable(T::Array[Entry::Method]))
245
+ owner_entries = self[receiver_name]
246
+ return unless owner_entries && method_entries
247
+
248
+ owner_name = T.must(owner_entries.first).name
249
+ method_entries.find { |entry| entry.owner&.name == owner_name }
250
+ end
251
+
234
252
  private
235
253
 
236
254
  # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
@@ -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/visitor"
8
+ require "ruby_indexer/lib/ruby_indexer/collector"
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"
@@ -84,6 +84,16 @@ module RubyIndexer
84
84
  )
85
85
  end
86
86
 
87
+ def test_indexables_does_not_include_non_ruby_files_inside_rubylibdir
88
+ path = Pathname.new(RbConfig::CONFIG["rubylibdir"]).join("extra_file.txt").to_s
89
+ FileUtils.touch(path)
90
+ indexables = @config.indexables
91
+
92
+ assert(indexables.none? { |indexable| indexable.full_path == path })
93
+ ensure
94
+ FileUtils.rm(T.must(path))
95
+ end
96
+
87
97
  def test_paths_are_unique
88
98
  @config.load_config
89
99
  indexables = @config.indexables