ruby-lsp 0.26.9 → 0.27.0.beta2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +2 -2
  4. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +8 -8
  5. data/lib/ruby_indexer/ruby_indexer.rb +0 -1
  6. data/lib/ruby_lsp/addon.rb +19 -19
  7. data/lib/ruby_lsp/global_state.rb +11 -2
  8. data/lib/ruby_lsp/internal.rb +6 -1
  9. data/lib/ruby_lsp/listeners/definition.rb +65 -99
  10. data/lib/ruby_lsp/listeners/document_link.rb +4 -0
  11. data/lib/ruby_lsp/listeners/hover.rb +258 -123
  12. data/lib/ruby_lsp/listeners/spec_style.rb +6 -1
  13. data/lib/ruby_lsp/listeners/test_discovery.rb +21 -14
  14. data/lib/ruby_lsp/listeners/test_style.rb +20 -8
  15. data/lib/ruby_lsp/node_context.rb +32 -9
  16. data/lib/ruby_lsp/requests/completion_resolve.rb +9 -13
  17. data/lib/ruby_lsp/requests/discover_tests.rb +5 -41
  18. data/lib/ruby_lsp/requests/hover.rb +2 -5
  19. data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -0
  20. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +66 -22
  21. data/lib/ruby_lsp/requests/references.rb +170 -70
  22. data/lib/ruby_lsp/requests/rename.rb +64 -72
  23. data/lib/ruby_lsp/requests/request.rb +3 -33
  24. data/lib/ruby_lsp/requests/support/common.rb +53 -0
  25. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +82 -46
  26. data/lib/ruby_lsp/requests/workspace_symbol.rb +15 -37
  27. data/lib/ruby_lsp/rubydex/declaration.rb +48 -0
  28. data/lib/ruby_lsp/rubydex/definition.rb +257 -0
  29. data/lib/ruby_lsp/rubydex/reference.rb +21 -0
  30. data/lib/ruby_lsp/server.rb +82 -8
  31. data/lib/ruby_lsp/store.rb +0 -6
  32. data/lib/ruby_lsp/test_helper.rb +3 -0
  33. data/lib/ruby_lsp/type_inferrer.rb +111 -31
  34. metadata +18 -5
  35. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +0 -335
  36. data/lib/ruby_lsp/static_docs.rb +0 -20
  37. data/static_docs/break.md +0 -103
  38. data/static_docs/yield.md +0 -81
