ruby-lsp 0.21.3 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f580352765054aabd1821330dab6fa4ce5fb65746dee3a96640589a511a047f0
4
- data.tar.gz: 41b51b6a3c13116b64a116f50337df14624088d8c50b2d5675c753b0ae612220
3
+ metadata.gz: e48cef95e466e2a75943921fc08ede1a449e07096e44078ca063bdbee6d3c54b
4
+ data.tar.gz: c0ce6a0636645674e9e9a87219124b622b1280420c14c4bd12f8f0f0aafc3f32
5
5
  SHA512:
6
- metadata.gz: 6801b771e4aec3f493211f8b50c5c6e2998cd7d2ff1061bbc8e7afb83c87e653fcf7dfd23c3b41ec2fa65cd1d1eec3fab840a9188936904c08c6f2745c12c5a8
7
- data.tar.gz: 9b68fee2853275c1343fa2815c6fe4263e6c90277b5e6567dfb84d4e0dd9d112353d77f18055384d9cb9aeb6a263ce8ec1b4b66f8b1eb2950f800aedf73fba3a
6
+ metadata.gz: 89d02237ecce7e784bb5275ba4d5fc30b64981ea337b078f3117037b98d8f734e0d7e6266078e76023125dd52bbcb6ded8d04fdac1c29eb8292fd29e401cece4
7
+ data.tar.gz: 7245d82a481bd9e6a56d0cce9ad03796bd1a6d9e770beb5632fcaf7669ab0af9e9c0c95f0b671d844231af718add21b98011665ac3355fab5647ab12378b60bf
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.21.3
1
+ 0.22.0
data/exe/ruby-lsp CHANGED
@@ -54,7 +54,7 @@ rescue OptionParser::InvalidOption => e
54
54
  exit(1)
55
55
  end
56
56
 
57
- # When we're running without bundler, then we need to make sure the custom bundle is fully configured and re-execute
57
+ # When we're running without bundler, then we need to make sure the composed bundle is fully configured and re-execute
58
58
  # using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of
59
59
  # the application's bundle
60
60
  if ENV["BUNDLE_GEMFILE"].nil?
@@ -74,9 +74,6 @@ rescue StandardError => e
74
74
  # If Bundler.setup fails, we need to restore the original $LOAD_PATH so that we can still require the Ruby LSP server
75
75
  # in degraded mode
76
76
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
77
- ensure
78
- require "fileutils"
79
- FileUtils.rm(bundle_env_path) if File.exist?(bundle_env_path)
80
77
  end
81
78
 
82
79
  error_path = File.join(".ruby-lsp", "install_error")
@@ -109,6 +109,12 @@ module RubyIndexer
109
109
 
110
110
  indexables = T.let([], T::Array[IndexablePath])
111
111
 
112
+ # Handle top level files separately. The path below is an optimization to prevent descending down directories that
113
+ # are going to be excluded anyway, so we need to handle top level scripts separately
114
+ Dir.glob(File.join(@workspace_path, "*.rb"), flags).each do |path|
115
+ indexables << IndexablePath.new(nil, path)
116
+ end
117
+
112
118
  # Add user specified patterns
113
119
  @included_patterns.each do |pattern|
114
120
  load_path_entry = T.let(nil, T.nilable(String))
@@ -18,13 +18,12 @@ module RubyIndexer
18
18
  parse_result: Prism::ParseResult,
19
19
  file_path: String,
20
20
  collect_comments: T::Boolean,
21
- enhancements: T::Array[Enhancement],
22
21
  ).void
23
22
  end
24
- def initialize(index, dispatcher, parse_result, file_path, collect_comments: false, enhancements: [])
23
+ def initialize(index, dispatcher, parse_result, file_path, collect_comments: false)
25
24
  @index = index
26
25
  @file_path = file_path
27
- @enhancements = enhancements
26
+ @enhancements = T.let(Enhancement.all(self), T::Array[Enhancement])
28
27
  @visibility_stack = T.let([Entry::Visibility::PUBLIC], T::Array[Entry::Visibility])
