ruby-lsp 0.17.8 → 0.17.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3a8a120b4cf3045cdeec98d2fb417ffdd300947b6426788d827346c43812ffc
4
- data.tar.gz: b43a823e71fca99a9cf79b3a6ce19e7a0ec86e499b943aecae1666788859d055
3
+ metadata.gz: f3deccbc9952a61d46392be2f050a4684b108e7cd3da0085a4e8997168dfc021
4
+ data.tar.gz: d1b9a804d9e67aa2d23b39f12c79c6e1d5ba940ec360170623f7e0870e7d6c78
5
5
  SHA512:
6
- metadata.gz: 9662d88d3f37b4fd7b744a69f38f2a20cd4bf00291f63968578e50bef3400916870efdcf58b6e81dd78a6829c4f8a7dd174e6d212ddc3a265420b29eaa2b6d8a
7
- data.tar.gz: b788ef9185e02a732cd2dccef6273d8dd3fb471ebb8c2300b3ac78cc48d3952eee378a777cc4cf4ceb927590fff7bcdfc57c95f0924b21e589eb850ec9128641
6
+ metadata.gz: fe2e99145d0f268644a5d8feeff49146e635998ab8ff3eff18f47b1f555bb249b296966d0a431c930ab6ebca58032ed954a338f7c075300869545711bcfae50e
7
+ data.tar.gz: 36f253a202755a49e33b8d6c4906aa0acfebe9fe52772519684d433649d7fbadfee186274eba94027017535e55438333759477e2dbb87a7da7e9ead0bc98aaa7
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.8
1
+ 0.17.9
@@ -84,6 +84,34 @@ module RubyIndexer
84
84
  @require_paths_tree.search(query)
85
85
  end
86
86
 
87
+ # Searches for a constant based on an unqualified name and returns the first possible match regardless of whether
88
+ # there are more possible matching entries
89
+ sig do
90
+ params(
91
+ name: String,
92
+ ).returns(T.nilable(T::Array[T.any(
93
+ Entry::Namespace,
94
+ Entry::Alias,
95
+ Entry::UnresolvedAlias,
96
+ Entry::Constant,
97
+ )]))
98
+ end
99
+ def first_unqualified_const(name)
100
+ _name, entries = @entries.find do |const_name, _entries|
101
+ const_name.end_with?(name)
102
+ end
103
+
104
+ T.cast(
105
+ entries,
106
+ T.nilable(T::Array[T.any(
107
+ Entry::Namespace,
108
+ Entry::Alias,
109
+ Entry::UnresolvedAlias,
110
+ Entry::Constant,
111
+ )]),
112
+ )
113
+ end
114
+
87
115
  # Searches entries in the index based on an exact prefix, intended for providing autocomplete. All possible matches
88
116
  # to the prefix are returned. The return is an array of arrays, where each entry is the array of entries for a given
89
117
  # name match. For example:
@@ -37,45 +37,33 @@ module RubyIndexer
37
37
  sig { params(declaration: RBS::AST::Declarations::Base, pathname: Pathname).void }
38
38
  def process_declaration(declaration, pathname)
39
39
  case declaration
40
- when RBS::AST::Declarations::Class
41
- handle_class_declaration(declaration, pathname)
42
- when RBS::AST::Declarations::Module
43
- handle_module_declaration(declaration, pathname)
40
+ when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module
41
+ handle_class_or_module_declaration(declaration, pathname)
44
42
  else # rubocop:disable Style/EmptyElse
45
43
  # Other kinds not yet handled
46
44
  end
47
45
  end
48
46
 
49
- sig { params(declaration: RBS::AST::Declarations::Class, pathname: Pathname).void }
50
- def handle_class_declaration(declaration, pathname)
51
- nesting = [declaration.name.name.to_s]
52
- file_path = pathname.to_s
53
- location = to_ruby_indexer_location(declaration.location)
54
- comments = Array(declaration.comment&.string)
55
- parent_class = declaration.super_class&.name&.name&.to_s
56
- class_entry = Entry::Class.new(nesting, file_path, location, location, comments, parent_class)
57
- add_declaration_mixins_to_entry(declaration, class_entry)
58
- @index.add(class_entry)
59
- declaration.members.each do |member|
60
- next unless member.is_a?(RBS::AST::Members::MethodDefinition)
61
-
62
- handle_method(member, class_entry)
63
- end
47
+ sig do
48
+ params(declaration: T.any(RBS::AST::Declarations::Class, RBS::AST::Declarations::Module), pathname: Pathname).void
64
49
  end
65
-
66
- sig { params(declaration: RBS::AST::Declarations::Module, pathname: Pathname).void }
67
- def handle_module_declaration(declaration, pathname)
50
+ def handle_class_or_module_declaration(declaration, pathname)
68
51
  nesting = [declaration.name.name.to_s]
69
52
  file_path = pathname.to_s
70
53
  location = to_ruby_indexer_location(declaration.location)
71
54
  comments = Array(declaration.comment&.string)
72
- module_entry = Entry::Module.new(nesting, file_path, location, location, comments)
73
- add_declaration_mixins_to_entry(declaration, module_entry)
74
- @index.add(module_entry)
55
+ entry = if declaration.is_a?(RBS::AST::Declarations::Class)
56
+ parent_class = declaration.super_class&.name&.name&.to_s
57
+ Entry::Class.new(nesting, file_path, location, location, comments, parent_class)
58
+ else
59
+ Entry::Module.new(nesting, file_path, location, location, comments)
60
+ end
61
+ add_declaration_mixins_to_entry(declaration, entry)
62
+ @index.add(entry)
75
63
  declaration.members.each do |member|
