ruby-lsp 0.17.14 → 0.17.15

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: 17c576dbc88c8c4012eb777b15df7df630a827d7692f6bceec06b68178b25357
4
- data.tar.gz: f023f22a818616b1c8c6fb1de77d0637e7bdae37b60b46e941b297aaa95729a9
3
+ metadata.gz: 43d715172510bba67801670dbfc574eed9851b9852aaea3f6e210acd74734af4
4
+ data.tar.gz: b35b3a55c3d65987390e4b125a090ff8f6da902bfa7ab9c08eb9eb8c72d5157f
5
5
  SHA512:
6
- metadata.gz: e0848d63fef1677ccf276182cdea5a8304e4a1e04a7ec4936f8f083c27bc62f575fe3f0a207f75805cf3c8eb106e43d5d1afe2df366028980cb5eee6ed97c14b
7
- data.tar.gz: d57869c1ee7dcec65becd9ee4fc225c91e946e697c3aab8a42d2453c1a85bcc91cd8e0a6364d810f8109c2b97102f101d24bf8986d2e4b3905b56368e260b0d3
6
+ metadata.gz: ef8a2ca24755eef8c5ed1146f1f6c40ec725988e9dd6a51efb326583e5606a221f733625f08d4dad5bb6091675c39fb18a485ff8c0bed1dc0b2e221a4390c585
7
+ data.tar.gz: 16101462585105a9a201f6341908f0b238f77f4acaa6dac723c95e35fcddc7c7b68d937bead5beecc14ae6bcac276b2f813523304f7eaded3132d3aa07eb943b
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.14
1
+ 0.17.15
@@ -16,15 +16,24 @@ module RubyIndexer
16
16
  T::Hash[String, T.untyped],
17
17
  )
18
18
 
19
+ sig { params(workspace_path: String).void }
20
+ attr_writer :workspace_path
21
+
19
22
  sig { void }
20
23
  def initialize
24
+ @workspace_path = T.let(Dir.pwd, String)
21
25
  @excluded_gems = T.let(initial_excluded_gems, T::Array[String])
22
26
  @included_gems = T.let([], T::Array[String])
23
- @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("**", "tmp", "**", "*")], T::Array[String])
27
+ @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("tmp", "**", "*")], T::Array[String])
28
+
24
29
  path = Bundler.settings["path"]
25
- @excluded_patterns << File.join(File.expand_path(path, Dir.pwd), "**", "*.rb") if path
30
+ if path
31
+ # Substitute Windows backslashes into forward slashes, which are used in glob patterns
32
+ glob = path.gsub(/[\\]+/, "/")
33
+ @excluded_patterns << File.join(glob, "**", "*.rb")
34
+ end
26
35
 