29
28
  @comments_by_line = T.let(
30
29
  parse_result.comments.to_h do |c|
@@ -37,6 +36,7 @@ module RubyIndexer
37
36
  parse_result.code_units_cache(@index.configuration.encoding),
38
37
  T.any(T.proc.params(arg0: Integer).returns(Integer), Prism::CodeUnitsCache),
39
38
  )
39
+ @source_lines = T.let(parse_result.source.lines, T::Array[String])
40
40
 
41
41
  # The nesting stack we're currently inside. Used to determine the fully qualified name of constants, but only
42
42
  # stored by unresolved aliases which need the original nesting to be lazily resolved
@@ -85,15 +85,9 @@ module RubyIndexer
85
85
 
86
86
  sig { params(node: Prism::ClassNode).void }
87
87
  def on_class_node_enter(node)
88
- @visibility_stack.push(Entry::Visibility::PUBLIC)
89
88
  constant_path = node.constant_path
90
- name = constant_path.slice
91
-
92
- comments = collect_comments(node)
93
-
94
89
  superclass = node.superclass
95
-
96
- nesting = actual_nesting(name)
90
+ nesting = actual_nesting(constant_path.slice)
97
91
 
98
92
  parent_class = case superclass
99
93
  when Prism::ConstantReadNode, Prism::ConstantPathNode
@@ -112,53 +106,29 @@ module RubyIndexer
112
106
  end
113
107
  end
114
108
 
115
- entry = Entry::Class.new(
109
+ add_class(
116
110
  nesting,
117
- @file_path,
118
- Location.from_prism_location(node.location, @code_units_cache),
119
- Location.from_prism_location(constant_path.location, @code_units_cache),
120
- comments,
121
- parent_class,
111
+ node.location,
112
+ constant_path.location,
113
+ parent_class_name: parent_class,
114
+ comments: collect_comments(node),
122
115
  )
123
-
124
- @owner_stack << entry
125
- @index.add(entry)
126
- @stack << name
127
116
  end
128
117
 
129
118
  sig { params(node: Prism::ClassNode).void }
130
119
  def on_class_node_leave(node)
131
- @stack.pop
132
- @owner_stack.pop
133
- @visibility_stack.pop
120
+ pop_namespace_stack
134
121
  end
135
122
 
136
123
  sig { params(node: Prism::ModuleNode).void }
137
124
  def on_module_node_enter(node)
138
- @visibility_stack.push(Entry::Visibility::PUBLIC)
139
125
  constant_path = node.constant_path
140
- name = constant_path.slice
141
-
142
- comments = collect_comments(node)
143
-
144
- entry = Entry::Module.new(
145
- actual_nesting(name),
146
- @file_path,
147
- Location.from_prism_location(node.location, @code_units_cache),
148
- Location.from_prism_location(constant_path.location, @code_units_cache),
149
- comments,
150
- )
151
-
152
- @owner_stack << entry
153
- @index.add(entry)
154
- @stack << name
126
+ add_module(constant_path.slice, node.location, constant_path.location, comments: collect_comments(node))
155
127
  end
156
128
 
157
129
  sig { params(node: Prism::ModuleNode).void }
158
130
  def on_module_node_leave(node)
159
- @stack.pop
160
- @owner_stack.pop
161
- @visibility_stack.pop
131
+ pop_namespace_stack
162
132
  end
163
133
 
164
134
  sig { params(node: Prism::SingletonClassNode).void }
@@ -200,9 +170,7 @@ module RubyIndexer
200
170
 
201
171
  sig { params(node: Prism::SingletonClassNode).void }
202
172
  def on_singleton_class_node_leave(node)
203
- @stack.pop
204
- @owner_stack.pop
205
- @visibility_stack.pop
173
+ pop_namespace_stack
206
174
  end
207
175
 
208
176
  sig { params(node: Prism::MultiWriteNode).void }
@@ -317,7 +285,7 @@ module RubyIndexer
317
285
  end
318
286
 
319
287
  @enhancements.each do |enhancement|
320
- enhancement.on_call_node_enter(@owner_stack.last, node, @file_path, @code_units_cache)
288
+ enhancement.on_call_node_enter(node)
321
289
  rescue StandardError => e
322
290
  @indexing_errors << <<~MSG
323
291
  Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message}