76
64
  next unless member.is_a?(RBS::AST::Members::MethodDefinition)
77
65
 
78
- handle_method(member, module_entry)
66
+ handle_method(member, entry)
79
67
  end
80
68
  end
81
69
 
@@ -1542,6 +1542,21 @@ module RubyIndexer
1542
1542
  assert_empty(@index.method_completion_candidates("bar", "Foo"))
1543
1543
  end
1544
1544
 
1545
+ def test_first_unqualified_const
1546
+ index(<<~RUBY)
1547
+ module Foo
1548
+ class Bar; end
1549
+ end
1550
+
1551
+ module Baz
1552
+ class Bar; end
1553
+ end
1554
+ RUBY
1555
+
1556
+ entry = T.must(@index.first_unqualified_const("Bar")&.first)
1557
+ assert_equal("Foo::Bar", entry.name)
1558
+ end
1559
+
1545
1560
  def test_completion_does_not_duplicate_overridden_methods
1546
1561
  index(<<~RUBY)
1547
1562
  class Foo
@@ -10,6 +10,16 @@ module RubyLsp
10
10
  end
11
11
  end
12
12
 
13
+ class SorbetLevel < T::Enum
14
+ enums do
15
+ None = new("none")
16
+ Ignore = new("ignore")
17
+ False = new("false")
18
+ True = new("true")
19
+ Strict = new("strict")
20
+ end
21
+ end
22
+
13
23
  extend T::Sig
14
24
  extend T::Helpers
15
25
 
@@ -213,10 +223,23 @@ module RubyLsp
213
223
  NodeContext.new(closest, parent, nesting_nodes, call_node)
214
224
  end
215
225
 
216
- sig { returns(T::Boolean) }
217
- def sorbet_sigil_is_true_or_higher
218
- parse_result.magic_comments.any? do |comment|
219
- comment.key == "typed" && ["true", "strict", "strong"].include?(comment.value)
226
+ sig { returns(SorbetLevel) }
227
+ def sorbet_level
228
+ sigil = parse_result.magic_comments.find do |comment|
229
+ comment.key == "typed"
230
+ end&.value
231
+
232
+ case sigil
233
+ when "ignore"
234
+ SorbetLevel::Ignore
235
+ when "false"
236
+ SorbetLevel::False
237
+ when "true"
238
+ SorbetLevel::True
239
+ when "strict", "strong"
240
+ SorbetLevel::Strict
241
+ else
242
+ SorbetLevel::None
220
243
  end
221
244
  end
222
245
 
@@ -36,10 +36,10 @@ module RubyLsp
36
36
  @test_library = T.let("minitest", String)
37
37
  @has_type_checker = T.let(true, T::Boolean)
38
38
  @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
39
- @type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
40
39
  @supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
41
40
  @supports_watching_files = T.let(false, T::Boolean)
42
41
  @experimental_features = T.let(false, T::Boolean)
42
+ @type_inferrer = T.let(TypeInferrer.new(@index, @experimental_features), TypeInferrer)
43
43
  end
44
44
 
45
45
  sig { params(identifier: String, instance: Requests::Support::Formatter).void }
@@ -90,6 +90,7 @@ module RubyLsp
90
90
  end
91
91
 
92
92
  @experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
93
+ @type_inferrer.experimental_features = @experimental_features
93
94
  end
94
95
 
95
96
  sig { returns(String) }
@@ -56,7 +56,7 @@ module RubyLsp
56
56
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
57
57
  global_state: GlobalState,
58
58
  node_context: NodeContext,
59
- typechecker_enabled: T::Boolean,
59
+ sorbet_level: Document::SorbetLevel,
60
60
  dispatcher: Prism::Dispatcher,
61
61
  uri: URI::Generic,
62
62
  trigger_character: T.nilable(String),
@@ -66,7 +66,7 @@ module RubyLsp
66
66
  response_builder,
67
67
  global_state,
68
68
  node_context,
69
- typechecker_enabled,
69
+ sorbet_level,
70
70
  dispatcher,
71
71
  uri,
72
72
  trigger_character
@@ -76,7 +76,7 @@ module RubyLsp
76
76
  @index = T.let(global_state.index, RubyIndexer::Index)
77
77
  @type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
78
78
  @node_context = node_context
79
- @typechecker_enabled = typechecker_enabled
79
+ @sorbet_level = sorbet_level
80
80
  @uri = uri
81
81
  @trigger_character = trigger_character
82
82
 
@@ -97,7 +97,9 @@ module RubyLsp
97
97
  # Handle completion on regular constant references (e.g. `Bar`)
98
98
  sig { params(node: Prism::ConstantReadNode).void }
99
99
  def on_constant_read_node_enter(node)
100
- return if @global_state.has_type_checker
100
+ # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
101
+ # no sigil, Sorbet will still provide completion for constants
102
+ return if @sorbet_level != Document::SorbetLevel::Ignore
101
103
 
102
104
  name = constant_name(node)
103
105
  return if name.nil?
@@ -118,7 +120,9 @@ module RubyLsp
118
120
  # Handle completion on namespaced constant references (e.g. `Foo::Bar`)
119
121
  sig { params(node: Prism::ConstantPathNode).void }
120
122
  def on_constant_path_node_enter(node)
