ruby-lsp 0.21.3 → 0.22.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,24 +5,28 @@ require_relative "test_case"
5
5
 
6
6
  module RubyIndexer
7
7
  class EnhancementTest < TestCase
8
+ def teardown
9
+ super
10
+ Enhancement.clear
11
+ end
12
+
8
13
  def test_enhancing_indexing_included_hook
9
- enhancement_class = Class.new(Enhancement) do
10
- def on_call_node_enter(owner, node, file_path, code_units_cache)
14
+ Class.new(Enhancement) do
15
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
16
+ owner = @listener.current_owner
11
17
  return unless owner
12
- return unless node.name == :extend
18
+ return unless call_node.name == :extend
13
19
 
14
- arguments = node.arguments&.arguments
20
+ arguments = call_node.arguments&.arguments
15
21
  return unless arguments
16
22
 
17
- location = Location.from_prism_location(node.location, code_units_cache)
18
-
19
23
  arguments.each do |node|
20
24
  next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
21
25
 
22
26
  module_name = node.full_name
23
27
  next unless module_name == "ActiveSupport::Concern"
24
28
 
25
- @index.register_included_hook(owner.name) do |index, base|
29
+ @listener.register_included_hook do |index, base|
26
30
  class_methods_name = "#{owner.name}::ClassMethods"
27
31
 
28
32
  if index.indexed?(class_methods_name)
@@ -31,16 +35,11 @@ module RubyIndexer
31
35
  end
32
36
  end
33
37
 