@@ -338,7 +306,7 @@ module RubyIndexer
338
306
  end
339
307
 
340
308
  @enhancements.each do |enhancement|
341
- enhancement.on_call_node_leave(@owner_stack.last, node, @file_path, @code_units_cache)
309
+ enhancement.on_call_node_leave(node)
342
310
  rescue StandardError => e
343
311
  @indexing_errors << <<~MSG
344
312
  Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message}
@@ -463,6 +431,98 @@ module RubyIndexer
463
431
  )
464
432
  end
465
433
 
434
+ sig do
435
+ params(
436
+ name: String,
437
+ node_location: Prism::Location,
438
+ signatures: T::Array[Entry::Signature],
439
+ visibility: Entry::Visibility,
440
+ comments: T.nilable(String),
441
+ ).void
442
+ end
443
+ def add_method(name, node_location, signatures, visibility: Entry::Visibility::PUBLIC, comments: nil)
444
+ location = Location.from_prism_location(node_location, @code_units_cache)
445
+
446
+ @index.add(Entry::Method.new(
447
+ name,
448
+ @file_path,
449
+ location,
450
+ location,
451
+ comments,
452
+ signatures,
453
+ visibility,
454
+ @owner_stack.last,
455
+ ))
456
+ end
457
+
458
+ sig do
459
+ params(
460
+ name: String,
461
+ full_location: Prism::Location,
462
+ name_location: Prism::Location,
463
+ comments: T.nilable(String),
464
+ ).void
465
+ end
466
+ def add_module(name, full_location, name_location, comments: nil)
467
+ location = Location.from_prism_location(full_location, @code_units_cache)
468
+ name_loc = Location.from_prism_location(name_location, @code_units_cache)
469
+
470
+ entry = Entry::Module.new(
471
+ actual_nesting(name),
472
+ @file_path,
473
+ location,
474
+ name_loc,
475
+ comments,
476
+ )
477
+
478
+ advance_namespace_stack(name, entry)
479
+ end
480
+
481
+ sig do
482
+ params(
483
+ name_or_nesting: T.any(String, T::Array[String]),
484
+ full_location: Prism::Location,
485
+ name_location: Prism::Location,
486
+ parent_class_name: T.nilable(String),
487
+ comments: T.nilable(String),
488
+ ).void
489
+ end
490
+ def add_class(name_or_nesting, full_location, name_location, parent_class_name: nil, comments: nil)
491
+ nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : actual_nesting(name_or_nesting)
492
+ entry = Entry::Class.new(
493
+ nesting,
494
+ @file_path,
495
+ Location.from_prism_location(full_location, @code_units_cache),
496
+ Location.from_prism_location(name_location, @code_units_cache),
497
+ comments,
498
+ parent_class_name,
499
+ )
500
+
501
+ advance_namespace_stack(T.must(nesting.last), entry)
502
+ end
503
+
504
+ sig { params(block: T.proc.params(index: Index, base: Entry::Namespace).void).void }
505
+ def register_included_hook(&block)
506
+ owner = @owner_stack.last
507
+ return unless owner
508
+
509
+ @index.register_included_hook(owner.name) do |index, base|
510
+ block.call(index, base)
511
+ end
512
+ end
513
+
514
+ sig { void }
515
+ def pop_namespace_stack
516
+ @stack.pop
517
+ @owner_stack.pop
518
+ @visibility_stack.pop
519
+ end
520
+
521
+ sig { returns(T.nilable(Entry::Namespace)) }
522
+ def current_owner
523
+ @owner_stack.last
524
+ end
525
+
466
526
  private