121
- return if @global_state.has_type_checker
123
+ # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
124
+ # no sigil, Sorbet will still provide completion for constants
125
+ return if @sorbet_level != Document::SorbetLevel::Ignore
122
126
 
123
127
  name = constant_name(node)
124
128
  return if name.nil?
@@ -128,28 +132,32 @@ module RubyLsp
128
132
 
129
133
  sig { params(node: Prism::CallNode).void }
130
134
  def on_call_node_enter(node)
131
- receiver = node.receiver
132
-
133
- # When writing `Foo::`, the AST assigns a method call node (because you can use that syntax to invoke singleton
134
- # methods). However, in addition to providing method completion, we also need to show possible constant
135
- # completions
136
- if (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)) &&
137
- node.call_operator == "::"
138
-
139
- name = constant_name(receiver)
140
-
141
- if name
142
- start_loc = node.location
143
- end_loc = T.must(node.call_operator_loc)
144
-
145
- constant_path_completion(
146
- "#{name}::",
147
- Interface::Range.new(
148
- start: Interface::Position.new(line: start_loc.start_line - 1, character: start_loc.start_column),
149
- end: Interface::Position.new(line: end_loc.end_line - 1, character: end_loc.end_column),
150
- ),
151
- )
152
- return
135
+ # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
136
+ # no sigil, Sorbet will still provide completion for constants
137
+ if @sorbet_level == Document::SorbetLevel::Ignore
138
+ receiver = node.receiver
139
+
140
+ # When writing `Foo::`, the AST assigns a method call node (because you can use that syntax to invoke
141
+ # singleton methods). However, in addition to providing method completion, we also need to show possible
142
+ # constant completions
143
+ if (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)) &&
144
+ node.call_operator == "::"
145
+
146
+ name = constant_name(receiver)
147
+
148
+ if name
149
+ start_loc = node.location
150
+ end_loc = T.must(node.call_operator_loc)
151
+
152
+ constant_path_completion(
153
+ "#{name}::",
154
+ Interface::Range.new(
155
+ start: Interface::Position.new(line: start_loc.start_line - 1, character: start_loc.start_column),
156
+ end: Interface::Position.new(line: end_loc.end_line - 1, character: end_loc.end_column),
157
+ ),
158
+ )
159
+ return
160
+ end
153
161
  end
154
162
  end
155
163
 
@@ -162,7 +170,7 @@ module RubyLsp
162
170
  when "require_relative"
163
171
  complete_require_relative(node)
164
172
  else
165
- complete_methods(node, name) unless @typechecker_enabled
173
+ complete_methods(node, name)
166
174
  end
167
175
  end
168
176
 
@@ -247,10 +255,14 @@ module RubyLsp
247
255
 
248
256
  sig { params(name: String, location: Prism::Location).void }
249
257
  def handle_instance_variable_completion(name, location)
258
+ # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
259
+ # to provide all features for them
260
+ return if @sorbet_level == Document::SorbetLevel::Strict
261
+
250
262
  type = @type_inferrer.infer_receiver_type(@node_context)
251
263
  return unless type
252
264
 
253
- @index.instance_variable_completion_candidates(name, type).each do |entry|
265
+ @index.instance_variable_completion_candidates(name, type.name).each do |entry|
254
266
  variable_name = entry.name
255
267
 
256
268
  label_details = Interface::CompletionItemLabelDetails.new(
@@ -321,12 +333,16 @@ module RubyLsp
321
333
 
322
334
  sig { params(node: Prism::CallNode, name: String).void }
323
335
  def complete_methods(node, name)
324
- # If the node has a receiver, then we don't need to provide local nor keyword completions
325
- if !@global_state.has_type_checker && !node.receiver
336
+ # If the node has a receiver, then we don't need to provide local nor keyword completions. Sorbet can provide
337
+ # local and keyword completion for any file with a Sorbet level of true or higher
338
+ if !sorbet_level_true_or_higher?(@sorbet_level) && !node.receiver
326
339
  add_local_completions(node, name)
327
340
  add_keyword_completions(node, name)
328
341
  end
329
342
 
343
+ # Sorbet can provide completion for methods invoked on self on typed true or higher files
344
+ return if sorbet_level_true_or_higher?(@sorbet_level) && self_receiver?(node)
345
+
330
346
  type = @type_inferrer.infer_receiver_type(@node_context)
331
347
  return unless type
332
348
 
@@ -350,8 +366,11 @@ module RubyLsp
350
366
 
351
367
  return unless range
352
368
 
353
- @index.method_completion_candidates(method_name, type).each do |entry|
369
+ guessed_type = type.name
370
+
371
+ @index.method_completion_candidates(method_name, type.name).each do |entry|
354
372
  entry_name = entry.name
373
+ owner_name = entry.owner&.name
355
374
 
356
375
  label_details = Interface::CompletionItemLabelDetails.new(
357
376
  description: entry.file_name,
@@ -364,7 +383,8 @@ module RubyLsp
364
383
  text_edit: Interface::TextEdit.new(range: range, new_text: entry_name),
365
384
  kind: Constant::CompletionItemKind::METHOD,
366
385
  data: {
367
- owner_name: entry.owner&.name,
386
+ owner_name: owner_name,
387
+ guessed_type: guessed_type,
368
388
  },
369
389
  )
370
390
  end
@@ -20,10 +20,10 @@ module RubyLsp
20
20
  uri: URI::Generic,
21
21
  node_context: NodeContext,
22
22
  dispatcher: Prism::Dispatcher,
23
- typechecker_enabled: T::Boolean,
23
+ sorbet_level: Document::SorbetLevel,
24
24
  ).void
