ruby-lsp 0.17.8 → 0.17.9

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