467
527
 
468
528
  sig do
@@ -661,8 +721,7 @@ module RubyIndexer
661
721
  comments = +""
662
722
 
663
723
  start_line = node.location.start_line - 1
664
- start_line -= 1 unless @comments_by_line.key?(start_line)
665
-
724
+ start_line -= 1 unless comment_exists_at?(start_line)
666
725
  start_line.downto(1) do |line|
667
726
  comment = @comments_by_line[line]
668
727
  break unless comment
@@ -683,6 +742,11 @@ module RubyIndexer
683
742
  comments
684
743
  end
685
744
 
745
+ sig { params(line: Integer).returns(T::Boolean) }
746
+ def comment_exists_at?(line)
747
+ @comments_by_line.key?(line) || !@source_lines[line - 1].to_s.strip.empty?
748
+ end
749
+
686
750
  sig { params(name: String).returns(String) }
687
751
  def fully_qualify_name(name)
688
752
  if @stack.empty? || name.start_with?("::")
@@ -746,16 +810,22 @@ module RubyIndexer
746
810
  return unless arguments
747
811
 
748
812
  arguments.each do |node|
749
- next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
750
-
751
- case operation
752
- when :include
753
- owner.mixin_operations << Entry::Include.new(node.full_name)
754
- when :prepend
755
- owner.mixin_operations << Entry::Prepend.new(node.full_name)
756
- when :extend
813
+ next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) ||
814
+ (node.is_a?(Prism::SelfNode) && operation == :extend)
815
+
816
+ if node.is_a?(Prism::SelfNode)
757
817
  singleton = @index.existing_or_new_singleton_class(owner.name)
758
- singleton.mixin_operations << Entry::Include.new(node.full_name)
818
+ singleton.mixin_operations << Entry::Include.new(owner.name)
819
+ else
820
+ case operation
821
+ when :include
822
+ owner.mixin_operations << Entry::Include.new(node.full_name)
823
+ when :prepend
824
+ owner.mixin_operations << Entry::Prepend.new(node.full_name)
825
+ when :extend
826
+ singleton = @index.existing_or_new_singleton_class(owner.name)
827
+ singleton.mixin_operations << Entry::Include.new(node.full_name)
828
+ end
759
829
  end
760
830
  rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
761
831
  Prism::ConstantPathNode::MissingNodesInConstantPathError
@@ -910,5 +980,13 @@ module RubyIndexer
910
980
 
911
981
  corrected_nesting
912
982
  end
983
+
984
+ sig { params(short_name: String, entry: Entry::Namespace).void }
985
+ def advance_namespace_stack(short_name, entry)
986
+ @visibility_stack.push(Entry::Visibility::PUBLIC)
987
+ @owner_stack << entry
988
+ @index.add(entry)
989
+ @stack << short_name
990
+ end
913
991
  end
914
992
  end
@@ -8,38 +8,41 @@ module RubyIndexer
8
8
 
9
9
  abstract!
10
10
 
11
- sig { params(index: Index).void }
12
- def initialize(index)
13
- @index = index
11
+ @enhancements = T.let([], T::Array[T::Class[Enhancement]])
12
+
13
+ class << self
14
+ extend T::Sig
15
+
16
+ sig { params(child: T::Class[Enhancement]).void }
17
+ def inherited(child)
18
+ @enhancements << child
19
+ super
20
+ end
21
+
22
+ sig { params(listener: DeclarationListener).returns(T::Array[Enhancement]) }
23
+ def all(listener)
24
+ @enhancements.map { |enhancement| enhancement.new(listener) }
25
+ end
26
+
27
+ # Only available for testing purposes
28
+ sig { void }
29
+ def clear
30
+ @enhancements.clear
31
+ end
32
+ end
33
+
34
+ sig { params(listener: DeclarationListener).void }
35
+ def initialize(listener)
36
+ @listener = listener
14
37
  end