25
25
  end
26
- def initialize(response_builder, global_state, language_id, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
26
+ def initialize(response_builder, global_state, language_id, uri, node_context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
27
27
  @response_builder = response_builder
28
28
  @global_state = global_state
29
29
  @index = T.let(global_state.index, RubyIndexer::Index)
@@ -31,7 +31,7 @@ module RubyLsp
31
31
  @language_id = language_id
32
32
  @uri = uri
33
33
  @node_context = node_context
34
- @typechecker_enabled = typechecker_enabled
34
+ @sorbet_level = sorbet_level
35
35
 
36
36
  dispatcher.register(
37
37
  self,
@@ -53,6 +53,11 @@ module RubyLsp
53
53
 
54
54
  sig { params(node: Prism::CallNode).void }
55
55
  def on_call_node_enter(node)
56
+ # Sorbet can handle go to definition for methods invoked on self on typed true or higher
57
+ if (@sorbet_level == Document::SorbetLevel::True || @sorbet_level == Document::SorbetLevel::Strict) &&
58
+ self_receiver?(node)
59
+ end
60
+
56
61
  message = node.message
57
62
  return unless message
58
63
 
@@ -60,7 +65,7 @@ module RubyLsp
60
65
 
61
66
  # Until we can properly infer the receiver type in erb files (maybe with ruby-lsp-rails),
62
67
  # treating method calls' type as `nil` will allow users to get some completion support first
63
- if @language_id == Document::LanguageId::ERB && inferrer_receiver_type == "Object"
68
+ if @language_id == Document::LanguageId::ERB && inferrer_receiver_type&.name == "Object"
64
69
  inferrer_receiver_type = nil
65
70
  end
66
71
 
@@ -149,6 +154,9 @@ module RubyLsp
149
154
 
150
155
  sig { void }
151
156
  def handle_super_node_definition
157
+ # Sorbet can handle super hover on typed true or higher
158
+ return if sorbet_level_true_or_higher?(@sorbet_level)
159
+
152
160
  surrounding_method = @node_context.surrounding_method
153
161
  return unless surrounding_method
154
162
 
@@ -161,10 +169,14 @@ module RubyLsp
161
169
 
162
170
  sig { params(name: String).void }
163
171
  def handle_instance_variable_definition(name)
172
+ # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
173
+ # to provide all features for them
174
+ return if @sorbet_level == Document::SorbetLevel::Strict
175
+
164
176
  type = @type_inferrer.infer_receiver_type(@node_context)
165
177
  return unless type
166
178
 
167
- entries = @index.resolve_instance_variable(name, type)
179
+ entries = @index.resolve_instance_variable(name, type.name)
168
180
  return unless entries
169
181
 
170
182
  entries.each do |entry|
@@ -182,10 +194,10 @@ module RubyLsp
182
194
  # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
183
195
  end
184
196
 
185
- sig { params(message: String, receiver_type: T.nilable(String), inherited_only: T::Boolean).void }
197
+ sig { params(message: String, receiver_type: T.nilable(TypeInferrer::Type), inherited_only: T::Boolean).void }
186
198
  def handle_method_definition(message, receiver_type, inherited_only: false)
187
199
  methods = if receiver_type
188
- @index.resolve_method(message, receiver_type, inherited_only: inherited_only)
200
+ @index.resolve_method(message, receiver_type.name, inherited_only: inherited_only)
189
201
  else
190
202
  # If the method doesn't have a receiver, then we provide a few candidates to jump to
191
203
  # But we don't want to provide too many candidates, as it can be overwhelming
@@ -196,7 +208,7 @@ module RubyLsp
196
208
 
197
209
  methods.each do |target_method|
198
210
  file_path = target_method.file_path
199
- next if @typechecker_enabled && not_in_dependencies?(file_path)
211
+ next if sorbet_level_true_or_higher?(@sorbet_level) && not_in_dependencies?(file_path)
200
212
 
201
213
  @response_builder << Interface::LocationLink.new(
202
214
  target_uri: URI::Generic.from_path(path: file_path).to_s,
@@ -253,10 +265,10 @@ module RubyLsp
253
265
 
254
266
  entries.each do |entry|
255
267
  # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
256
- # additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
257
- # in the project, even if the files are typed false
268
+ # additional behavior on top of jumping to RBIs. The only sigil where Sorbet cannot handle constants is typed
269
+ # ignore
258
270
  file_path = entry.file_path
259
- next if @typechecker_enabled && not_in_dependencies?(file_path)
271
+ next if @sorbet_level != Document::SorbetLevel::Ignore && not_in_dependencies?(file_path)
260
272
 
261
273
  @response_builder << Interface::LocationLink.new(
262
274
  target_uri: URI::Generic.from_path(path: file_path).to_s,
@@ -42,17 +42,17 @@ module RubyLsp
42
42
  uri: URI::Generic,
43
43
  node_context: NodeContext,
44
44
  dispatcher: Prism::Dispatcher,
45
- typechecker_enabled: T::Boolean,
45
+ sorbet_level: Document::SorbetLevel,
46
46
  ).void
47
47
  end
48
- def initialize(response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
48
+ def initialize(response_builder, global_state, uri, node_context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
49
49
  @response_builder = response_builder
50
50
  @global_state = global_state
51
51
  @index = T.let(global_state.index, RubyIndexer::Index)
52
52
  @type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
53
53
  @path = T.let(uri.to_standardized_path, T.nilable(String))
54
54
  @node_context = node_context
55
- @typechecker_enabled = typechecker_enabled
55
+ @sorbet_level = sorbet_level
56
56
 
57
57
  dispatcher.register(
58
58
  self,
@@ -73,7 +73,7 @@ module RubyLsp
73
73
 
74
74
  sig { params(node: Prism::ConstantReadNode).void }
75
75
  def on_constant_read_node_enter(node)
76
- return if @typechecker_enabled
76
+ return if @sorbet_level != Document::SorbetLevel::Ignore
77
77
 
78
78
  name = constant_name(node)
79
79
  return if name.nil?
@@ -83,14 +83,14 @@ module RubyLsp
83
83
 
84
84
  sig { params(node: Prism::ConstantWriteNode).void }
85
85
  def on_constant_write_node_enter(node)
86
- return if @global_state.has_type_checker
86
+ return if @sorbet_level != Document::SorbetLevel::Ignore
87
87
 
88
88
  generate_hover(node.name.to_s, node.name_loc)
89
89
  end
90
90
 
91
91
  sig { params(node: Prism::ConstantPathNode).void }
92
92
  def on_constant_path_node_enter(node)
93
- return if @global_state.has_type_checker
93
+ return if @sorbet_level != Document::SorbetLevel::Ignore
94
94
 
95
95
  name = constant_name(node)
96
96
  return if name.nil?
@@ -105,7 +105,7 @@ module RubyLsp
105
105
  return
106
106
  end
107
107
 
108
- return if @typechecker_enabled
108
+ return if sorbet_level_true_or_higher?(@sorbet_level) && self_receiver?(node)
109
109
 
110
110
  message = node.message
111
111
  return unless message
@@ -157,6 +157,9 @@ module RubyLsp
157
157
 
158
158
  sig { void }
159
159
  def handle_super_node_hover
160
+ # Sorbet can handle super hover on typed true or higher
161
+ return if sorbet_level_true_or_higher?(@sorbet_level)
162
+
160
163
  surrounding_method = @node_context.surrounding_method
161
164
  return unless surrounding_method
162
165
 
@@ -168,11 +171,16 @@ module RubyLsp
168
171
  type = @type_inferrer.infer_receiver_type(@node_context)
169
172
  return unless type
170
173
 
171
- methods = @index.resolve_method(message, type, inherited_only: inherited_only)
174
+ methods = @index.resolve_method(message, type.name, inherited_only: inherited_only)
172
175
  return unless methods
173
176
 
174
177
  title = "#{message}#{T.must(methods.first).decorated_parameters}"
175
178
 
179
+ if type.is_a?(TypeInferrer::GuessedType)
180
+ title << "\n\nGuessed receiver: #{type.name}"
181
+ @response_builder.push("[Learn more about guessed types](#{GUESSED_TYPES_URL})\n", category: :links)
182
+ end
183
+
176
184
  categorized_markdown_from_index_entries(title, methods).each do |category, content|
177
185
  @response_builder.push(content, category: category)
178
186
  end
@@ -180,10 +188,14 @@ module RubyLsp
180
188
 
181
189
  sig { params(name: String).void }
182
190
  def handle_instance_variable_hover(name)
191
+ # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
192
+ # to provide all features for them
193
+ return if @sorbet_level == Document::SorbetLevel::Strict
194
+
183
195
  type = @type_inferrer.infer_receiver_type(@node_context)
184
196
  return unless type
185
197
 
186
- entries = @index.resolve_instance_variable(name, type)
198
+ entries = @index.resolve_instance_variable(name, type.name)
187
199
  return unless entries
188
200
 
189
201
  categorized_markdown_from_index_entries(name, entries).each do |category, content|
@@ -13,11 +13,11 @@ module RubyLsp
13
13
  global_state: GlobalState,
14
14
  node_context: NodeContext,
15
15
  dispatcher: Prism::Dispatcher,
16
- typechecker_enabled: T::Boolean,
16
+ sorbet_level: Document::SorbetLevel,
17
17
  ).void
18
18
  end
19
- def initialize(response_builder, global_state, node_context, dispatcher, typechecker_enabled)
20
- @typechecker_enabled = typechecker_enabled
19
+ def initialize(response_builder, global_state, node_context, dispatcher, sorbet_level)
20
+ @sorbet_level = sorbet_level
21
21
  @response_builder = response_builder
22
22
  @global_state = global_state
23
23
  @index = T.let(global_state.index, RubyIndexer::Index)
@@ -28,7 +28,7 @@ module RubyLsp
28
28
 
29
29
  sig { params(node: Prism::CallNode).void }
30
30
  def on_call_node_enter(node)
31
- return if @typechecker_enabled
31
+ return if sorbet_level_true_or_higher?(@sorbet_level)
32
32
 
33
33
  message = node.message
34
34
  return unless message
@@ -36,7 +36,7 @@ module RubyLsp
36
36
  type = @type_inferrer.infer_receiver_type(@node_context)
37
37
  return unless type
38
38
 
39
- methods = @index.resolve_method(message, type)
39
+ methods = @index.resolve_method(message, type.name)
40
40
  return unless methods
41
41
 
42
42
  target_method = methods.first
@@ -61,6 +61,13 @@ module RubyLsp
61
61
  active_parameter += 1
62
62
  end
63
63
 
64
+ title = +""
65
+
66
+ extra_links = if type.is_a?(TypeInferrer::GuessedType)
67
+ title << "\n\nGuessed receiver: #{type.name}"
68
+ "[Learn more about guessed types](#{GUESSED_TYPES_URL})"
69
+ end
70
+
64
71
  signature_help = Interface::SignatureHelp.new(
65
72
  signatures: [
66
73
  Interface::SignatureInformation.new(
@@ -68,7 +75,7 @@ module RubyLsp
68
75
  parameters: parameters.map { |param| Interface::ParameterInformation.new(label: param.name) },
69
76
  documentation: Interface::MarkupContent.new(
70
77
  kind: "markdown",
71
- value: markdown_from_index_entries("", methods),
78
+ value: markdown_from_index_entries(title, methods, extra_links: extra_links),
72
79
  ),
73
80
  ),
74
81
  ],
@@ -49,11 +49,11 @@ module RubyLsp
49
49
  document: Document,
50
50
  global_state: GlobalState,
51
51
  params: T::Hash[Symbol, T.untyped],
52
- typechecker_enabled: T::Boolean,
52
+ sorbet_level: Document::SorbetLevel,
53
53
  dispatcher: Prism::Dispatcher,
54
54
  ).void
55
55
  end
56
- def initialize(document, global_state, params, typechecker_enabled, dispatcher)
56
+ def initialize(document, global_state, params, sorbet_level, dispatcher)
57
57
  super()
58
58
  @target = T.let(nil, T.nilable(Prism::Node))
59
59
  @dispatcher = dispatcher
@@ -84,7 +84,7 @@ module RubyLsp
84
84
  @response_builder,
85
85
  global_state,
86
86
  node_context,
87
- typechecker_enabled,
87
+ sorbet_level,
88
88
  dispatcher,
89
89
  document.uri,
90
90
  params.dig(:context, :triggerCharacter),
@@ -47,7 +47,7 @@ module RubyLsp
47
47
  #
48
48
  # For example, forgetting to return the `insertText` included in the original item will make the editor use the
49
49
  # `label` for the text edit instead
50
- label = @item[:label]
50
+ label = @item[:label].dup
51
51
  entries = @index[label] || []
52
52
 
53
53
  owner_name = @item.dig(:data, :owner_name)
@@ -62,12 +62,19 @@ module RubyLsp
62
62
  first_entry = T.must(entries.first)
63
63
 
64
64
  if first_entry.is_a?(RubyIndexer::Entry::Member)
65
- label = "#{label}#{first_entry.decorated_parameters}"
65
+ label = +"#{label}#{first_entry.decorated_parameters}"
66
+ end
67
+
68
+ guessed_type = @item.dig(:data, :guessed_type)
69
+
70
+ extra_links = if guessed_type
71
+ label << "\n\nGuessed receiver: #{guessed_type}"
72
+ "[Learn more about guessed types](#{GUESSED_TYPES_URL})"
66
73
  end
67
74
 
68
75
  @item[:documentation] = Interface::MarkupContent.new(
69
76
  kind: "markdown",
70
- value: markdown_from_index_entries(label, entries, MAX_DOCUMENTATION_ENTRIES),
77
+ value: markdown_from_index_entries(label, entries, MAX_DOCUMENTATION_ENTRIES, extra_links: extra_links),
71
78
  )
72
79
 
73
80
  @item
@@ -36,10 +36,10 @@ module RubyLsp
36
36
  global_state: GlobalState,
37
37
  position: T::Hash[Symbol, T.untyped],
38
38
  dispatcher: Prism::Dispatcher,
39
- typechecker_enabled: T::Boolean,
39
+ sorbet_level: Document::SorbetLevel,
40
40
  ).void
41
41
  end
42
- def initialize(document, global_state, position, dispatcher, typechecker_enabled)
42
+ def initialize(document, global_state, position, dispatcher, sorbet_level)
43
43
  super()
44
44
  @response_builder = T.let(
45
45
  ResponseBuilders::CollectionResponseBuilder[T.any(Interface::Location, Interface::LocationLink)].new,
@@ -96,7 +96,7 @@ module RubyLsp
96
96
  document.uri,
97
97
  node_context,
98
98
  dispatcher,
99
- typechecker_enabled,
99
+ sorbet_level,
100
100
  )
101
101
 
102
102
  Addon.addons.each do |addon|
@@ -36,10 +36,10 @@ module RubyLsp
36
36
  global_state: GlobalState,
37
37
  position: T::Hash[Symbol, T.untyped],
38
38
  dispatcher: Prism::Dispatcher,
39
- typechecker_enabled: T::Boolean,
39
+ sorbet_level: Document::SorbetLevel,
40
40
  ).void
41
41
  end
42
- def initialize(document, global_state, position, dispatcher, typechecker_enabled)
42
+ def initialize(document, global_state, position, dispatcher, sorbet_level)
43
43
  super()
44
44
  node_context = document.locate_node(position, node_types: Listeners::Hover::ALLOWED_TARGETS)
45
45
  target = node_context.node
@@ -65,7 +65,7 @@ module RubyLsp
65
65
  @target = T.let(target, T.nilable(Prism::Node))
66
66
  uri = document.uri
67
67
  @response_builder = T.let(ResponseBuilders::Hover.new, ResponseBuilders::Hover)
68
- Listeners::Hover.new(@response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled)
68
+ Listeners::Hover.new(@response_builder, global_state, uri, node_context, dispatcher, sorbet_level)
69
69
  Addon.addons.each do |addon|
70
70
  addon.create_hover_listener(@response_builder, node_context, dispatcher)
71
71
  end
@@ -46,10 +46,10 @@ module RubyLsp
46
46
  position: T::Hash[Symbol, T.untyped],
47
47
  context: T.nilable(T::Hash[Symbol, T.untyped]),
48
48
  dispatcher: Prism::Dispatcher,
49
- typechecker_enabled: T::Boolean,
49
+ sorbet_level: Document::SorbetLevel,
50
50
  ).void
51
51
  end
52
- def initialize(document, global_state, position, context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
52
+ def initialize(document, global_state, position, context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
53
53
  super()
54
54
  node_context = document.locate_node(
55
55
  { line: position[:line], character: position[:character] },
@@ -61,7 +61,7 @@ module RubyLsp
61
61
  @target = T.let(target, T.nilable(Prism::Node))
62
62
  @dispatcher = dispatcher
63
63
  @response_builder = T.let(ResponseBuilders::SignatureHelp.new, ResponseBuilders::SignatureHelp)
64
- Listeners::SignatureHelp.new(@response_builder, global_state, node_context, dispatcher, typechecker_enabled)
64
+ Listeners::SignatureHelp.new(@response_builder, global_state, node_context, dispatcher, sorbet_level)
65
65
  end
66
66
 
67
67
  sig { override.returns(T.nilable(Interface::SignatureHelp)) }
@@ -130,13 +130,17 @@ module RubyLsp
130
130
  title: String,
131
131
  entries: T.any(T::Array[RubyIndexer::Entry], RubyIndexer::Entry),
132
132
  max_entries: T.nilable(Integer),
133
+ extra_links: T.nilable(String),
133
134
  ).returns(String)
134
135
  end
135
- def markdown_from_index_entries(title, entries, max_entries = nil)
136
+ def markdown_from_index_entries(title, entries, max_entries = nil, extra_links: nil)
136
137
  categorized_markdown = categorized_markdown_from_index_entries(title, entries, max_entries)
137
138
 
139
+ markdown = +(categorized_markdown[:title] || "")
140
+ markdown << "\n\n#{extra_links}" if extra_links
141
+
138
142
  <<~MARKDOWN.chomp
139
- #{categorized_markdown[:title]}
143
+ #{markdown}
140
144
 
141
145
  #{categorized_markdown[:links]}
142
146
 
@@ -204,6 +208,11 @@ module RubyLsp
204
208
  Constant::SymbolKind::FIELD
205
209
  end
206
210
  end
211
+
212
+ sig { params(sorbet_level: Document::SorbetLevel).returns(T::Boolean) }
213
+ def sorbet_level_true_or_higher?(sorbet_level)
214
+ sorbet_level == Document::SorbetLevel::True || sorbet_level == Document::SorbetLevel::Strict
215
+ end
207
216
  end
208
217
  end
209
218
  end
@@ -477,15 +477,17 @@ module RubyLsp
477
477
  @global_state,
478
478
  params[:position],
479
479
  dispatcher,
480
- typechecker_enabled?(document),
480
+ sorbet_level(document),
481
481
  ).perform,
482
482
  ),
483
483
  )
484
484
  end
485
485
 
486
- sig { params(document: Document).returns(T::Boolean) }
487
- def typechecker_enabled?(document)
488
- @global_state.has_type_checker && document.sorbet_sigil_is_true_or_higher
486
+ sig { params(document: Document).returns(Document::SorbetLevel) }
487
+ def sorbet_level(document)
488
+ return Document::SorbetLevel::Ignore unless @global_state.has_type_checker
489
+
490
+ document.sorbet_level
489
491
  end
490
492
 
491
493
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
@@ -594,7 +596,7 @@ module RubyLsp
594
596
  document,
595
597
  @global_state,
596
598
  params,
597
- typechecker_enabled?(document),
599
+ sorbet_level(document),
598
600
  dispatcher,
599
601
  ).perform,
600
602
  ),
