ruby-lsp 0.21.3 → 0.22.0

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: 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