15
38
 
16
39
  # The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to
17
40
  # register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the
18
41
  # `ClassMethods` modules
19
- sig do
20
- overridable.params(
21
- owner: T.nilable(Entry::Namespace),
22
- node: Prism::CallNode,
23
- file_path: String,
24
- code_units_cache: T.any(
25
- T.proc.params(arg0: Integer).returns(Integer),
26
- Prism::CodeUnitsCache,
27
- ),
28
- ).void
29
- end
30
- def on_call_node_enter(owner, node, file_path, code_units_cache); end
31
-
32
- sig do
33
- overridable.params(
34
- owner: T.nilable(Entry::Namespace),
35
- node: Prism::CallNode,
36
- file_path: String,
37
- code_units_cache: T.any(
38
- T.proc.params(arg0: Integer).returns(Integer),
39
- Prism::CodeUnitsCache,
40
- ),
41
- ).void
42
- end
43
- def on_call_node_leave(owner, node, file_path, code_units_cache); end
42
+ sig { overridable.params(node: Prism::CallNode).void }
43
+ def on_call_node_enter(node); end # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
44
+
45
+ sig { overridable.params(node: Prism::CallNode).void }
46
+ def on_call_node_leave(node); end # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
44
47
  end
45
48
  end
@@ -7,6 +7,7 @@ module RubyIndexer
7
7
 
8
8
  class UnresolvableAliasError < StandardError; end
9
9
  class NonExistingNamespaceError < StandardError; end
10
+ class IndexNotEmptyError < StandardError; end
10
11
 
11
12
  # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
12
13
  ENTRY_SIMILARITY_THRESHOLD = 0.7
@@ -39,9 +40,6 @@ module RubyIndexer
39
40
  # Holds the linearized ancestors list for every namespace
40
41
  @ancestors = T.let({}, T::Hash[String, T::Array[String]])
41
42
 
42
- # List of classes that are enhancing the index
43
- @enhancements = T.let([], T::Array[Enhancement])
44
-
45
43
  # Map of module name to included hooks that have to be executed when we include the given module
46
44
  @included_hooks = T.let(
47
45
  {},
@@ -51,12 +49,6 @@ module RubyIndexer
51
49
  @configuration = T.let(RubyIndexer::Configuration.new, Configuration)
52
50
  end
53
51
 
54
- # Register an enhancement to the index. Enhancements must conform to the `Enhancement` interface
55
- sig { params(enhancement: Enhancement).void }
56
- def register_enhancement(enhancement)
57
- @enhancements << enhancement
58
- end
59
-
60
52
  # Register an included `hook` that will be executed when `module_name` is included into any namespace
61
53
  sig { params(module_name: String, hook: T.proc.params(index: Index, base: Entry::Namespace).void).void }
62
54
  def register_included_hook(module_name, &hook)
@@ -360,6 +352,15 @@ module RubyIndexer
360
352
  ).void
361
353
  end
362
354
  def index_all(indexable_paths: @configuration.indexables, &block)
355
+ # When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the
356
+ # existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this
357
+ # behavior and can take appropriate action.
358
+ # binding.break
359
+ if @entries.any?
360
+ raise IndexNotEmptyError,
361
+ "The index is not empty. To prevent invalid entries, `index_all` can only be called once."
362
+ end
363
+
363
364
  RBSIndexer.new(self).index_ruby_core
364
365
  # Calculate how many paths are worth 1% of progress
365
366
  progress_step = (indexable_paths.length / 100.0).ceil
@@ -386,7 +387,6 @@ module RubyIndexer
386
387
  result,
387
388
  indexable_path.full_path,
388
389
  collect_comments: collect_comments,
389
- enhancements: @enhancements,
390
390
  )
391
391
  dispatcher.dispatch(result.value)
392
392
 
@@ -302,10 +302,10 @@ module RubyIndexer
302
302
  RUBY
303
303
 
304
304
  b_const = @index["A::B"].first