@@ -624,7 +626,7 @@ module RubyLsp
624
626
  params[:position],
625
627
  params[:context],
626
628
  dispatcher,
627
- typechecker_enabled?(document),
629
+ sorbet_level(document),
628
630
  ).perform,
629
631
  ),
630
632
  )
@@ -644,7 +646,7 @@ module RubyLsp
644
646
  @global_state,
645
647
  params[:position],
646
648
  dispatcher,
647
- typechecker_enabled?(document),
649
+ sorbet_level(document),
648
650
  ).perform,
649
651
  ),
650
652
  )
@@ -38,13 +38,18 @@ module RubyLsp
38
38
  document = @state[uri.to_s]
39
39
  return document unless document.nil?
40
40
 
41
- path = T.must(uri.to_standardized_path)
41
+ # For unsaved files (`untitled:Untitled-1` uris), there's no path to read from. If we don't have the untitled file
42
+ # already present in the store, then we have to raise non existing document error
43
+ path = uri.to_standardized_path
44
+ raise NonExistingDocumentError, uri.to_s unless path
45
+
42
46
  ext = File.extname(path)
43
47
  language_id = if ext == ".erb" || ext == ".rhtml"
44
48
  Document::LanguageId::ERB
45
49
  else
46
50
  Document::LanguageId::Ruby