@@ -0,0 +1,257 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Rubydex
5
+ # @abstract
6
+ class Definition
7
+ #: () -> RubyLsp::Interface::LocationLink
8
+ def to_lsp_location_link
9
+ selection_range = to_lsp_selection_range
10
+
11
+ RubyLsp::Interface::LocationLink.new(
12
+ target_uri: location.uri,
13
+ target_range: selection_range,
14
+ target_selection_range: to_lsp_name_range || selection_range,
15
+ )
16
+ end
17
+
18
+ # @abstract
19
+ #: () -> Integer
20
+ def to_lsp_kind
21
+ raise RubyLsp::AbstractMethodInvokedError
22
+ end
23
+
24
+ # Direct ancestor references contributed by this definition (superclass, includes, prepends).
25
+ # Extends are intentionally excluded here because they extend the singleton class, not the
26
+ # instance-side ancestor chain. Definition subclasses that can't contribute ancestors return [].
27
+ #: () -> Array[Rubydex::ConstantReference]
28
+ def direct_supertype_references
29
+ []
30
+ end
31
+
32
+ #: (String name, ?detail: String?) -> RubyLsp::Interface::TypeHierarchyItem
33
+ def to_lsp_type_hierarchy_item(name, detail: nil)
34
+ range = to_lsp_selection_range
35
+
36
+ RubyLsp::Interface::TypeHierarchyItem.new(
37
+ name: name,
38
+ kind: to_lsp_kind,
39
+ uri: location.uri,
40
+ range: range,
41
+ selection_range: to_lsp_name_range || range,
42
+ detail: detail,
43
+ data: { fully_qualified_name: name },
44
+ )
45
+ end
46
+
47
+ #: (String name) -> RubyLsp::Interface::WorkspaceSymbol
48
+ def to_lsp_workspace_symbol(name)
49
+ # We use the namespace as the container name, but we also use the full name as the regular name. The reason we do
50
+ # this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the short
51
+ # name `Bar`, then searching for `Foo::Bar` would not return any results
52
+ *container, _short_name = name.split("::")
53
+ container_name = container.join("::")
54
+
55
+ RubyLsp::Interface::WorkspaceSymbol.new(
56
+ name: name,
57
+ container_name: container_name,
58
+ kind: to_lsp_kind,
59
+ location: to_lsp_selection_location,
60
+ )
61
+ end
62
+
63
+ #: () -> RubyLsp::Interface::Range
64
+ def to_lsp_selection_range
65
+ loc = location
66
+
67
+ RubyLsp::Interface::Range.new(
68
+ start: RubyLsp::Interface::Position.new(line: loc.start_line, character: loc.start_column),
69
+ end: RubyLsp::Interface::Position.new(line: loc.end_line, character: loc.end_column),
70
+ )
71
+ end
72
+
73
+ #: () -> RubyLsp::Interface::Location
74
+ def to_lsp_selection_location
75
+ location = self.location
76
+
77
+ RubyLsp::Interface::Location.new(
78
+ uri: location.uri,
79
+ range: RubyLsp::Interface::Range.new(
80
+ start: RubyLsp::Interface::Position.new(line: location.start_line, character: location.start_column),
81
+ end: RubyLsp::Interface::Position.new(line: location.end_line, character: location.end_column),
82
+ ),
83
+ )
84
+ end
85
+
86
+ #: () -> RubyLsp::Interface::Range?
87
+ def to_lsp_name_range
88
+ loc = name_location
89
+ return unless loc
90
+
91
+ RubyLsp::Interface::Range.new(
92
+ start: RubyLsp::Interface::Position.new(line: loc.start_line, character: loc.start_column),
93
+ end: RubyLsp::Interface::Position.new(line: loc.end_line, character: loc.end_column),
94
+ )
95
+ end
96
+
97
+ #: () -> RubyLsp::Interface::Location?
98
+ def to_lsp_name_location
99
+ location = name_location
100
+ return unless location
101
+
102
+ RubyLsp::Interface::Location.new(
103
+ uri: location.uri,
104
+ range: RubyLsp::Interface::Range.new(
105
+ start: RubyLsp::Interface::Position.new(line: location.start_line, character: location.start_column),
106
+ end: RubyLsp::Interface::Position.new(line: location.end_line, character: location.end_column),
107
+ ),
108
+ )
109
+ end
110
+ end
111
+
112
+ # Shared supertype aggregation for Rubydex definition types that carry namespace mixins
113
+ # (`ClassDefinition`, `ModuleDefinition`, `SingletonClassDefinition`). The including class is
114
+ # expected to provide `#mixins`, which every Rubydex namespace definition already does.
115
+ # @abstract
116
+ module NamespaceDefinition
117
+ # @abstract
118
+ #: () -> Array[Rubydex::Mixin]
119
+ def mixins
120
+ raise RubyLsp::AbstractMethodInvokedError
121
+ end
122
+
123
+ #: () -> Array[Rubydex::ConstantReference]
124
+ def direct_supertype_references
125
+ mixins.filter_map do |mixin|
126
+ mixin.constant_reference if mixin.is_a?(Include) || mixin.is_a?(Prepend)
127
+ end
128
+ end
129
+ end
130
+
131
+ class ClassDefinition
132
+ include NamespaceDefinition
133
+
134
+ # @override
135
+ #: () -> Integer
136
+ def to_lsp_kind
137
+ RubyLsp::Constant::SymbolKind::CLASS
138
+ end
139
+
140
+ # @override
141
+ #: () -> Array[Rubydex::ConstantReference]
142
+ def direct_supertype_references
143
+ refs = super
144
+ superclass_ref = superclass
145
+ refs << superclass_ref if superclass_ref
146
+ refs
147
+ end
148
+ end
149
+
150
+ class ModuleDefinition
151
+ include NamespaceDefinition
152
+
153
+ # @override
154
+ #: () -> Integer
155
+ def to_lsp_kind
156
+ RubyLsp::Constant::SymbolKind::NAMESPACE
157
+ end
158
+ end
159
+
160
+ class SingletonClassDefinition
161
+ include NamespaceDefinition
162
+
163
+ # @override
164
+ #: () -> Integer
165
+ def to_lsp_kind
166
+ RubyLsp::Constant::SymbolKind::CLASS
167
+ end
168
+ end
169
+
170
+ class ConstantDefinition
171
+ # @override
172
+ #: () -> Integer
173
+ def to_lsp_kind
174
+ RubyLsp::Constant::SymbolKind::CONSTANT
175
+ end
176
+ end
177
+
178
+ class ConstantAliasDefinition
179
+ # @override
180
+ #: () -> Integer
181
+ def to_lsp_kind
182
+ RubyLsp::Constant::SymbolKind::CONSTANT
183
+ end
184
+ end
185
+
186
+ class MethodDefinition
187
+ # @override
188
+ #: () -> Integer
189
+ def to_lsp_kind
190
+ name == "initialize()" ? RubyLsp::Constant::SymbolKind::CONSTRUCTOR : RubyLsp::Constant::SymbolKind::METHOD
191
+ end
192
+ end
193
+
194
+ class MethodAliasDefinition
195
+ # @override
196
+ #: () -> Integer
197
+ def to_lsp_kind
198
+ RubyLsp::Constant::SymbolKind::METHOD
199
+ end
200
+ end
201
+
202
+ class AttrReaderDefinition
203
+ # @override
204
+ #: () -> Integer
205
+ def to_lsp_kind
206
+ RubyLsp::Constant::SymbolKind::PROPERTY
207
+ end
208
+ end
209
+
210
+ class AttrWriterDefinition
211
+ # @override
212
+ #: () -> Integer
213
+ def to_lsp_kind
214
+ RubyLsp::Constant::SymbolKind::PROPERTY
215
+ end
216
+ end
217
+
218
+ class AttrAccessorDefinition
219
+ # @override
220
+ #: () -> Integer
221
+ def to_lsp_kind
222
+ RubyLsp::Constant::SymbolKind::PROPERTY
223
+ end
224
+ end
225
+
226
+ class InstanceVariableDefinition
227
+ # @override
228
+ #: () -> Integer
229
+ def to_lsp_kind
230
+ RubyLsp::Constant::SymbolKind::FIELD
231
+ end
232
+ end
233
+
234
+ class ClassVariableDefinition
235
+ # @override
236
+ #: () -> Integer
237
+ def to_lsp_kind
238
+ RubyLsp::Constant::SymbolKind::FIELD
239
+ end
240
+ end
241
+
242
+ class GlobalVariableDefinition
243
+ # @override
244
+ #: () -> Integer
245
+ def to_lsp_kind
246
+ RubyLsp::Constant::SymbolKind::VARIABLE
247
+ end
248
+ end
249
+
250
+ class GlobalVariableAliasDefinition
251
+ # @override
252
+ #: () -> Integer
253
+ def to_lsp_kind
254
+ RubyLsp::Constant::SymbolKind::VARIABLE
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Rubydex
5
+ class Reference
6
+ #: () -> RubyLsp::Interface::Range
7
+ def to_lsp_range
8
+ loc = location
9
+
10
+ RubyLsp::Interface::Range.new(
11
+ start: RubyLsp::Interface::Position.new(line: loc.start_line, character: loc.start_column),
12
+ end: RubyLsp::Interface::Position.new(line: loc.end_line, character: loc.end_column),
13
+ )
14
+ end
15
+
16
+ #: () -> RubyLsp::Interface::Location
17
+ def to_lsp_location
18
+ RubyLsp::Interface::Location.new(uri: location.uri, range: to_lsp_range)
19
+ end
20
+ end
21
+ end
@@ -3,7 +3,11 @@
3
3
 