305
- assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
305
+ assert_predicate(b_const, :private?)
306
306
 
307
307
  c_const = @index["A::C"].first
308
- assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
308
+ assert_predicate(c_const, :private?)
309
309
 
310
310
  d_const = @index["A::D"].first
311
311
  assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
@@ -160,5 +160,15 @@ module RubyIndexer
160
160
  )
161
161
  end
162
162
  end
163
+
164
+ def test_includes_top_level_files
165
+ Dir.mktmpdir do |dir|
166
+ FileUtils.touch(File.join(dir, "find_me.rb"))
167
+ @config.workspace_path = dir
168
+
169
+ indexables = @config.indexables
170
+ assert(indexables.find { |i| File.basename(i.full_path) == "find_me.rb" })
171
+ end
172
+ end
163
173
  end
164
174
  end
@@ -130,13 +130,13 @@ module RubyIndexer
130
130
  RUBY
131
131
 
132
132
  b_const = @index["A::B"].first
133
- assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
133
+ assert_predicate(b_const, :private?)
134
134
 
135
135
  c_const = @index["A::C"].first
136
- assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
136
+ assert_predicate(c_const, :private?)
137
137
 
138
138
  d_const = @index["A::D"].first
139
- assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
139
+ assert_predicate(d_const, :public?)
140
140
  end
141
141
 
142
142
  def test_marking_constants_as_private_reopening_namespaces
@@ -163,13 +163,13 @@ module RubyIndexer
163
163
  RUBY
164
164
 
165
165
  a_const = @index["A::B::CONST_A"].first
166
- assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
166
+ assert_predicate(a_const, :private?)
167
167
 
168
168
  b_const = @index["A::B::CONST_B"].first
169
- assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
169
+ assert_predicate(b_const, :private?)
170
170
 
171
171
  c_const = @index["A::B::CONST_C"].first
172
- assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
172
+ assert_predicate(c_const, :private?)
173
173
  end
174
174
 
175
175
  def test_marking_constants_as_private_with_receiver
@@ -187,10 +187,10 @@ module RubyIndexer
187
187
  RUBY
188
188
 
189
189
  a_const = @index["A::B::CONST_A"].first
190
- assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
190
+ assert_predicate(a_const, :private?)
191
191
 
192
192
  b_const = @index["A::B::CONST_B"].first
193
- assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
193
+ assert_predicate(b_const, :private?)
194
194
  end
195
195
 
196
196
  def test_indexing_constant_aliases
@@ -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,39 @@ module RubyIndexer
149
149
  end
150
150
  end
151
151
 
152
+ def test_comments_documentation
153
+ index(<<~RUBY)
154
+ # Documentation for Foo
155
+
156
+ class Foo
157
+ # ####################
158
+ # Documentation for bar
159
+ # ####################
160
+ #
161
+ def bar
162
+ end
163
+
164
+ # test
165
+
166
+ # Documentation for baz
167
+ def baz; end
168
+ def ban; end
169
+ end
170
+ RUBY
171
+
172
+ foo_comment = @index["Foo"].first.comments
173
+ assert_equal("Documentation for Foo", foo_comment)
174
+
175
+ bar_comment = @index["bar"].first.comments
176
+ assert_equal("####################\nDocumentation for bar\n####################\n", bar_comment)
177
+
178
+ baz_comment = @index["baz"].first.comments
179
+ assert_equal("Documentation for baz", baz_comment)
180
+
181
+ ban_comment = @index["ban"].first.comments
182
+ assert_empty(ban_comment)
183
+ end
184
+
152
185
  def test_method_with_parameters
153
186
  index(<<~RUBY)
154
187
  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)
@@ -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"
@@ -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
@@ -12,7 +12,7 @@ require "digest"
12
12
  require "time"
13
13
  require "uri"
14
14
 