47
51
  end
52
+
48
53
  set(uri: uri, source: File.binread(path), version: 0, language_id: language_id)
49
54
  T.must(@state[uri.to_s])
50
55
  rescue Errno::ENOENT
@@ -21,7 +21,7 @@ module RubyLsp
21
21
  &block)
22
22
  server = RubyLsp::Server.new(test_mode: true)
23
23
  server.global_state.stubs(:has_type_checker).returns(false) if stub_no_typechecker
24
- server.global_state.apply_options({})
24
+ server.global_state.apply_options({ initializationOptions: { experimentalFeaturesEnabled: true } })
25
25
  language_id = uri.to_s.end_with?(".erb") ? "erb" : "ruby"
26
26
 
27
27
  if source
@@ -7,12 +7,16 @@ module RubyLsp
7
7
  class TypeInferrer
8
8
  extend T::Sig
9
9
 
10
- sig { params(index: RubyIndexer::Index).void }
11
- def initialize(index)
10
+ sig { params(experimental_features: T::Boolean).returns(T::Boolean) }
11
+ attr_writer :experimental_features
12
+
13
+ sig { params(index: RubyIndexer::Index, experimental_features: T::Boolean).void }
14
+ def initialize(index, experimental_features = true)
12
15
  @index = index
16
+ @experimental_features = experimental_features
13
17
  end