4
4
  module RubyLsp
5
5
  class Server < BaseServer
6
- NON_REPORTABLE_SETUP_ERRORS = [Bundler::GemNotFound, Bundler::GitError].freeze #: Array[singleton(StandardError)]
6
+ NON_REPORTABLE_SETUP_ERRORS = [
7
+ Bundler::GemNotFound,
8
+ Bundler::GitError,
9
+ Bundler::Dsl::DSLError,
10
+ ].freeze #: Array[singleton(StandardError)]
7
11
 
8
12
  # Only for testing
9
13
  #: GlobalState
@@ -120,6 +124,16 @@ module RubyLsp
120
124
  @global_state.synchronize { @cancelled_requests << message[:params][:id] }
121
125
  when nil
122
126
  process_response(message) if message[:result]
127
+ else
128
+ id = message[:id]
129
+
130
+ if id
131
+ send_message(Error.new(
132
+ id: id,
133
+ code: Constant::ErrorCodes::METHOD_NOT_FOUND,
134
+ message: "Method not found: #{message[:method]}",
135
+ ))
136
+ end
123
137
  end
124
138
  rescue DelegateRequestError
125
139
  send_message(Error.new(id: message[:id], code: DelegateRequestError::CODE, message: "DELEGATE_REQUEST"))