34
- @index.add(Entry::Method.new(
38
+ @listener.add_method(
35
39
  "new_method",
36
- file_path,
37
- location,
38
- location,
39
- nil,
40
+ call_node.location,
40
41
  [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
41
- Entry::Visibility::PUBLIC,
42
- owner,
43
- ))
42
+ )
44
43
  rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
45
44
  Prism::ConstantPathNode::MissingNodesInConstantPathError
46
45
  # Do nothing
@@ -48,7 +47,6 @@ module RubyIndexer
48
47
  end
49
48
  end
50
49
 
51
- @index.register_enhancement(enhancement_class.new(@index))
52
50
  index(<<~RUBY)
53
51
  module ActiveSupport
54
52
  module Concern
@@ -96,9 +94,9 @@ module RubyIndexer
96
94
  end
97
95
 
98
96
  def test_enhancing_indexing_configuration_dsl
99
- enhancement_class = Class.new(Enhancement) do
100
- def on_call_node_enter(owner, node, file_path, code_units_cache)
101
- return unless owner
97
+ Class.new(Enhancement) do
98
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
99
+ return unless @listener.current_owner
102
100
 
103
101
  name = node.name
104
102
  return unless name == :has_many
@@ -109,22 +107,14 @@ module RubyIndexer
109
107
  association_name = arguments.first
110
108
  return unless association_name.is_a?(Prism::SymbolNode)
111
109
 
112
- location = Location.from_prism_location(association_name.location, code_units_cache)
113
-
114
- @index.add(Entry::Method.new(
110
+ @listener.add_method(
115
111
  T.must(association_name.value),
116
- file_path,
117
- location,
118
- location,
119
- nil,
112
+ association_name.location,
120
113
  [],
121
- Entry::Visibility::PUBLIC,
122
- owner,
123
- ))
114
+ )
124
115
  end
125
116
  end
126
117
 
127
- @index.register_enhancement(enhancement_class.new(@index))
128
118
  index(<<~RUBY)
129
119
  module ActiveSupport
130
120
  module Concern
@@ -157,8 +147,8 @@ module RubyIndexer
157
147
  end
158
148
 
159
149
  def test_error_handling_in_on_call_node_enter_enhancement
160
- enhancement_class = Class.new(Enhancement) do
161
- def on_call_node_enter(owner, node, file_path, code_units_cache)
150
+ Class.new(Enhancement) do
151
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
162
152
  raise "Error"
163
153
  end
164
154
 
@@ -169,8 +159,6 @@ module RubyIndexer
169
159
  end
170
160
  end
171
161
 
172
- @index.register_enhancement(enhancement_class.new(@index))
173
-
174
162
  _stdout, stderr = capture_io do
175
163
  index(<<~RUBY)
176
164
  module ActiveSupport
@@ -192,8 +180,8 @@ module RubyIndexer
192
180
  end
193
181
 
194
182
  def test_error_handling_in_on_call_node_leave_enhancement
195
- enhancement_class = Class.new(Enhancement) do
196
- def on_call_node_leave(owner, node, file_path, code_units_cache)
183
+ Class.new(Enhancement) do
184
+ def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
197
185
  raise "Error"
198
186
  end
199
187
 
@@ -204,8 +192,6 @@ module RubyIndexer
204
192
  end
205
193
  end
206
194
 
207
- @index.register_enhancement(enhancement_class.new(@index))
208
-
209
195
  _stdout, stderr = capture_io do
210
196
  index(<<~RUBY)
211
197
  module ActiveSupport
@@ -225,5 +211,115 @@ module RubyIndexer
225
211
  # The module should still be indexed
226
212
  assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
227
213
  end
214
+
215
+ def test_advancing_namespace_stack_from_enhancement
216
+ Class.new(Enhancement) do
217
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
218
+ owner = @listener.current_owner
219
+ return unless owner
220
+
221
+ case call_node.name
222
+ when :class_methods
223
+ @listener.add_module("ClassMethods", call_node.location, call_node.location)
224
+ when :extend
225
+ arguments = call_node.arguments&.arguments
226
+ return unless arguments
227
+
228
+ arguments.each do |node|
229
+ next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
230
+
231
+ module_name = node.full_name
232
+ next unless module_name == "ActiveSupport::Concern"
233
+
234
+ @listener.register_included_hook do |index, base|
235
+ class_methods_name = "#{owner.name}::ClassMethods"
236
+
237
+ if index.indexed?(class_methods_name)
238
+ singleton = index.existing_or_new_singleton_class(base.name)
239
+ singleton.mixin_operations << Entry::Include.new(class_methods_name)
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
247
+ return unless call_node.name == :class_methods
248
+
249
+ @listener.pop_namespace_stack
250
+ end
251
+ end
252
+
253
+ index(<<~RUBY)
254
+ module ActiveSupport
255
+ module Concern
256
+ end
257
+ end
258
+
259
+ module MyConcern
260
+ extend ActiveSupport::Concern
261
+
262
+ class_methods do
263
+ def foo; end
264
+ end
265
+ end
266
+
267
+ class User
268
+ include MyConcern
269
+ end
270
+ RUBY
271
+
272
+ assert_equal(
273
+ [
274
+ "User::<Class:User>",
275
+ "MyConcern::ClassMethods",
276
+ "Object::<Class:Object>",
277
+ "BasicObject::<Class:BasicObject>",
278
+ "Class",
279
+ "Module",
280
+ "Object",
281
+ "Kernel",
282
+ "BasicObject",
283
+ ],
284
+ @index.linearized_ancestors_of("User::<Class:User>"),
285
+ )
286
+
287
+ refute_nil(@index.resolve_method("foo", "User::<Class:User>"))
288
+ end
289
+
290
+ def test_creating_anonymous_classes_from_enhancement
291
+ Class.new(Enhancement) do
292
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
293
+ case call_node.name
294
+ when :context
295
+ arguments = call_node.arguments&.arguments
296
+ first_argument = arguments&.first
297
+ return unless first_argument.is_a?(Prism::StringNode)
298
+
299
+ @listener.add_class(
300
+ "<RSpec:#{first_argument.content}>",
301
+ call_node.location,
302
+ first_argument.location,
303
+ )
304
+ when :subject
305
+ @listener.add_method("subject", call_node.location, [])
306
+ end
307
+ end
308
+
309
+ def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
310
+ return unless call_node.name == :context
311
+
312
+ @listener.pop_namespace_stack
313
+ end
314
+ end
315
+
316
+ index(<<~RUBY)
317
+ context "does something" do
318
+ subject { call_whatever }
319
+ end
320
+ RUBY
321
+
322
+ refute_nil(@index.resolve_method("subject", "<RSpec:does something>"))
323
+ end
228
324
  end
229
325
  end
@@ -1672,6 +1672,38 @@ module RubyIndexer
1672
1672
  )
1673
1673
  end
1674
1674
 