14
18
 
15
- sig { params(node_context: NodeContext).returns(T.nilable(String)) }
19
+ sig { params(node_context: NodeContext).returns(T.nilable(Type)) }
16
20
  def infer_receiver_type(node_context)
17
21
  node = node_context.node
18
22
 
@@ -28,7 +32,7 @@ module RubyLsp
28
32
 
29
33
  private
30
34
 
31
- sig { params(node: Prism::CallNode, node_context: NodeContext).returns(T.nilable(String)) }
35
+ sig { params(node: Prism::CallNode, node_context: NodeContext).returns(T.nilable(Type)) }
32
36
  def infer_receiver_for_call_node(node, node_context)
33
37
  receiver = node.receiver
34
38
 
@@ -47,23 +51,40 @@ module RubyLsp
47
51
  return unless name
48
52
 
49
53
  *parts, last = name.split("::")
50
- return "#{last}::<Class:#{last}>" if parts.empty?
54
+ return Type.new("#{last}::<Class:#{last}>") if parts.empty?
55
+
56
+ Type.new("#{parts.join("::")}::#{last}::<Class:#{last}>")
57
+ else
58
+ return unless @experimental_features
59
+
60
+ raw_receiver = node.receiver&.slice
51
61
 
52
- "#{parts.join("::")}::#{last}::<Class:#{last}>"
62
+ if raw_receiver
63
+ guessed_name = raw_receiver
64
+ .delete_prefix("@")
65
+ .delete_prefix("@@")
66
+ .split("_")
67
+ .map(&:capitalize)
68
+ .join
69
+
70
+ entries = @index.resolve(guessed_name, node_context.nesting) || @index.first_unqualified_const(guessed_name)
71
+ name = entries&.first&.name
72
+ GuessedType.new(name) if name
73
+ end
53
74
  end