@@ -415,7 +429,19 @@ module RubyLsp
415
429
  params = message[:params]
416
430
  text_document = params[:textDocument]
417
431
 
418
- @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
432
+ document = @store.get(text_document[:uri])
433
+ document.push_edits(params[:contentChanges], version: text_document[:version])
434
+
435
+ language_id = document.language_id
436
+
437
+ if [:ruby, :rbs].include?(language_id)
438
+ graph = @global_state.graph
439
+
440
+ benchmark("index_source") do
441
+ graph.index_source(text_document[:uri].to_s, document.source, language_id.to_s)
442
+ end
443
+ benchmark("incremental_resolve") { graph.resolve }
444
+ end
419
445
  end
420
446
 
421
447
  #: (Hash[Symbol, untyped] message) -> void
@@ -1033,6 +1059,23 @@ module RubyLsp
1033
1059
  # is fine, but we shouldn't process the same file changes more than once
1034
1060
  changes.uniq!
1035
1061
 
1062
+ graph = @global_state.graph
1063
+
1064
+ # Handle deletions and accumulate additions and changes for indexing
1065
+ additions_and_changes = changes.each_with_object([]) do |change, acc|
1066
+ if change[:type] == Constant::FileChangeType::DELETED
1067
+ graph.delete_document(change[:uri])
1068
+ else
1069
+ path = URI(change[:uri]).to_standardized_path
1070
+ next if path.nil?
1071
+ next unless File.directory?(path) || [".rb", ".rbs"].include?(File.extname(path))
1072
+
1073
+ acc << path
1074
+ end
1075
+ end
1076
+ benchmark("index_all") { graph.index_all(additions_and_changes) }
1077
+ benchmark("incremental_resolve") { graph.resolve }
1078
+
1036
1079
  index = @global_state.index
1037
1080
  changes.each do |change|
1038
1081
  # File change events include folders, but we're only interested in files
@@ -1169,7 +1212,7 @@ module RubyLsp
1169
1212
 
1170
1213
  response = Requests::PrepareTypeHierarchy.new(
1171
1214
  document,
1172
- @global_state.index,
1215
+ @global_state,
1173
1216
  params[:position],
1174
1217
  ).perform
1175
1218
 
@@ -1179,7 +1222,7 @@ module RubyLsp
1179
1222
  #: (Hash[Symbol, untyped] message) -> void
1180
1223
  def type_hierarchy_supertypes(message)
1181
1224
  response = Requests::TypeHierarchySupertypes.new(
1182
- @global_state.index,
1225
+ @global_state,
1183
1226
  message.dig(:params, :item),
1184
1227
  ).perform
1185
1228
  send_message(Result.new(id: message[:id], response: response))
@@ -1228,12 +1271,18 @@ module RubyLsp
1228
1271
 
1229
1272
  #: -> void
1230
1273
  def perform_initial_indexing
1274
+ progress("indexing-progress", message: "Indexing workspace...")
1275
+ benchmark("index_workspace") { @global_state.graph.index_workspace }
1276
+
1277
+ progress("indexing-progress", message: "Resolving graph...")
1278
+ benchmark("full_resolve") { @global_state.graph.resolve }
1279
+
1231
1280
  # The begin progress invocation happens during `initialize`, so that the notification is sent before we are
1232
1281
  # stuck indexing files
1233
1282
  Thread.new do
1234
1283
  begin
1235
1284
  @global_state.index.index_all do |percentage|