1675
+ def test_extend_self
1676
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
1677
+ module Foo
1678
+ def bar
1679
+ end
1680
+
1681
+ extend self
1682
+
1683
+ def baz
1684
+ end
1685
+ end
1686
+ RUBY
1687
+
1688
+ ["bar", "baz"].product(["Foo", "Foo::<Class:Foo>"]).each do |method, receiver|
1689
+ entry = @index.resolve_method(method, receiver)&.first
1690
+ refute_nil(entry)
1691
+ assert_equal(method, T.must(entry).name)
1692
+ end
1693
+
1694
+ assert_equal(
1695
+ [
1696
+ "Foo::<Class:Foo>",
1697
+ "Foo",
1698
+ "Module",
1699
+ "Object",
1700
+ "Kernel",
1701
+ "BasicObject",
1702
+ ],
1703
+ @index.linearized_ancestors_of("Foo::<Class:Foo>"),
1704
+ )
1705
+ end
1706
+
1675
1707
  def test_linearizing_singleton_ancestors
1676
1708
  @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
1677
1709
  module First
@@ -2023,5 +2055,12 @@ module RubyIndexer
2023
2055
  ),
2024
2056
  )
2025
2057
  end
2058
+
2059
+ def test_prevents_multiple_calls_to_index_all
2060
+ # For this test class, `index_all` is already called once in `setup`.
2061
+ assert_raises(Index::IndexNotEmptyError) do
2062
+ @index.index_all
2063
+ end
2064
+ end
2026
2065
  end
2027
2066
  end
@@ -141,7 +141,7 @@ module RubyIndexer
141
141
  # The first entry points to the location of the module_function call
142
142
  assert_equal("Test", first_entry.owner.name)
143
143
  assert_instance_of(Entry::Module, first_entry.owner)
144
- assert_equal(Entry::Visibility::PRIVATE, first_entry.visibility)
144
+ assert_predicate(first_entry, :private?)
145
145
  # The second entry points to the public singleton method
146
146
  assert_equal("Test::<Class:Test>", second_entry.owner.name)
147
147
  assert_instance_of(Entry::SingletonClass, second_entry.owner)
@@ -149,6 +149,119 @@ module RubyIndexer
149
149
  end
150
150
  end
151
151
 
152
+ def test_private_class_method_visibility_tracking_string_symbol_arguments
153
+ index(<<~RUBY)
154
+ class Test
155
+ def self.foo
156
+ end
157
+
158
+ def self.bar
159
+ end
160
+
161
+ private_class_method("foo", :bar)
162
+
163
+ def self.baz
164
+ end
165
+ end
166
+ RUBY
167
+
168
+ ["foo", "bar"].each do |keyword|
169
+ entries = T.must(@index[keyword])
170
+ assert_equal(1, entries.size)
171
+ entry = entries.first
172
+ assert_predicate(entry, :private?)
173
+ end
174
+
175
+ entries = T.must(@index["baz"])
176
+ assert_equal(1, entries.size)
177
+ entry = entries.first
178
+ assert_predicate(entry, :public?)
179
+ end
180
+
181
+ def test_private_class_method_visibility_tracking_array_argument
182
+ index(<<~RUBY)
183
+ class Test
184
+ def self.foo
185
+ end
186
+
187
+ def self.bar
188
+ end
189
+
190
+ private_class_method(["foo", :bar])
191
+
192
+ def self.baz
193
+ end
194
+ end
195
+ RUBY
196
+
197
+ ["foo", "bar"].each do |keyword|
198
+ entries = T.must(@index[keyword])
199
+ assert_equal(1, entries.size)
200
+ entry = entries.first
201
+ assert_predicate(entry, :private?)
202
+ end
203
+
204
+ entries = T.must(@index["baz"])
205
+ assert_equal(1, entries.size)
206
+ entry = entries.first
207
+ assert_predicate(entry, :public?)
208
+ end
209
+
210
+ def test_private_class_method_visibility_tracking_method_argument
211
+ index(<<~RUBY)
212
+ class Test
213
+ private_class_method def self.foo
214
+ end
215
+
216
+ def self.bar
217
+ end
218
+ end
219
+ RUBY
220
+
221
+ entries = T.must(@index["foo"])
222
+ assert_equal(1, entries.size)
223
+ entry = entries.first
224
+ assert_predicate(entry, :private?)
225
+
226
+ entries = T.must(@index["bar"])
227
+ assert_equal(1, entries.size)
228
+ entry = entries.first
229
+ assert_predicate(entry, :public?)
230
+ end
231
+
232
+ def test_comments_documentation
233
+ index(<<~RUBY)
234
+ # Documentation for Foo
235
+
236
+ class Foo
237
+ # ####################
238
+ # Documentation for bar
239
+ # ####################
240
+ #
241
+ def bar
242
+ end
243
+
244
+ # test
245
+
246
+ # Documentation for baz
247
+ def baz; end
248
+ def ban; end
249
+ end
250
+ RUBY
251
+
252
+ foo_comment = @index["Foo"].first.comments
253
+ assert_equal("Documentation for Foo", foo_comment)
254
+
255
+ bar_comment = @index["bar"].first.comments
256
+ assert_equal("####################\nDocumentation for bar\n####################\n", bar_comment)
257
+
258
+ baz_comment = @index["baz"].first.comments
259
+ assert_equal("Documentation for baz", baz_comment)
260
+
261
+ ban_comment = @index["ban"].first.comments
262
+ assert_empty(ban_comment)
263
+ end
264
+
152
265
  def test_method_with_parameters