15
- # This file is a script that will configure a custom bundle for the Ruby LSP. The custom bundle allows developers to use
15
+ # This file is a script that will configure a composed bundle for the Ruby LSP. The composed bundle allows developers to use
16
16
  # the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
17
17
  # exact locked versions of dependencies.
18
18
 
@@ -62,7 +62,7 @@ module RubyLsp
62
62
  @retry = T.let(false, T::Boolean)
63
63
  end
64
64
 
65
- # Sets up the custom bundle and returns the `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_APP_CONFIG` that should be
65
+ # Sets up the composed bundle and returns the `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_APP_CONFIG` that should be
66
66
  # used for running the server
67
67
  sig { returns(T::Hash[String, String]) }
68
68
  def setup!
@@ -73,12 +73,12 @@ module RubyLsp
73
73
  ignore_file = @custom_dir + ".gitignore"
74
74
  ignore_file.write("*") unless ignore_file.exist?
75
75
 
76
- # Do not set up a custom bundle if LSP dependencies are already in the Gemfile
76
+ # Do not set up a composed bundle if LSP dependencies are already in the Gemfile
77
77
  if @dependencies["ruby-lsp"] &&
78
78
  @dependencies["debug"] &&
79
79
  (@rails_app ? @dependencies["ruby-lsp-rails"] : true)
80
80
  $stderr.puts(
81
- "Ruby LSP> Skipping custom bundle setup since LSP dependencies are already in #{@gemfile}",
81
+ "Ruby LSP> Skipping composed bundle setup since LSP dependencies are already in #{@gemfile}",
82
82
  )
83
83
 
84
84
  return run_bundle_install
@@ -96,7 +96,7 @@ module RubyLsp
96
96
 
97
97
  if @custom_lockfile.exist? && @lockfile_hash_path.exist? && @lockfile_hash_path.read == current_lockfile_hash
98
98
  $stderr.puts(
99
- "Ruby LSP> Skipping custom bundle setup since #{@custom_lockfile} already exists and is up to date",
99
+ "Ruby LSP> Skipping composed bundle setup since #{@custom_lockfile} already exists and is up to date",
100
100
  )
101
101
  return run_bundle_install(@custom_gemfile)
102
102
  end
@@ -110,8 +110,8 @@ module RubyLsp
110
110
  private
111
111
 
112
112
  sig { returns(T::Hash[String, T.untyped]) }