1236
- progress("indexing-progress", percentage)
1285
+ progress("indexing-progress", percentage: percentage)
1237
1286
  true
1238
1287
  rescue ClosedQueueError
1239
1288
  # Since we run indexing on a separate thread, it's possible to kill the server before indexing is complete.
@@ -1287,11 +1336,13 @@ module RubyLsp
1287
1336
  send_message(Notification.progress_begin(id, title, percentage: percentage, message: "#{percentage}% completed"))
1288
1337
  end
1289
1338
 
1290
- #: (String id, Integer percentage) -> void
1291
- def progress(id, percentage)
1339
+ #: (String, ?message: String?, ?percentage: Integer?) -> void
1340
+ def progress(id, message: nil, percentage: nil)
1292
1341
  return unless @global_state.client_capabilities.supports_progress
1293
1342
 
1294
- send_message(Notification.progress_report(id, percentage: percentage, message: "#{percentage}% completed"))
1343
+ message ||= "#{percentage}% completed" if percentage
1344
+
1345
+ send_message(Notification.progress_report(id, percentage: percentage, message: message))
1295
1346
  end
1296
1347
 
1297
1348
  #: (String id) -> void
@@ -1524,5 +1575,28 @@ module RubyLsp
1524
1575
  response: code_lens,
1525
1576
  ))
1526
1577
  end
1578
+
1579
+ #: [T] (String) { () -> T } -> T
1580
+ def benchmark(label, &block)
1581
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
1582
+ result = block.call
1583
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
1584
+
1585
+ send_message(Notification.telemetry({
1586
+ type: "data",
1587
+ eventName: "ruby_lsp.response_time",
1588
+ data: {
1589
+ type: "histogram",
1590
+ value: duration,
1591
+ attributes: {
1592
+ message: label,
1593
+ lspVersion: RubyLsp::VERSION,
1594
+ rubyVersion: RUBY_VERSION,
1595
+ },
1596
+ },
1597
+ }))
1598
+
1599
+ result
1600
+ end
1527
1601
  end
1528
1602
  end
@@ -53,12 +53,6 @@ module RubyLsp
53
53
  end
54
54
  end
55
55
 
56
- #: (uri: URI::Generic, edits: Array[Hash[Symbol, untyped]], version: Integer) -> void
57
- def push_edits(uri:, edits:, version:)
58
- @state[uri.to_s] #: as !nil
59
- .push_edits(edits, version: version)
60
- end
61
-
62
56
  #: -> void
63
57
  def clear
64
58
  @state.clear
@@ -30,6 +30,9 @@ module RubyLsp
30
30
  })
31
31
 
32
32
  server.global_state.index.index_single(uri, source)
33
+ graph = server.global_state.graph
34
+ graph.index_source(uri.to_s, source, "ruby")
35
+ graph.resolve
33
36
  end
34
37
 
35
38
  server.load_addons(include_project_addons: false) if load_addons
@@ -5,9 +5,9 @@ module RubyLsp
5
5
  # A minimalistic type checker to try to resolve types that can be inferred without requiring a type system or
6
6
  # annotations
7
7
  class TypeInferrer
8
- #: (RubyIndexer::Index index) -> void
9
- def initialize(index)
10
- @index = index
8
+ #: (Rubydex::Graph) -> void
9
+ def initialize(graph)
10
+ @graph = graph
11
11
  end
12
12
 
13
13
  #: (NodeContext node_context) -> Type?
@@ -19,7 +19,7 @@ module RubyLsp
19
19
  infer_receiver_for_call_node(node, node_context)
20
20
  when Prism::InstanceVariableReadNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableWriteNode,
21
21
  Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableTargetNode,
22
- Prism::SuperNode, Prism::ForwardingSuperNode
22
+ Prism::SuperNode, Prism::ForwardingSuperNode, Prism::DefNode
23
23
  self_receiver_handling(node_context)
24
24
  when Prism::ClassVariableAndWriteNode, Prism::ClassVariableWriteNode, Prism::ClassVariableOperatorWriteNode,
25
25
  Prism::ClassVariableOrWriteNode, Prism::ClassVariableReadNode, Prism::ClassVariableTargetNode
@@ -81,14 +81,13 @@ module RubyLsp
81
81
  receiver_name = RubyIndexer::Index.constant_name(receiver)
82
82
  return unless receiver_name
83
83
 
84
- resolved_receiver = @index.resolve(receiver_name, node_context.nesting)
85
- name = resolved_receiver&.first&.name
86
- return unless name
84
+ resolved_receiver = @graph.resolve_constant(receiver_name, node_context.nesting)
85
+ return unless resolved_receiver
87
86
 
88
- *parts, last = name.split("::")
89
- return Type.new("#{last}::<Class:#{last}>") if parts.empty?
87
+ *parts, last = resolved_receiver.name.split("::")
88
+ return Type.new("#{last}::<#{last}>") if parts.empty?
90
89
 
91
- Type.new("#{parts.join("::")}::#{last}::<Class:#{last}>")
90
+ Type.new("#{parts.join("::")}::#{last}::<#{last}>")
92
91
  when Prism::CallNode
93
92
  raw_receiver = receiver.message
94
93
 
@@ -96,12 +95,14 @@ module RubyLsp
96
95
  # When invoking `new`, we recursively infer the type of the receiver to get the class type its being invoked
97
96
  # on and then return the attached version of that type, since it's being instantiated.
98
97
  type = infer_receiver_for_call_node(receiver, node_context)
99
-
100
98
  return unless type
101
99
 
102
100
  # If the method `new` was overridden, then we cannot assume that it will return a new instance of the class
103
- new_method = @index.resolve_method("new", type.name)&.first
104
- return if new_method && new_method.owner&.name != "Class"
101
+ declaration = @graph[type.name] #: as Rubydex::Namespace?
102
+ return unless declaration
103
+
104
+ new_method = declaration.find_member("new()")
105
+ return if new_method && new_method.owner.name != "Class"
105
106
 
106
107
  type.attached
107
108
  elsif raw_receiver
@@ -121,45 +122,124 @@ module RubyLsp
121
122
  .map(&:capitalize)
122
123
  .join
123
124
 
124
- entries = @index.resolve(guessed_name, nesting) || @index.first_unqualified_const(guessed_name)
125
- name = entries&.first&.name
126
- return unless name
125
+ declaration = @graph.resolve_constant(guessed_name, nesting)
126
+ declaration ||= @graph.search(guessed_name).first
127
+ return unless declaration
127
128
 
128
- GuessedType.new(name)
129
+ GuessedType.new(declaration.name)
129
130
  end
130
131
 
131
- #: (NodeContext node_context) -> Type
132
+ #: (NodeContext node_context) -> Type?
132
133
  def self_receiver_handling(node_context)
133
134
  nesting = node_context.nesting
134
135
  # If we're at the top level, then the invocation is happening on `<main>`, which is a special singleton that
135
136
  # inherits from Object
136
137
  return Type.new("Object") if nesting.empty?
137
- return Type.new(node_context.fully_qualified_name) if node_context.surrounding_method
138
+
139
+ surrounding_method = node_context.surrounding_method
140
+
141
+ if surrounding_method
142
+ receiver_name = surrounding_method.receiver
143
+
144
+ case receiver_name
145
+ when "self"
146
+ # `def self.foo` — self is the singleton of the enclosing class/module
147
+ return resolve_singleton_type_from_nesting(nesting)
148
+ when "none"
149
+ # Instance method — self is an instance of the enclosing class/module
150
+ return resolve_type_from_nesting(nesting)
151
+ when nil
152
+ # Dynamic receiver that we cannot handle
153
+ return
154
+ else
155
+ # Explicit constant receiver (e.g. `def Bar.baz`) — self is that constant's singleton class
156
+ resolved = resolve_receiver_singleton_type(receiver_name, nesting)
157
+ return resolved if resolved
158
+
159
+ return resolve_type_from_nesting(nesting)
160
+ end
161
+ end
138
162
 
139
163
  # If we're not inside a method, then we're inside the body of a class or module, which is a singleton