153
266
  index(<<~RUBY)
154
267
  class Foo
@@ -21,7 +21,7 @@ module RubyLsp
21
21
  attr_reader :encoding
22
22
 
23
23
  sig { returns(T::Boolean) }
24
- attr_reader :experimental_features, :top_level_bundle
24
+ attr_reader :top_level_bundle
25
25
 
26
26
  sig { returns(TypeInferrer) }
27
27
  attr_reader :type_inferrer
@@ -40,7 +40,6 @@ module RubyLsp
40
40
  @has_type_checker = T.let(true, T::Boolean)
41
41
  @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
42
42
  @supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
43
- @experimental_features = T.let(false, T::Boolean)
44
43
  @type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
45
44
  @addon_settings = T.let({}, T::Hash[String, T.untyped])
46
45
  @top_level_bundle = T.let(
@@ -131,7 +130,6 @@ module RubyLsp
131
130
  end
132
131
  @index.configuration.encoding = @encoding
133
132
 
134
- @experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
135
133
  @client_capabilities.apply_client_capabilities(options[:capabilities]) if options[:capabilities]
136
134
 
137
135
  addon_settings = options.dig(:initializationOptions, :addonSettings)
@@ -148,7 +146,7 @@ module RubyLsp
148
146
 
149
147
  sig { params(flag: Symbol).returns(T.nilable(T::Boolean)) }
150
148
  def enabled_feature?(flag)
151
- @enabled_feature_flags[flag]
149
+ @enabled_feature_flags[:all] || @enabled_feature_flags[flag]
152
150
  end
153
151
 
154
152
  sig { returns(String) }
@@ -21,6 +21,7 @@ require "prism"
21
21
  require "prism/visitor"
22
22
  require "language_server-protocol"
23
23
  require "rbs"
24
+ require "fileutils"
24
25
 
25
26
  require "ruby-lsp"
26
27
  require "ruby_lsp/base_server"
@@ -41,6 +41,9 @@ module RubyLsp
41
41
  :on_module_node_enter,
42
42
  :on_module_node_leave,
43
43
  :on_instance_variable_write_node_enter,
44
+ :on_instance_variable_operator_write_node_enter,
45
+ :on_instance_variable_or_write_node_enter,
46
+ :on_instance_variable_and_write_node_enter,
44
47
  :on_class_variable_write_node_enter,
45
48
  :on_singleton_class_node_enter,
46
49
  :on_singleton_class_node_leave,
@@ -249,21 +252,51 @@ module RubyLsp
249
252
  @response_builder.pop
250
253
  end
251
254
 
255
+ sig { params(node: Prism::ClassVariableWriteNode).void }
256
+ def on_class_variable_write_node_enter(node)
257
+ create_document_symbol(
258
+ name: node.name.to_s,
259
+ kind: Constant::SymbolKind::VARIABLE,
260
+ range_location: node.name_loc,
261
+ selection_range_location: node.name_loc,
262
+ )
263
+ end
264
+
252
265
  sig { params(node: Prism::InstanceVariableWriteNode).void }
253
266
  def on_instance_variable_write_node_enter(node)
254
267
  create_document_symbol(
255
268
  name: node.name.to_s,
256
- kind: Constant::SymbolKind::VARIABLE,
269
+ kind: Constant::SymbolKind::FIELD,
257
270
  range_location: node.name_loc,
258
271
  selection_range_location: node.name_loc,
259
272
  )
260
273
  end
261
274
 
262
- sig { params(node: Prism::ClassVariableWriteNode).void }
263
- def on_class_variable_write_node_enter(node)
275
+ sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
276
+ def on_instance_variable_operator_write_node_enter(node)
264
277
  create_document_symbol(
265
278
  name: node.name.to_s,
266
- kind: Constant::SymbolKind::VARIABLE,
279
+ kind: Constant::SymbolKind::FIELD,
280
+ range_location: node.name_loc,
281
+ selection_range_location: node.name_loc,
282
+ )
283
+ end
284
+
285
+ sig { params(node: Prism::InstanceVariableOrWriteNode).void }
286
+ def on_instance_variable_or_write_node_enter(node)
287
+ create_document_symbol(
288
+ name: node.name.to_s,
289
+ kind: Constant::SymbolKind::FIELD,
290
+ range_location: node.name_loc,
291
+ selection_range_location: node.name_loc,
292
+ )
293
+ end
294
+
295
+ sig { params(node: Prism::InstanceVariableAndWriteNode).void }
296
+ def on_instance_variable_and_write_node_enter(node)
297
+ create_document_symbol(
298
+ name: node.name.to_s,
299
+ kind: Constant::SymbolKind::FIELD,
267
300
  range_location: node.name_loc,
268
301
  selection_range_location: node.name_loc,
269
302
  )
@@ -118,6 +118,8 @@ module RubyLsp
118
118
  Prism::InstanceVariableWriteNode
119
119
 
120
120
  !covers_position?(target.name_loc, position)
121
+ when Prism::CallNode
122
+ !covers_position?(target.message_loc, position)
121
123
  else
122
124
  false
123
125
  end
@@ -103,6 +103,8 @@ module RubyLsp
103
103
  Prism::GlobalVariableOrWriteNode,
104
104
  Prism::GlobalVariableWriteNode
105
105
  !covers_position?(target.name_loc, position)
106
+ when Prism::CallNode
107
+ !covers_position?(target.message_loc, position)
106
108
  else
107
109
  false
108
110
  end
@@ -15,6 +15,7 @@ rescue LoadError
15
15
  return
16
16
  end
17
17
 
18
+ # Remember to update the version in the documentation (usage/dependency-compatibility section) if you change this
18
19
  # Ensure that RuboCop is at least version 1.4.0
19
20
  begin
20
21
  gem("rubocop", ">= 1.4.0")
@@ -216,6 +216,13 @@ module RubyLsp
216
216
  Hash.new(true)
217
217
  end
218
218
 
219
+ bundle_env_path = File.join(".ruby-lsp", "bundle_env")
220
+ bundle_env = if File.exist?(bundle_env_path)
221
+ env = File.readlines(bundle_env_path).to_h { |line| T.cast(line.chomp.split("=", 2), [String, String]) }
222
+ FileUtils.rm(bundle_env_path)
223
+ env
224
+ end
225
+
219
226
  document_symbol_provider = Requests::DocumentSymbol.provider if enabled_features["documentSymbols"]
220
227
  document_link_provider = Requests::DocumentLink.provider if enabled_features["documentLink"]
221
228
  code_lens_provider = Requests::CodeLens.provider if enabled_features["codeLens"]
@@ -269,6 +276,7 @@ module RubyLsp
269
276
  },
270
277
  formatter: @global_state.formatter,
271
278
  degraded_mode: !!(@install_error || @setup_error),
279
+ bundle_env: bundle_env,
272
280
  }
273
281
 
274
282
  send_message(Result.new(id: message[:id], response: response))
@@ -604,6 +612,11 @@ module RubyLsp
604
612
  # don't want to format it
605
613
  path = uri.to_standardized_path
606
614
  unless path.nil? || path.start_with?(@global_state.workspace_path)
615
+ send_log_message(<<~MESSAGE)
616
+ Ignoring formatting request for file outside of the workspace.
617
+ Workspace path was set by editor as #{@global_state.workspace_path}.
618
+ File path requested for formatting was #{path}
619
+ MESSAGE
607
620
  send_empty_response(message[:id])
608
621
  return
609
622
  end