113
- def custom_bundle_dependencies
114
- @custom_bundle_dependencies ||= T.let(
113
+ def composed_bundle_dependencies
114
+ @composed_bundle_dependencies ||= T.let(
115
115
  begin
116
116
  original_bundle_gemfile = ENV["BUNDLE_GEMFILE"]
117
117
 
@@ -136,8 +136,8 @@ module RubyLsp
136
136
  "",
137
137
  ]
138
138
 
139
- # If there's a top level Gemfile, we want to evaluate from the custom bundle. We get the source from the top level
140
- # Gemfile, so if there isn't one we need to add a default source
139
+ # If there's a top level Gemfile, we want to evaluate from the composed bundle. We get the source from the top
140
+ # level Gemfile, so if there isn't one we need to add a default source
141
141
  if @gemfile&.exist? && @lockfile&.exist?
142
142
  parts << "eval_gemfile(File.expand_path(\"../#{@gemfile_name}\", __dir__))"
143
143
  else
@@ -187,7 +187,7 @@ module RubyLsp
187
187
  env = bundler_settings_as_env
188
188
  env["BUNDLE_GEMFILE"] = bundle_gemfile.to_s
189
189
 
190
- # If the user has a custom bundle path configured, we need to ensure that we will use the absolute and not
190
+ # If the user has a composed bundle path configured, we need to ensure that we will use the absolute and not
191
191
  # relative version of it when running `bundle install`. This is necessary to avoid installing the gems under the
192
192
  # `.ruby-lsp` folder, which is not the user's intention. For example, if the path is configured as `vendor`, we
193
193
  # want to install it in the top level `vendor` and not `.ruby-lsp/vendor`
@@ -244,7 +244,7 @@ module RubyLsp
244
244
  base_bundle = base_bundle_command(env)
245
245
 
246
246
  # If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
247
- # to upgrade them or else we'll produce undesired source control changes. If the custom bundle was just created
247
+ # to upgrade them or else we'll produce undesired source control changes. If the composed bundle was just created
248
248
  # and any of `ruby-lsp`, `ruby-lsp-rails` or `debug` weren't a part of the Gemfile, then we need to run `bundle
249
249
  # install` for the first time to generate the Gemfile.lock with them included or else Bundler will complain that
250
250
  # they're missing. We can only update if the custom `.ruby-lsp/Gemfile.lock` already exists and includes all gems
@@ -274,16 +274,16 @@ module RubyLsp
274
274
  command << "1>&2"
275
275
 
276
276
  # Add bundle update
277
- $stderr.puts("Ruby LSP> Running bundle install for the custom bundle. This may take a while...")
277
+ $stderr.puts("Ruby LSP> Running bundle install for the composed bundle. This may take a while...")
278
278
  $stderr.puts("Ruby LSP> Command: #{command}")
279
279
 
280
- # Try to run the bundle install or update command. If that fails, it normally means that the custom lockfile is in
281
- # a bad state that no longer reflects the top level one. In that case, we can remove the whole directory, try
280
+ # Try to run the bundle install or update command. If that fails, it normally means that the composed lockfile is
281
+ # in a bad state that no longer reflects the top level one. In that case, we can remove the whole directory, try
282
282
  # another time and give up if it fails again
283
283
  if !system(env, command) && !@retry && @custom_gemfile.exist?
284
284
  @retry = true
285
285
  @custom_dir.rmtree
286
- $stderr.puts("Ruby LSP> Running bundle install failed. Trying to re-generate the custom bundle from scratch")
286
+ $stderr.puts("Ruby LSP> Running bundle install failed. Trying to re-generate the composed bundle from scratch")
287
287
  return setup!
288
288
  end
289
289
 
@@ -330,14 +330,14 @@ module RubyLsp
330
330
  if @rails_app
331
331
  return false if @dependencies.values_at("ruby-lsp", "ruby-lsp-rails", "debug").all?
332
332
 
333
- # If the custom lockfile doesn't include `ruby-lsp`, `ruby-lsp-rails` or `debug`, we need to run bundle install
334
- # before updating
335
- return false if custom_bundle_dependencies.values_at("ruby-lsp", "debug", "ruby-lsp-rails").any?(&:nil?)
333
+ # If the composed lockfile doesn't include `ruby-lsp`, `ruby-lsp-rails` or `debug`, we need to run bundle
334
+ # install before updating
335
+ return false if composed_bundle_dependencies.values_at("ruby-lsp", "debug", "ruby-lsp-rails").any?(&:nil?)
336
336
  else
337
337
  return false if @dependencies.values_at("ruby-lsp", "debug").all?
338
338
 
339
- # If the custom lockfile doesn't include `ruby-lsp` or `debug`, we need to run bundle install before updating
340
- return false if custom_bundle_dependencies.values_at("ruby-lsp", "debug").any?(&:nil?)
339
+ # If the composed lockfile doesn't include `ruby-lsp` or `debug`, we need to run bundle install before updating
340
+ return false if composed_bundle_dependencies.values_at("ruby-lsp", "debug").any?(&:nil?)
341
341
  end
342
342
 
343
343
  # If the last updated file doesn't exist or was updated more than 4 hours ago, we should update
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.21.3
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-05 00:00:00.000000000 Z
11
+ date: 2024-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -217,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
217
  - !ruby/object:Gem::Version
218
218
  version: '0'
219
219
  requirements: []
220
- rubygems_version: 3.5.22
220
+ rubygems_version: 3.5.23
221
221
  signing_key:
222
222
  specification_version: 4
223
223
  summary: An opinionated language server for Ruby