ruby-lsp 0.17.11 → 0.17.13

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: 13815c41a1b7509782277cb5dac09481130e18efc4253a01cede31ca41b335a7
4
- data.tar.gz: 40e6584ff6ff743ea1e62a29d6090d80bf318ae3dd5d90808de1b35723bea063
3
+ metadata.gz: 042725d7afce428b5c024933a7101151f2f69c57cbb40b4f938384c13d6c974b
4
+ data.tar.gz: bec8636d402451e1009e87ddd98e6fdedc062643e614b596a0698ffcfcc9b271
5
5
  SHA512:
6
- metadata.gz: 74da4b254b598cd4b10701b65c49d045cc33be83330399e67602f8e43597c9e54567715343a67fa720b31084de540e498cc3d5525e64679500d52b4842e44cf7
7
- data.tar.gz: 313695187e34ceb34c50d320bbe0abe0bda18d99157e8f4710faf8f72bc5028abd813511665fd4eac274dd73606eebb1c901b949fa307a8f925c2bfaa39b0c45
6
+ metadata.gz: 6a6adb40a9ccaaf916f46c041f3eb2cc7a81be73c5d3bf7c05a45472cd9d08e5a9dd04d804b8e15f091645d420037e324368ccbcc619311a1c60c81f241c1b89
7
+ data.tar.gz: d6b19c8d02cfab8dca9e4d675e8ee549e8b392631a6080757c0413da29a07d1db411d9360b7553a798805d9e6faac7318863e35ba664488c5eb41c869a7e734a
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.11
1
+ 0.17.13
data/exe/ruby-lsp CHANGED
@@ -112,20 +112,20 @@ if options[:time_index]
112
112
  end
113
113
 
114
114
  if options[:doctor]
115
+ index = RubyIndexer::Index.new
116
+
115
117
  if File.exist?(".index.yml")
116
118
  begin
117
119
  config = YAML.parse_file(".index.yml").to_ruby
118
120
  rescue => e
119
121
  abort("Error parsing config: #{e.message}")
120
122
  end
121
- RubyIndexer.configuration.apply_config(config)
123
+ index.configuration.apply_config(config)
122
124
  end
123
125
 
124
- index = RubyIndexer::Index.new
125
-
126
126
  puts "Globbing for indexable files"
127
127
 
128
- RubyIndexer.configuration.indexables.each do |indexable|
128
+ index.configuration.indexables.each do |indexable|
129
129
  puts "indexing: #{indexable.full_path}"
130
130
  index.index_single(indexable)
131
131
  end
data/exe/ruby-lsp-check CHANGED
@@ -44,7 +44,7 @@ puts "\n"
44
44
  puts "Verifying that indexing executes successfully. This may take a while..."
45
45
 
46
46
  index = RubyIndexer::Index.new
47
- indexables = RubyIndexer.configuration.indexables
47
+ indexables = index.configuration.indexables
48
48
 
49
49
  indexables.each_with_index do |indexable, i|
50
50
  index.index_single(indexable)
@@ -8,12 +8,22 @@ module RubyIndexer
8
8
  OBJECT_NESTING = T.let(["Object"].freeze, T::Array[String])
9
9
  BASIC_OBJECT_NESTING = T.let(["BasicObject"].freeze, T::Array[String])
10
10
 
11
+ sig { returns(T::Array[String]) }
12
+ attr_reader :indexing_errors
13
+
11
14
  sig do
12
- params(index: Index, dispatcher: Prism::Dispatcher, parse_result: Prism::ParseResult, file_path: String).void
15
+ params(
16
+ index: Index,
17
+ dispatcher: Prism::Dispatcher,
18
+ parse_result: Prism::ParseResult,
19
+ file_path: String,
20
+ enhancements: T::Array[Enhancement],
21
+ ).void
13
22
  end
14
- def initialize(index, dispatcher, parse_result, file_path)
23
+ def initialize(index, dispatcher, parse_result, file_path, enhancements: [])
15
24
  @index = index
16
25
  @file_path = file_path
26
+ @enhancements = enhancements
17
27
  @visibility_stack = T.let([Entry::Visibility::PUBLIC], T::Array[Entry::Visibility])