140
- # context.
141
- #
142
- # If the class/module definition is using compact style (e.g.: `class Foo::Bar`), then we need to split the name
143
- # into its individual parts to build the correct singleton name
144
- parts = nesting.flat_map { |part| part.split("::") }
145
- Type.new("#{parts.join("::")}::<Class:#{parts.last}>")
164
+ # context. Resolve through the graph to get the correct fully qualified name
165
+ resolve_singleton_type_from_nesting(nesting)
166
+ end
167
+
168
+ # Resolves the fully qualified name of the innermost constant from the nesting and returns it as a type.
169
+ # For instance methods, the nesting won't have singleton markers, so the result is an instance type.
170
+ # For `def self.` methods, the nesting includes a singleton marker, which is preserved in the result.
171
+ #: (Array[String] nesting) -> Type
172
+ def resolve_type_from_nesting(nesting)
173
+ resolved_name = resolve_nesting_fully_qualified_name(nesting)
174
+ Type.new(resolved_name)
175
+ end
176
+
177
+ # Resolves the nesting and returns a singleton type (appends `::<Last>`)
178
+ #: (Array[String] nesting) -> Type
179
+ def resolve_singleton_type_from_nesting(nesting)
180
+ resolved_name = resolve_nesting_fully_qualified_name(nesting)
181
+ last_part = resolved_name.split("::").last #: as !nil
182
+ Type.new("#{resolved_name}::<#{last_part}>")
183
+ end
184
+
185
+ # Resolves the innermost constant in the nesting through the graph, handling compact-path definitions
186
+ # like `class Bar::Baz` inside a different module where the lexical nesting doesn't reflect the true
187
+ # constant hierarchy. Falls back to lexical joining if resolution fails.
188
+ #: (Array[String] nesting) -> String
189
+ def resolve_nesting_fully_qualified_name(nesting)
190
+ nesting_parts = nesting.dup
191
+ trailing_singletons = [] #: Array[String]
192
+
193
+ nesting_parts.reverse_each do |part|
194
+ break unless part.start_with?("<")
195
+
196
+ popped = nesting_parts.pop #: as !nil
197
+ trailing_singletons.unshift(popped)
198
+ end
199
+
200
+ if nesting_parts.any?
201
+ resolved = @graph.resolve_constant(
202
+ nesting_parts.last, #: as !nil
203
+ nesting_parts[0...-1], #: as !nil
204
+ )
205
+
206
+ if resolved
207
+ parts = resolved.name.split("::") + trailing_singletons
208
+ return parts.join("::")
209
+ end
210
+ end
211
+
212
+ # Fallback to lexical joining if resolution fails
213
+ nesting.flat_map { |part| part.split("::") }.join("::")
214
+ end
215
+
216
+ #: (String, Array[String]) -> Type?
217
+ def resolve_receiver_singleton_type(receiver_name, nesting)
218
+ receiver_declaration = @graph.resolve_constant(receiver_name, nesting)
219
+ return unless receiver_declaration.is_a?(Rubydex::Namespace)
220
+
221
+ singleton = receiver_declaration.singleton_class
222
+ return unless singleton
223
+
224
+ Type.new(singleton.name)
146
225
  end
147
226
 
148
227
  #: (NodeContext node_context) -> Type?
149
228
  def infer_receiver_for_class_variables(node_context)
150
229
  nesting_parts = node_context.nesting.dup
151
-
152
230
  return Type.new("Object") if nesting_parts.empty?
153
231
 
154
232
  nesting_parts.reverse_each do |part|
155
- break unless part.include?("<Class:")
233
+ break unless part.start_with?("<")
156
234
 
157
235
  nesting_parts.pop
158
236
  end
159
237
 
160
- receiver_name = nesting_parts.join("::")
161
- resolved_receiver = @index.resolve(receiver_name, node_context.nesting)&.first
162
- return unless resolved_receiver&.name
238
+ resolved_receiver = @graph.resolve_constant(
239
+ nesting_parts.last, #: as !nil
240
+ nesting_parts[0...-1], #: as !nil
241
+ )
242
+ return unless resolved_receiver
163
243
 
164
244
  Type.new(resolved_receiver.name)
165
245
  end
@@ -174,7 +254,7 @@ module RubyLsp
174
254
  @name = name
175
255
  end
176
256
 
177
- # Returns the attached version of this type by removing the `<Class:...>` part from its name
257
+ # Returns the attached version of this type by removing the `<...>` part from its name
178
258
  #: -> Type
179
259
  def attached
180
260
  Type.new(