54
75
  end
55
76
 
56
- sig { params(node_context: NodeContext).returns(String) }
77
+ sig { params(node_context: NodeContext).returns(Type) }
57
78
  def self_receiver_handling(node_context)
58
79
  nesting = node_context.nesting
59
80
  # If we're at the top level, then the invocation is happening on `<main>`, which is a special singleton that
60
81
  # inherits from Object
61
- return "Object" if nesting.empty?
62
- return node_context.fully_qualified_name if node_context.surrounding_method
82
+ return Type.new("Object") if nesting.empty?
83
+ return Type.new(node_context.fully_qualified_name) if node_context.surrounding_method
63
84
 
64
85
  # If we're not inside a method, then we're inside the body of a class or module, which is a singleton
65
86
  # context
66
- "#{nesting.join("::")}::<Class:#{nesting.last}>"
87
+ Type.new("#{nesting.join("::")}::<Class:#{nesting.last}>")
67
88
  end
68
89
 
69
90
  sig do
@@ -80,5 +101,22 @@ module RubyLsp
80
101
  Prism::ConstantPathNode::MissingNodesInConstantPathError
81
102
  nil
82
103
  end
104
+
105
+ # A known type
106
+ class Type
107
+ extend T::Sig
108
+
109
+ sig { returns(String) }
110
+ attr_reader :name
111
+
112
+ sig { params(name: String).void }
113
+ def initialize(name)
114
+ @name = name
115
+ end
116
+ end
117
+
118
+ # A type that was guessed based on the receiver raw name
119
+ class GuessedType < Type
120
+ end
83
121
  end
84
122
  end
@@ -25,6 +25,7 @@ module RubyLsp
25
25
  end,
26
26
  String,
27
27
  )
28
+ GUESSED_TYPES_URL = "https://github.com/Shopify/ruby-lsp/blob/main/DESIGN_AND_ROADMAP.md#guessed-types"
28
29
 
29
30
  # A notification to be sent to the client
30
31
  class Message
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.8
4
+ version: 0.17.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-18 00:00:00.000000000 Z
11
+ date: 2024-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -204,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
204
  - !ruby/object:Gem::Version
205
205
  version: '0'
206
206
  requirements: []
207
- rubygems_version: 3.5.15
207
+ rubygems_version: 3.5.16
208
208
  signing_key:
209
209
  specification_version: 4
210
210
  summary: An opinionated language server for Ruby