18
28
  @comments_by_line = T.let(
19
29
  parse_result.comments.to_h do |c|
@@ -29,6 +39,7 @@ module RubyIndexer
29
39
 
30
40
  # A stack of namespace entries that represent where we currently are. Used to properly assign methods to an owner
31
41
  @owner_stack = T.let([], T::Array[Entry::Namespace])
42
+ @indexing_errors = T.let([], T::Array[String])
32
43
 
33
44
  dispatcher.register(
34
45
  self,
@@ -279,6 +290,12 @@ module RubyIndexer
279
290
  when :private
280
291
  @visibility_stack.push(Entry::Visibility::PRIVATE)
281
292
  end
293
+
294
+ @enhancements.each do |enhancement|
295
+ enhancement.on_call_node(@index, @owner_stack.last, node, @file_path)
296
+ rescue StandardError => e
297
+ @indexing_errors << "Indexing error in #{@file_path} with '#{enhancement.class.name}' enhancement: #{e.message}"
298
+ end
282
299
  end
283
300
 
284
301
  sig { params(node: Prism::CallNode).void }
@@ -535,7 +552,7 @@ module RubyIndexer
535
552
  comment_content = comment.location.slice.chomp
536
553
 
537
554
  # invalid encodings would raise an "invalid byte sequence" exception
538
- if !comment_content.valid_encoding? || comment_content.match?(RubyIndexer.configuration.magic_comment_regex)
555
+ if !comment_content.valid_encoding? || comment_content.match?(@index.configuration.magic_comment_regex)
539
556
  next
540
557
  end
541
558
 
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ module Enhancement
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ interface!
10
+
11
+ requires_ancestor { Object }
12
+
13
+ # The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to
14
+ # register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the
15
+ # `ClassMethods` modules
16
+ sig do
17
+ abstract.params(
18
+ index: Index,
19
+ owner: T.nilable(Entry::Namespace),
20
+ node: Prism::CallNode,
21
+ file_path: String,
22
+ ).void
23
+ end
24
+ def on_call_node(index, owner, node, file_path); end
25
+ end
26
+ end
@@ -342,6 +342,19 @@ module RubyIndexer
342
342
 
343
343
  "(#{first_signature.format})"
344
344
  end
345
+
346
+ sig { returns(String) }
347
+ def formatted_signatures
348
+ overloads_count = signatures.size
349
+ case overloads_count
350
+ when 1
351
+ ""
352
+ when 2
353
+ "\n(+1 overload)"
354
+ else
355
+ "\n(+#{overloads_count - 1} overloads)"
356
+ end
357
+ end
345
358
  end
346
359
 
347
360
  class Accessor < Member
@@ -542,6 +555,16 @@ module RubyIndexer
542
555
  def decorated_parameters
543
556
  @target.decorated_parameters
544
557
  end
558
+
559
+ sig { returns(String) }
560
+ def formatted_signatures
561
+ @target.formatted_signatures
562
+ end
563
+
564
+ sig { returns(T::Array[Signature]) }
565
+ def signatures
566
+ @target.signatures
567
+ end
545
568
  end
546
569
 
547
570
  # Ruby doesn't support method overloading, so a method will have only one signature.
@@ -11,6 +11,9 @@ module RubyIndexer
11
11
  # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
12
12
  ENTRY_SIMILARITY_THRESHOLD = 0.7
13
13
 
14
+ sig { returns(Configuration) }
15
+ attr_reader :configuration
16
+
14
17
  sig { void }
15
18
  def initialize
16
19
  # Holds all entries in the index using the following format:
@@ -35,6 +38,29 @@ module RubyIndexer
35
38
 
36
39
  # Holds the linearized ancestors list for every namespace
37
40
  @ancestors = T.let({}, T::Hash[String, T::Array[String]])
41
+
42
+ # List of classes that are enhancing the index
43
+ @enhancements = T.let([], T::Array[Enhancement])
44
+
45
+ # Map of module name to included hooks that have to be executed when we include the given module
46
+ @included_hooks = T.let(
47
+ {},
48
+ T::Hash[String, T::Array[T.proc.params(index: Index, base: Entry::Namespace).void]],
49
+ )
50
+
51
+ @configuration = T.let(RubyIndexer::Configuration.new, Configuration)
52
+ end
53
+
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
+ # Register an included `hook` that will be executed when `module_name` is included into any namespace
61
+ sig { params(module_name: String, hook: T.proc.params(index: Index, base: Entry::Namespace).void).void }
62
+ def register_included_hook(module_name, &hook)
63
+ (@included_hooks[module_name] ||= []) << hook
38
64
  end
39
65
 
40
66
  sig { params(indexable: IndexablePath).void }
@@ -275,7 +301,7 @@ module RubyIndexer
275
301
  block: T.nilable(T.proc.params(progress: Integer).returns(T::Boolean)),
276
302
  ).void
277
303
  end
278
- def index_all(indexable_paths: RubyIndexer.configuration.indexables, &block)
304
+ def index_all(indexable_paths: @configuration.indexables, &block)
279
305
  RBSIndexer.new(self).index_ruby_core
280
306
  # Calculate how many paths are worth 1% of progress
281
307
  progress_step = (indexable_paths.length / 100.0).ceil
@@ -296,11 +322,25 @@ module RubyIndexer
296
322
  dispatcher = Prism::Dispatcher.new
297
323
 
298
324
  result = Prism.parse(content)
299
- DeclarationListener.new(self, dispatcher, result, indexable_path.full_path)
325
+ listener = DeclarationListener.new(
326
+ self,
327
+ dispatcher,
328
+ result,
329
+ indexable_path.full_path,
330
+ enhancements: @enhancements,
331
+ )
300
332
  dispatcher.dispatch(result.value)
301
333
 
334
+ indexing_errors = listener.indexing_errors.uniq
335
+
302
336
  require_path = indexable_path.require_path
303
337
  @require_paths_tree.insert(require_path, indexable_path) if require_path
338
+
339
+ if indexing_errors.any?
340
+ indexing_errors.each do |error|
341
+ $stderr.puts error
342
+ end
343
+ end
304
344
  rescue Errno::EISDIR, Errno::ENOENT
305
345
  # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
306
346
  # it
@@ -457,6 +497,12 @@ module RubyIndexer
457
497
  end
458
498
  end
459
499
 
500
+ # We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add
501
+ # new singleton methods or to extend a module through an include. There's no need to support instance methods, the
502
+ # inclusion of another module or the prepending of another module, because those features are already a part of
503
+ # Ruby and can be used directly without any metaprogramming
504
+ run_included_hooks(attached_class_name, nesting) if singleton_levels > 0
505
+
460
506
  linearize_mixins(ancestors, namespaces, nesting)
461
507
  linearize_superclass(
462
508
  ancestors,
@@ -570,6 +616,34 @@ module RubyIndexer
570
616
 
571
617
  private
572
618
 
619
+ # Runs the registered included hooks
620
+ sig { params(fully_qualified_name: String, nesting: T::Array[String]).void }
621
+ def run_included_hooks(fully_qualified_name, nesting)
622
+ return if @included_hooks.empty?
623
+
624
+ namespaces = self[fully_qualified_name]&.grep(Entry::Namespace)
625
+ return unless namespaces
626
+
627
+ namespaces.each do |namespace|
628
+ namespace.mixin_operations.each do |operation|
629
+ next unless operation.is_a?(Entry::Include)
630
+
631
+ # First we resolve the include name, so that we know the actual module being referred to in the include
632
+ resolved_modules = resolve(operation.module_name, nesting)
633
+ next unless resolved_modules
634
+
635
+ module_name = T.must(resolved_modules.first).name
636
+
637
+ # Then we grab any hooks registered for that module
638
+ hooks = @included_hooks[module_name]
639
+ next unless hooks
640
+
641
+ # We invoke the hooks with the index and the namespace that included the module
642
+ hooks.each { |hook| hook.call(self, namespace) }
643
+ end
644
+ end
645
+ end
646
+
573
647
  # Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
574
648
  # linearized ancestors of the mixins
575
649
  sig do
@@ -6,6 +6,7 @@ require "did_you_mean"
6
6
 
7
7
  require "ruby_indexer/lib/ruby_indexer/indexable_path"
8
8
  require "ruby_indexer/lib/ruby_indexer/declaration_listener"
9
+ require "ruby_indexer/lib/ruby_indexer/enhancement"
9
10
  require "ruby_indexer/lib/ruby_indexer/index"
10
11
  require "ruby_indexer/lib/ruby_indexer/entry"
11
12
  require "ruby_indexer/lib/ruby_indexer/configuration"
@@ -14,12 +15,4 @@ require "ruby_indexer/lib/ruby_indexer/location"
14
15
  require "ruby_indexer/lib/ruby_indexer/rbs_indexer"
15
16
 
16
17
  module RubyIndexer
17
- @configuration = T.let(Configuration.new, Configuration)
18
-
19
- class << self
20
- extend T::Sig
21
-
22
- sig { returns(Configuration) }
23
- attr_reader :configuration
24
- end
25
18
  end
@@ -108,7 +108,7 @@ module RubyIndexer
108
108
  end
109
109
 
110
110
  def test_magic_comments_regex
111
- regex = RubyIndexer.configuration.magic_comment_regex
111
+ regex = @config.magic_comment_regex
112
112
 
113
113
  [
114
114
  "# frozen_string_literal:",
@@ -0,0 +1,197 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test_case"
5
+
6
+ module RubyIndexer
7
+ class EnhancementTest < TestCase
8
+ def test_enhancing_indexing_included_hook
9
+ enhancement_class = Class.new do
10
+ include Enhancement
11
+
12
+ def on_call_node(index, owner, node, file_path)
13
+ return unless owner
14
+ return unless node.name == :extend
15
+
16
+ arguments = node.arguments&.arguments
17
+ return unless arguments
18
+
19
+ location = node.location
20
+
21
+ arguments.each do |node|
22
+ next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
23
+
24
+ module_name = node.full_name
25
+ next unless module_name == "ActiveSupport::Concern"
26
+
27
+ index.register_included_hook(owner.name) do |index, base|
28
+ class_methods_name = "#{owner.name}::ClassMethods"
29
+
30
+ if index.indexed?(class_methods_name)
31
+ singleton = index.existing_or_new_singleton_class(base.name)
32
+ singleton.mixin_operations << Entry::Include.new(class_methods_name)
33
+ end
34
+ end
35
+
36
+ index.add(Entry::Method.new(
37
+ "new_method",
38
+ file_path,
39
+ location,
40
+ location,
41
+ [],
42
+ [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
43
+ Entry::Visibility::PUBLIC,
44
+ owner,
45
+ ))
46
+ rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
47
+ Prism::ConstantPathNode::MissingNodesInConstantPathError
48
+ # Do nothing
49
+ end
50
+ end
51
+ end
52
+
53
+ @index.register_enhancement(enhancement_class.new)
54
+ index(<<~RUBY)
55
+ module ActiveSupport
56
+ module Concern
57
+ def self.extended(base)
58
+ base.class_eval("def new_method(a); end")
59
+ end
60
+ end
61
+ end
62
+
63
+ module ActiveRecord
64
+ module Associations
65
+ extend ActiveSupport::Concern
66
+
67
+ module ClassMethods
68
+ def belongs_to(something); end
69
+ end
70
+ end
71
+
72
+ class Base
73
+ include Associations
74
+ end
75
+ end
76
+
77
+ class User < ActiveRecord::Base
78
+ end
79
+ RUBY
80
+
81
+ assert_equal(
82
+ [
83
+ "User::<Class:User>",
84
+ "ActiveRecord::Base::<Class:Base>",
85
+ "ActiveRecord::Associations::ClassMethods",
86
+ "Object::<Class:Object>",
87
+ "BasicObject::<Class:BasicObject>",
88
+ "Class",
89
+ "Module",
90
+ "Object",
91
+ "Kernel",
92
+ "BasicObject",
93
+ ],
94
+ @index.linearized_ancestors_of("User::<Class:User>"),
95
+ )
96
+
97
+ assert_entry("new_method", Entry::Method, "/fake/path/foo.rb:10-4:10-33")
98
+ end
99
+
100
+ def test_enhancing_indexing_configuration_dsl
101
+ enhancement_class = Class.new do
102
+ include Enhancement
103
+
104
+ def on_call_node(index, owner, node, file_path)
105
+ return unless owner
106
+
107
+ name = node.name
108
+ return unless name == :has_many
109
+
110
+ arguments = node.arguments&.arguments
111
+ return unless arguments
112
+
113
+ association_name = arguments.first
114
+ return unless association_name.is_a?(Prism::SymbolNode)
115
+
116
+ location = association_name.location
117
+
118
+ index.add(Entry::Method.new(
119
+ T.must(association_name.value),
120
+ file_path,
121
+ location,
122
+ location,
123
+ [],
124
+ [],
125
+ Entry::Visibility::PUBLIC,
126
+ owner,
127
+ ))
128
+ end
129
+ end
130
+
131
+ @index.register_enhancement(enhancement_class.new)
132
+ index(<<~RUBY)
133
+ module ActiveSupport
134
+ module Concern
135
+ def self.extended(base)
136
+ base.class_eval("def new_method(a); end")
137
+ end
138
+ end
139
+ end
140
+
141
+ module ActiveRecord
142
+ module Associations
143
+ extend ActiveSupport::Concern
144
+
145
+ module ClassMethods
146
+ def belongs_to(something); end
147
+ end
148
+ end
149
+
150
+ class Base
151
+ include Associations
152
+ end
153
+ end
154
+
155
+ class User < ActiveRecord::Base
156
+ has_many :posts
157
+ end
158
+ RUBY
159
+
160
+ assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17")
161
+ end
162
+
163
+ def test_error_handling_in_enhancement
164
+ enhancement_class = Class.new do
165
+ include Enhancement
166
+
167
+ def on_call_node(index, owner, node, file_path)
168
+ raise "Error"
169
+ end
170
+
171
+ class << self
172
+ def name
173
+ "TestEnhancement"
174
+ end
175
+ end
176
+ end
177
+
178
+ @index.register_enhancement(enhancement_class.new)
179
+
180
+ _stdout, stderr = capture_io do
181
+ index(<<~RUBY)
182
+ module ActiveSupport
183
+ module Concern
184
+ def self.extended(base)
185
+ base.class_eval("def new_method(a); end")
186
+ end
187
+ end
188
+ end
189
+ RUBY
190
+ end
191
+
192
+ assert_match(%r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' enhancement}, stderr)
193
+ # The module should still be indexed
194
+ assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
195
+ end
196
+ end
197
+ end
@@ -49,15 +49,18 @@ module RubyLsp
49
49
  super
50
50
  end
51
51
 
52
- # Discovers and loads all addons. Returns the list of activated addons
53
- sig { params(global_state: GlobalState, outgoing_queue: Thread::Queue).returns(T::Array[Addon]) }
52
+ # Discovers and loads all addons. Returns a list of errors when trying to require addons
53
+ sig do
54
+ params(global_state: GlobalState, outgoing_queue: Thread::Queue).returns(T::Array[StandardError])
55
+ end
54
56
  def load_addons(global_state, outgoing_queue)
55
57
  # Require all addons entry points, which should be placed under
56
58
  # `some_gem/lib/ruby_lsp/your_gem_name/addon.rb`
57
- Gem.find_files("ruby_lsp/**/addon.rb").each do |addon|
59
+ errors = Gem.find_files("ruby_lsp/**/addon.rb").filter_map do |addon|
58
60
  require File.expand_path(addon)
61
+ nil
59
62
  rescue => e
60
- $stderr.puts(e.full_message)
63
+ e
61
64
  end
62
65
 
63
66
  # Instantiate all discovered addon classes
@@ -71,6 +74,8 @@ module RubyLsp
71
74
  rescue => e
72
75
  addon.add_error(e)
73
76
  end
77
+
78
+ errors
74
79
  end
75
80
 
76
81
  # Intended for use by tests for addons
@@ -65,7 +65,7 @@ module RubyLsp
65
65
  when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
66
66
  process_message(message)
67
67
  when "shutdown"
68
- $stderr.puts("Shutting down Ruby LSP...")
68
+ send_log_message("Shutting down Ruby LSP...")
69
69
 
70
70
  shutdown
71
71
 
@@ -76,7 +76,7 @@ module RubyLsp
76
76
  when "exit"
77
77
  @mutex.synchronize do
78
78
  status = @incoming_queue.closed? ? 0 : 1
79
- $stderr.puts("Shutdown complete with status #{status}")
79
+ send_log_message("Shutdown complete with status #{status}")
80
80
  exit(status)
81
81
  end
82
82
  else
@@ -145,5 +145,10 @@ module RubyLsp
145
145
  def send_empty_response(id)
146
146
  send_message(Result.new(id: id, response: nil))
147
147
  end
148
+
149
+ sig { params(message: String, type: Integer).void }
150
+ def send_log_message(message, type: Constant::MessageType::LOG)
151
+ send_message(Notification.window_log_message(message, type: Constant::MessageType::LOG))
152
+ end
148
153
  end
149
154
  end
@@ -10,16 +10,6 @@ module RubyLsp
10
10
  end
11
11
  end
12
12
 
13
- class SorbetLevel < T::Enum
14
- enums do
15
- None = new("none")
16
- Ignore = new("ignore")
17
- False = new("false")
18
- True = new("true")
19
- Strict = new("strict")
20
- end
21
- end
22
-
23
13
  extend T::Sig
24
14
  extend T::Helpers
25
15
 
@@ -223,26 +213,6 @@ module RubyLsp
223
213
  NodeContext.new(closest, parent, nesting_nodes, call_node)
224
214
  end
225
215
 
226
- sig { returns(SorbetLevel) }
227
- def sorbet_level
228
- sigil = parse_result.magic_comments.find do |comment|
229
- comment.key == "typed"
230
- end&.value
231
-
232
- case sigil
233
- when "ignore"
234
- SorbetLevel::Ignore
235
- when "false"
236
- SorbetLevel::False
237
- when "true"
238
- SorbetLevel::True
239
- when "strict", "strong"
240
- SorbetLevel::Strict
241
- else
242
- SorbetLevel::None
243
- end
244
- end
245
-
246
216
  class Scanner
247
217
  extend T::Sig
248
218