27
- @included_patterns = T.let([File.join(Dir.pwd, "**", "*.rb")], T::Array[String])
36
+ @included_patterns = T.let([File.join("**", "*.rb")], T::Array[String])
28
37
  @excluded_magic_comments = T.let(
29
38
  [
30
39
  "frozen_string_literal:",
@@ -55,12 +64,12 @@ module RubyIndexer
55
64
  indexables = @included_patterns.flat_map do |pattern|
56
65
  load_path_entry = T.let(nil, T.nilable(String))
57
66
 
58
- Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
67
+ Dir.glob(File.join(@workspace_path, pattern), File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
59
68
  path = File.expand_path(path)
60
69
  # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
61
70
  # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens
62
- # on repositories that define multiple gems, like Rails. All frameworks are defined inside the Dir.pwd, but
63
- # each one of them belongs to a different $LOAD_PATH entry
71
+ # on repositories that define multiple gems, like Rails. All frameworks are defined inside the current
72
+ # workspace directory, but each one of them belongs to a different $LOAD_PATH entry
64
73
  if load_path_entry.nil? || !path.start_with?(load_path_entry)
65
74
  load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
66
75
  end
@@ -69,9 +78,19 @@ module RubyIndexer
69
78
  end
70
79
  end
71
80
 
81
+ # If the patterns are relative, we make it relative to the workspace path. If they are absolute, then we shouldn't
82
+ # concatenate anything
83
+ excluded_patterns = @excluded_patterns.map do |pattern|
84
+ if File.absolute_path?(pattern)
85
+ pattern
86
+ else
87
+ File.join(@workspace_path, pattern)
88
+ end
89
+ end
90
+
72
91
  # Remove user specified patterns
73
92
  indexables.reject! do |indexable|
74
- @excluded_patterns.any? do |pattern|
93
+ excluded_patterns.any? do |pattern|
75
94
  File.fnmatch?(pattern, indexable.full_path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
76
95
  end
77
96
  end
@@ -122,7 +141,7 @@ module RubyIndexer
122
141
  # When working on a gem, it will be included in the locked_gems list. Since these are the project's own files,
123
142
  # we have already included and handled exclude patterns for it and should not re-include or it'll lead to
124
143
  # duplicates or accidentally ignoring exclude patterns
125
- next if spec.full_gem_path == Dir.pwd
144
+ next if spec.full_gem_path == @workspace_path
126
145
 
127
146
  indexables.concat(
128
147
  spec.require_paths.flat_map do |require_path|
@@ -185,7 +204,7 @@ module RubyIndexer
185
204
  # If the dependency is prerelease, `to_spec` may return `nil` due to a bug in older version of Bundler/RubyGems:
186
205
  # https://github.com/Shopify/ruby-lsp/issues/1246
187
206
  this_gem = Bundler.definition.dependencies.find do |d|
188
- d.to_spec&.full_gem_path == Dir.pwd
207
+ d.to_spec&.full_gem_path == @workspace_path
189
208
  rescue Gem::MissingSpecError
190
209
  false
191
210
  end
@@ -152,16 +152,17 @@ module RubyIndexer
152
152
 
153
153
  if current_owner
154
154
  expression = node.expression
155
- @stack << (expression.is_a?(Prism::SelfNode) ? "<Class:#{@stack.last}>" : "<Class:#{expression.slice}>")
155
+ name = (expression.is_a?(Prism::SelfNode) ? "<Class:#{@stack.last}>" : "<Class:#{expression.slice}>")
156
+ real_nesting = actual_nesting(name)
156
157
 
157
- existing_entries = T.cast(@index[@stack.join("::")], T.nilable(T::Array[Entry::SingletonClass]))
158
+ existing_entries = T.cast(@index[real_nesting.join("::")], T.nilable(T::Array[Entry::SingletonClass]))
158
159
 
159
160
  if existing_entries
160
161
  entry = T.must(existing_entries.first)
161
162
  entry.update_singleton_information(node.location, expression.location, collect_comments(node))
162
163
  else
163
164
  entry = Entry::SingletonClass.new(
164
- @stack,
165
+ real_nesting,
165
166
  @file_path,
166
167
  node.location,
167
168
  expression.location,
@@ -172,6 +173,7 @@ module RubyIndexer
172
173
  end
173
174
 
174
175
  @owner_stack << entry
176
+ @stack << name
175
177
  end
176
178
  end
177
179
 
@@ -564,6 +564,27 @@ module RubyIndexer
564
564
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
565
565
  end
566
566
 
567
+ def test_indexing_singletons_inside_top_level_references
568
+ index(<<~RUBY)
569
+ module ::Foo
570
+ class Bar
571
+ class << self
572
+ end
573
+ end
574
+ end
575
+ RUBY
576
+
577
+ # We want to explicitly verify that we didn't introduce the leading `::` by accident, but `Index#[]` deletes the
578
+ # prefix when we use `refute_entry`
579
+ entries = @index.instance_variable_get(:@entries)
580
+ refute(entries.key?("::Foo"))
581
+ refute(entries.key?("::Foo::Bar"))
582
+ refute(entries.key?("::Foo::Bar::<Class:Bar>"))
583
+ assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:5-3")
584
+ assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:4-5")
585
+ assert_entry("Foo::Bar::<Class:Bar>", Entry::SingletonClass, "/fake/path/foo.rb:2-4:3-7")
586
+ end
587
+
567
588
  def test_indexing_namespaces_inside_nested_top_level_references
568
589
  index(<<~RUBY)
569
590
  class Baz
@@ -7,10 +7,12 @@ module RubyIndexer
7
7
  class ConfigurationTest < Minitest::Test
8
8
  def setup
9
9
  @config = Configuration.new
10
+ @workspace_path = File.expand_path(File.join("..", "..", ".."), __dir__)
11
+ @config.workspace_path = @workspace_path
10
12
  end
11
13
 
12
14
  def test_load_configuration_executes_configure_block
13
- @config.apply_config({ "excluded_patterns" => ["**/test/fixtures/**/*.rb"] })
15
+ @config.apply_config({ "excluded_patterns" => ["**/fixtures/**/*.rb"] })
14
16
  indexables = @config.indexables
15
17
 
16
18
  assert(indexables.none? { |indexable| indexable.full_path.include?("test/fixtures") })
@@ -25,7 +27,7 @@ module RubyIndexer
25
27
  indexables = @config.indexables
26
28
 
27
29
  # All paths should be expanded
28
- assert(indexables.none? { |indexable| indexable.full_path.start_with?("lib/") })
30
+ assert(indexables.all? { |indexable| File.absolute_path?(indexable.full_path) })
29
31
  end
30
32
 
31
33
  def test_indexables_only_includes_gem_require_paths
@@ -71,17 +73,20 @@ module RubyIndexer
71
73
  Bundler.settings.temporary(path: "vendor/bundle") do
72
74
  config = Configuration.new
73
75
 
74
- assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
76
+ assert_includes(config.instance_variable_get(:@excluded_patterns), "vendor/bundle/**/*.rb")
75
77
  end
76
78
  end
77
79
 
78
80
  def test_indexables_does_not_include_gems_own_installed_files
79
81
  indexables = @config.indexables
82
+ indexables_inside_bundled_lsp = indexables.select do |indexable|
83
+ indexable.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s)
84
+ end
80
85
 
81
- assert(
82
- indexables.none? do |indexable|
83
- indexable.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s)
84
- end,
86
+ assert_empty(
87
+ indexables_inside_bundled_lsp,
88
+ "Indexables should not include files from the gem currently being worked on. " \
89
+ "Included: #{indexables_inside_bundled_lsp.map(&:full_path)}",
85
90
  )
86
91
  end
87
92
 
@@ -126,5 +131,34 @@ module RubyIndexer
126
131
  assert_match(regex, comment)
127
132
  end
128
133
  end
134
+
135
+ def test_indexables_respect_given_workspace_path
136
+ Dir.mktmpdir do |dir|
137
+ FileUtils.mkdir(File.join(dir, "ignore"))
138
+ FileUtils.touch(File.join(dir, "ignore", "file0.rb"))
139
+ FileUtils.touch(File.join(dir, "file1.rb"))
140
+ FileUtils.touch(File.join(dir, "file2.rb"))
141
+
142
+ @config.apply_config({ "excluded_patterns" => ["ignore/**/*.rb"] })
143
+ @config.workspace_path = dir
144
+ indexables = @config.indexables
145
+
146
+ assert(indexables.none? { |indexable| indexable.full_path.start_with?(File.join(dir, "ignore")) })
147
+
148
+ # After switching the workspace path, all indexables will be found in one of these places:
149
+ # - The new workspace path
150
+ # - The Ruby LSP's own code (because Bundler is requiring the dependency from source)
151
+ # - Bundled gems
152
+ # - Default gems
153
+ assert(
154
+ indexables.all? do |i|
155
+ i.full_path.start_with?(dir) ||
156
+ i.full_path.start_with?(File.join(Dir.pwd, "lib")) ||
157
+ i.full_path.start_with?(Bundler.bundle_path.to_s) ||
158
+ i.full_path.start_with?(RbConfig::CONFIG["rubylibdir"])
159
+ end,
160
+ )
161
+ end
162
+ end
129
163
  end
130
164
  end
@@ -208,10 +208,13 @@ module RubyLsp
208
208
  def handle_method_definition(message, receiver_type, inherited_only: false)
209
209
  methods = if receiver_type
210
210
  @index.resolve_method(message, receiver_type.name, inherited_only: inherited_only)
211
- else
212
- # If the method doesn't have a receiver, then we provide a few candidates to jump to
213
- # But we don't want to provide too many candidates, as it can be overwhelming
214
- @index[message]&.take(MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER)
211
+ end
212
+
213
+ # If the method doesn't have a receiver, or the guessed receiver doesn't have any matched candidates,
214
+ # then we provide a few candidates to jump to
215
+ # But we don't want to provide too many candidates, as it can be overwhelming
216
+ if receiver_type.nil? || (receiver_type.is_a?(TypeInferrer::GuessedType) && methods.nil?)
217
+ methods = @index[message]&.take(MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER)
215
218
  end
216
219
 
217
220
  return unless methods
@@ -250,7 +253,7 @@ module RubyLsp
250
253
  when :require_relative
251
254
  required_file = "#{node.content}.rb"
252
255
  path = @uri.to_standardized_path
253
- current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
256
+ current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : @global_state.workspace_path
254
257
  candidate = File.expand_path(File.join(current_folder, required_file))
255
258
 
256
259
  @response_builder << Interface::Location.new(
@@ -879,7 +879,7 @@ module RubyLsp
879
879
  # Indexing produces a high number of short lived object allocations. That might lead to some fragmentation and
880
880
  # an unnecessarily expanded heap. Compacting ensures that the heap is as small as possible and that future
881
881
  # allocations and garbage collections are faster
882
- GC.compact
882
+ GC.compact unless @test_mode
883
883
 
884
884
  # Always end the progress notification even if indexing failed or else it never goes away and the user has no
885
885
  # way of dismissing it
@@ -1000,10 +1000,10 @@ module RubyLsp
1000
1000
 
1001
1001
  return unless indexing_options
1002
1002
 
1003
+ configuration = @global_state.index.configuration
1004
+ configuration.workspace_path = @global_state.workspace_path
1003
1005
  # The index expects snake case configurations, but VS Code standardizes on camel case settings
1004
- @global_state.index.configuration.apply_config(
1005
- indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase },
1006
- )
1006
+ configuration.apply_config(indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase })
1007
1007
  end
1008
1008
  end
1009
1009
  end
@@ -43,7 +43,7 @@ module RubyLsp
43
43
  @gemfile_name = T.let(@gemfile&.basename&.to_s || "Gemfile", String)
44
44
 
45
45
  # Custom bundle paths
46
- @custom_dir = T.let(Pathname.new(".ruby-lsp").expand_path(Dir.pwd), Pathname)
46
+ @custom_dir = T.let(Pathname.new(".ruby-lsp").expand_path(@project_path), Pathname)
47
47
  @custom_gemfile = T.let(@custom_dir + @gemfile_name, Pathname)
48
48
  @custom_lockfile = T.let(@custom_dir + (@lockfile&.basename || "Gemfile.lock"), Pathname)
49
49
  @lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
@@ -183,14 +183,14 @@ module RubyLsp
183
183
  # `.ruby-lsp` folder, which is not the user's intention. For example, if the path is configured as `vendor`, we
184
184
  # want to install it in the top level `vendor` and not `.ruby-lsp/vendor`
185
185
  path = Bundler.settings["path"]
186
- expanded_path = File.expand_path(path, Dir.pwd) if path
186
+ expanded_path = File.expand_path(path, @project_path) if path
187
187
 
188
188
  # Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
189
189
  env = {}
190
190
  env["BUNDLE_GEMFILE"] = bundle_gemfile.to_s
191
191
  env["BUNDLE_PATH"] = expanded_path if expanded_path
192
192
 
193
- local_config_path = File.join(Dir.pwd, ".bundle")
193
+ local_config_path = File.join(@project_path, ".bundle")
194
194
  env["BUNDLE_APP_CONFIG"] = local_config_path if Dir.exist?(local_config_path)
195
195
 
196
196
  # If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
@@ -286,7 +286,7 @@ module RubyLsp
286
286
  sig { returns(T::Boolean) }
287
287
  def rails_app?
288
288
  config = Pathname.new("config/application.rb").expand_path
289
- application_contents = config.read if config.exist?
289
+ application_contents = config.read(external_encoding: Encoding::UTF_8) if config.exist?
290
290
  return false unless application_contents
291
291
 
292
292
  /class .* < (::)?Rails::Application/.match?(application_contents)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.14
4
+ version: 0.17.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-19 00:00:00.000000000 Z
11
+ date: 2024-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol