ruby-lsp 0.20.0 → 0.20.1

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: d43d431cf15d817d80ebb8564f90a748b871ad3c2c278482d4b1b5c831b7bcd2
4
- data.tar.gz: 28ab582396723d582f7909ad277300430b6e14e6a281408be21a71f4d768930f
3
+ metadata.gz: 58327a9f9a3d85375cbbf81e3e170a948d5c8dfbdcdcac0a1fdef5f685d20c95
4
+ data.tar.gz: 45572eb6645bce73d2079bed65f0d92c98921517926b968bba96788769a3aada
5
5
  SHA512:
6
- metadata.gz: 73a87212da50592a6b201a5f21ea559de74fb12ed795428e06392a4375aa73015a103aedd56734b9b05a2f0a27fcb7366b16107ea9f1a080b3459e2eff1dcd88
7
- data.tar.gz: 7ad77f3dccbabba9cb306c73ccefca4e84e9ff480bc0594d20a1c6e03727948119f7accefc522e5ed25d6de1d21fff03b18c704c7c8908b33eb92cfc5b2c665d
6
+ metadata.gz: c506eeff4d24060e3afdb4ad9da319c12b725050c8f25a9348ec7dea9082f8adced7671b7b1eb8f7f09412942139e47a2df5059b5672d2b6a75f0ffc5a59885d
7
+ data.tar.gz: d9a89ce24946e5f9cc47b5a48086f6fd977176d31332a0b634950dc00a41fc9a36e4753eae2896e2da0129e92bff4278265eaa970252a5292c783e5bc82258e9
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.20.0
1
+ 0.20.1
data/exe/ruby-lsp CHANGED
@@ -63,7 +63,13 @@ if ENV["BUNDLE_GEMFILE"].nil?
63
63
  exit(78)
64
64
  end
65
65
 
66
- exit exec(env, "bundle exec ruby-lsp #{original_args.join(" ")}")
66
+ base_bundle = if env["BUNDLER_VERSION"]
67
+ "bundle _#{env["BUNDLER_VERSION"]}_"
68
+ else
69
+ "bundle"
70
+ end
71
+
72
+ exit exec(env, "#{base_bundle} exec ruby-lsp #{original_args.join(" ")}")
67
73
  end
68
74
 
69
75
  require "ruby_lsp/load_sorbet"
@@ -28,7 +28,17 @@ module RubyIndexer
28
28
  @encoding = T.let(Encoding::UTF_8, Encoding)
29
29
  @excluded_gems = T.let(initial_excluded_gems, T::Array[String])
30
30
  @included_gems = T.let([], T::Array[String])
31
- @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("tmp", "**", "*")], T::Array[String])
31
+
32
+ @excluded_patterns = T.let(
33
+ [
34
+ File.join("**", "*_test.rb"),
35
+ File.join("node_modules", "**", "*"),
36
+ File.join("spec", "**", "*"),
37
+ File.join("test", "**", "*"),
38
+ File.join("tmp", "**", "*"),
39
+ ],
40
+ T::Array[String],
41
+ )
32
42
 
33
43
  path = Bundler.settings["path"]
34
44
  if path
@@ -56,6 +66,21 @@ module RubyIndexer
56
66
  )
57
67
  end
58
68
 
69
+ sig { returns(String) }
70
+ def merged_excluded_file_pattern
71
+ # This regex looks for @excluded_patterns that follow the format of "something/**/*", where
72
+ # "something" is one or more non-"/"
73
+ #
74
+ # Returns "/path/to/workspace/{tmp,node_modules}/**/*"
75
+ @excluded_patterns
76
+ .filter_map do |pattern|
77
+ next if File.absolute_path?(pattern)
78
+
79
+ pattern.match(%r{\A([^/]+)/\*\*/\*\z})&.captures&.first
80
+ end
81
+ .then { |dirs| File.join(@workspace_path, "{#{dirs.join(",")}}/**/*") }
82
+ end
83
+
59
84
  sig { returns(T::Array[IndexablePath]) }
60
85
  def indexables
61
86
  excluded_gems = @excluded_gems - @included_gems
@@ -64,21 +89,47 @@ module RubyIndexer
64
89
  # NOTE: indexing the patterns (both included and excluded) needs to happen before indexing gems, otherwise we risk
65
90
  # having duplicates if BUNDLE_PATH is set to a folder inside the project structure
66
91
 
92
+ flags = File::FNM_PATHNAME | File::FNM_EXTGLOB
93
+
94
+ # In order to speed up indexing, only traverse into top-level directories that are not entirely excluded.
95
+ # For example, if "tmp/**/*" is excluded, we don't need to traverse into "tmp" at all. However, if
96
+ # "vendor/bundle/**/*" is excluded, we will traverse all of "vendor" and `reject!` out all "vendor/bundle" entries
97
+ # later.
98
+ excluded_pattern = merged_excluded_file_pattern
99
+ included_paths = Dir.glob(File.join(@workspace_path, "*/"), flags)
100
+ .filter_map do |included_path|
101
+ next if File.fnmatch?(excluded_pattern, included_path, flags)
102
+
103
+ relative_path = included_path
104
+ .delete_prefix(@workspace_path)
105
+ .tap { |path| path.delete_prefix!("/") }
106
+
107
+ [included_path, relative_path]
108
+ end
109
+
110
+ indexables = T.let([], T::Array[IndexablePath])
111
+
67
112
  # Add user specified patterns
68
- indexables = @included_patterns.flat_map do |pattern|
113
+ @included_patterns.each do |pattern|
69
114
  load_path_entry = T.let(nil, T.nilable(String))
70
115
 
71
- Dir.glob(File.join(@workspace_path, pattern), File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
72
- path = File.expand_path(path)
73
- # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
74
- # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens
75
- # on repositories that define multiple gems, like Rails. All frameworks are defined inside the current
76
- # workspace directory, but each one of them belongs to a different $LOAD_PATH entry
77
- if load_path_entry.nil? || !path.start_with?(load_path_entry)
78
- load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
79
- end
116
+ included_paths.each do |included_path, relative_path|
117
+ relative_pattern = pattern.delete_prefix(File.join(relative_path, "/"))
118
+
119
+ next unless pattern.start_with?("**") || pattern.start_with?(relative_path)
80
120
 
81
- IndexablePath.new(load_path_entry, path)
121
+ Dir.glob(File.join(included_path, relative_pattern), flags).each do |path|
122
+ path = File.expand_path(path)
123
+ # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
124
+ # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This
125
+ # happens on repositories that define multiple gems, like Rails. All frameworks are defined inside the
126
+ # current workspace directory, but each one of them belongs to a different $LOAD_PATH entry
127
+ if load_path_entry.nil? || !path.start_with?(load_path_entry)
128
+ load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
129
+ end
130
+
131
+ indexables << IndexablePath.new(load_path_entry, path)
132
+ end
82
133
  end
83
134
  end
84
135
 
@@ -980,29 +980,23 @@ module RubyIndexer
980
980
  # nesting
981
981
  sig { params(name: String, nesting: T::Array[String]).returns(String) }
982
982
  def build_non_redundant_full_name(name, nesting)
983
+ # If there's no nesting, then we can just return the name as is
983
984
  return name if nesting.empty?
984
985
 
985
- namespace = nesting.join("::")
986
-
987
986
  # If the name is not qualified, we can just concatenate the nesting and the name
988
- return "#{namespace}::#{name}" unless name.include?("::")
987
+ return "#{nesting.join("::")}::#{name}" unless name.include?("::")
989
988
 
990
989
  name_parts = name.split("::")
990
+ first_redundant_part = nesting.index(name_parts[0])
991
991
 
992
- # Find the first part of the name that is not in the nesting
993
- index = name_parts.index { |part| !nesting.include?(part) }
992
+ # If there are no redundant parts between the name and the nesting, then the full name is both combined
993
+ return "#{nesting.join("::")}::#{name}" unless first_redundant_part
994
994
 
995
- if index.nil?
996
- # All parts of the nesting are redundant because they are already present in the name. We can return the name
997
- # directly
998
- name
999
- elsif index == 0
1000
- # No parts of the nesting are in the name, we can concatenate the namespace and the name
1001
- "#{namespace}::#{name}"
1002
- else
1003
- # The name includes some parts of the nesting. We need to remove the redundant parts
1004
- "#{namespace}::#{T.must(name_parts[index..-1]).join("::")}"
1005
- end
995
+ # Otherwise, push all of the leading parts of the nesting that aren't redundant into the name. For example, if we
996
+ # have a reference to `Foo::Bar` inside the `[Namespace, Foo]` nesting, then only the `Foo` part is redundant, but
997
+ # we still need to include the `Namespace` part
998
+ T.unsafe(name_parts).unshift(*nesting[0...first_redundant_part])
999
+ name_parts.join("::")
1006
1000
  end
1007
1001
 
1008
1002
  sig do
@@ -1934,5 +1934,94 @@ module RubyIndexer
1934
1934
  real_namespace = @index.follow_aliased_namespace("Namespace::Second")
1935
1935
  assert_equal("First::Second", real_namespace)
1936
1936
  end
1937
+
1938
+ def test_resolving_alias_to_non_existing_namespace
1939
+ index(<<~RUBY)
1940
+ module Namespace
1941
+ class Foo
1942
+ module InnerNamespace
1943
+ Constants = Namespace::Foo::Constants
1944
+ end
1945
+ end
1946
+ end
1947
+ RUBY
1948
+
1949
+ entry = @index.resolve("Constants", ["Namespace", "Foo", "InnerNamespace"])&.first
1950
+ assert_instance_of(Entry::UnresolvedConstantAlias, entry)
1951
+
1952
+ entry = @index.resolve("Namespace::Foo::Constants", ["Namespace", "Foo", "InnerNamespace"])&.first
1953
+ assert_nil(entry)
1954
+ end
1955
+
1956
+ def test_resolving_alias_to_existing_constant_from_inner_namespace
1957
+ index(<<~RUBY)
1958
+ module Parent
1959
+ CONST = 123
1960
+ end
1961
+
1962
+ module First
1963
+ module Namespace
1964
+ class Foo
1965
+ include Parent
1966
+
1967
+ module InnerNamespace
1968
+ Constants = Namespace::Foo::CONST
1969
+ end
1970
+ end
1971
+ end
1972
+ end
1973
+ RUBY
1974
+
1975
+ entry = @index.resolve("Namespace::Foo::CONST", ["First", "Namespace", "Foo", "InnerNamespace"])&.first
1976
+ assert_equal("Parent::CONST", entry.name)
1977
+ assert_instance_of(Entry::Constant, entry)
1978
+ end
1979
+
1980
+ def test_build_non_redundant_name
1981
+ assert_equal(
1982
+ "Namespace::Foo::Constants",
1983
+ @index.send(
1984
+ :build_non_redundant_full_name,
1985
+ "Namespace::Foo::Constants",
1986
+ ["Namespace", "Foo", "InnerNamespace"],
1987
+ ),
1988
+ )
1989
+
1990
+ assert_equal(
1991
+ "Namespace::Foo::Constants",
1992
+ @index.send(
1993
+ :build_non_redundant_full_name,
1994
+ "Namespace::Foo::Constants",
1995
+ ["Namespace", "Foo"],
1996
+ ),
1997
+ )
1998
+
1999
+ assert_equal(
2000
+ "Namespace::Foo::Constants",
2001
+ @index.send(
2002
+ :build_non_redundant_full_name,
2003
+ "Foo::Constants",
2004
+ ["Namespace", "Foo"],
2005
+ ),
2006
+ )
2007
+
2008
+ assert_equal(
2009
+ "Bar::Namespace::Foo::Constants",
2010
+ @index.send(
2011
+ :build_non_redundant_full_name,
2012
+ "Namespace::Foo::Constants",
2013
+ ["Bar"],
2014
+ ),
2015
+ )
2016
+
2017
+ assert_equal(
2018
+ "First::Namespace::Foo::Constants",
2019
+ @index.send(
2020
+ :build_non_redundant_full_name,
2021
+ "Namespace::Foo::Constants",
2022
+ ["First", "Namespace", "Foo", "InnerNamespace"],
2023
+ ),
2024
+ )
2025
+ end
1937
2026
  end
1938
2027
  end
@@ -78,6 +78,8 @@ module RubyLsp
78
78
 
79
79
  parse_result = Prism.parse_file(path)
80
80
  collect_references(reference_target, parse_result, uri)
81
+ rescue Errno::EISDIR, Errno::ENOENT
82
+ # If `path` is a directory, just ignore it and continue. If the file doesn't exist, then we also ignore it.
81
83
  end
82
84
 
83
85
  @store.each do |_uri, document|
@@ -147,6 +147,8 @@ module RubyLsp
147
147
  parse_result = Prism.parse_file(path)
148
148
  edits = collect_changes(target, parse_result, name, uri)
149
149
  changes[uri.to_s] = edits unless edits.empty?
150
+ rescue Errno::EISDIR, Errno::ENOENT
151
+ # If `path` is a directory, just ignore it and continue. If the file doesn't exist, then we also ignore it.
150
152
  end
151
153
 
152
154
  @store.each do |uri, document|
@@ -48,7 +48,9 @@ module RubyLsp
48
48
  @lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
49
49
  @last_updated_path = T.let(@custom_dir + "last_updated", Pathname)
50
50
 
51
- @dependencies = T.let(load_dependencies, T::Hash[String, T.untyped])
51
+ dependencies, bundler_version = load_dependencies
52
+ @dependencies = T.let(dependencies, T::Hash[String, T.untyped])
53
+ @bundler_version = T.let(bundler_version, T.nilable(Gem::Version))
52
54
  @rails_app = T.let(rails_app?, T::Boolean)
53
55
  @retry = T.let(false, T::Boolean)
54
56
  end
@@ -156,14 +158,15 @@ module RubyLsp
156
158
  @custom_gemfile.write(content) unless @custom_gemfile.exist? && @custom_gemfile.read == content
157
159
  end
158
160
 
159
- sig { returns(T::Hash[String, T.untyped]) }
161
+ sig { returns([T::Hash[String, T.untyped], T.nilable(Gem::Version)]) }
160
162
  def load_dependencies
161
- return {} unless @lockfile&.exist?
163
+ return [{}, nil] unless @lockfile&.exist?
162
164
 
163
165
  # We need to parse the Gemfile.lock manually here. If we try to do `bundler/setup` to use something more
164
166
  # convenient, we may end up with issues when the globally installed `ruby-lsp` version mismatches the one included
165
167
  # in the `Gemfile`
166
- dependencies = Bundler::LockfileParser.new(@lockfile.read).dependencies
168
+ lockfile_parser = Bundler::LockfileParser.new(@lockfile.read)
169
+ dependencies = lockfile_parser.dependencies
167
170
 
168
171
  # When working on a gem, the `ruby-lsp` might be listed as a dependency in the gemspec. We need to make sure we
169
172
  # check those as well or else we may get version mismatch errors. Notice that bundler allows more than one
@@ -172,7 +175,7 @@ module RubyLsp
172
175
  dependencies.merge!(Bundler.load_gemspec(path).dependencies.to_h { |dep| [dep.name, dep] })
173
176
  end
174
177
 
175
- dependencies
178
+ [dependencies, lockfile_parser.bundler_version]
176
179
  end
177
180
 
178
181
  sig { params(bundle_gemfile: T.nilable(Pathname)).returns(T::Hash[String, String]) }
@@ -188,6 +191,16 @@ module RubyLsp
188
191
  env["BUNDLE_PATH"] = File.expand_path(env["BUNDLE_PATH"], @project_path)
189
192
  end
190
193
 
194
+ # If there's a Bundler version locked, then we need to use that one to run bundle commands, so that the composed
195
+ # lockfile is also locked to the same version. This avoids Bundler restarts on version mismatches
196
+ base_bundle = if @bundler_version
197
+ env["BUNDLER_VERSION"] = @bundler_version.to_s
198
+ install_bundler_if_needed
199
+ "bundle _#{@bundler_version}_"
200
+ else
201
+ "bundle"
202
+ end
203
+
191
204
  # If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
192
205
  # to upgrade them or else we'll produce undesired source control changes. If the custom bundle was just created
193
206
  # and any of `ruby-lsp`, `ruby-lsp-rails` or `debug` weren't a part of the Gemfile, then we need to run `bundle
@@ -196,13 +209,13 @@ module RubyLsp
196
209
 
197
210
  # When not updating, we run `(bundle check || bundle install)`
198
211
  # When updating, we run `((bundle check && bundle update ruby-lsp debug) || bundle install)`
199
- command = +"(bundle check"
212
+ command = +"(#{base_bundle} check"
200
213
 
201
214
  if should_bundle_update?
202
215
  # If any of `ruby-lsp`, `ruby-lsp-rails` or `debug` are not in the Gemfile, try to update them to the latest
203
216
  # version
204
217
  command.prepend("(")
205
- command << " && bundle update "
218
+ command << " && #{base_bundle} update "
206
219
  command << "ruby-lsp " unless @dependencies["ruby-lsp"]
207
220
  command << "debug " unless @dependencies["debug"]
208
221
  command << "ruby-lsp-rails " if @rails_app && !@dependencies["ruby-lsp-rails"]
@@ -212,7 +225,7 @@ module RubyLsp
212
225
  @last_updated_path.write(Time.now.iso8601)
213
226
  end
214
227
 
215
- command << " || bundle install) "
228
+ command << " || #{base_bundle} install) "
216
229
 
217
230
  # Redirect stdout to stderr to prevent going into an infinite loop. The extension might confuse stdout output with
218
231
  # responses
@@ -259,6 +272,15 @@ module RubyLsp
259
272
  end
260
273
  end
261
274
 
275
+ sig { void }
276
+ def install_bundler_if_needed
277
+ # Try to find the bundler version specified in the lockfile in installed gems. If not found, install it
278
+ requirement = Gem::Requirement.new(@bundler_version.to_s)
279
+ return if Gem::Specification.any? { |s| s.name == "bundler" && requirement =~ s.version }
280
+
281
+ Gem.install("bundler", @bundler_version.to_s)
282
+ end
283
+
262
284
  sig { returns(T::Boolean) }
263
285
  def should_bundle_update?
264
286
  # If `ruby-lsp`, `ruby-lsp-rails` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it
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.20.0
4
+ version: 0.20.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-11 00:00:00.000000000 Z
11
+ date: 2024-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol