ruby-lsp 0.26.1 → 0.26.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +23 -5
- data/lib/ruby_indexer/test/index_test.rb +40 -0
- data/lib/ruby_lsp/base_server.rb +22 -13
- data/lib/ruby_lsp/listeners/document_highlight.rb +26 -1
- data/lib/ruby_lsp/listeners/document_link.rb +50 -18
- data/lib/ruby_lsp/listeners/spec_style.rb +32 -15
- data/lib/ruby_lsp/listeners/test_discovery.rb +20 -5
- data/lib/ruby_lsp/listeners/test_style.rb +10 -8
- data/lib/ruby_lsp/requests/go_to_relevant_file.rb +64 -10
- data/lib/ruby_lsp/requests/support/common.rb +8 -4
- data/lib/ruby_lsp/requests/support/package_url.rb +414 -0
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +10 -6
- data/lib/ruby_lsp/requests/workspace_symbol.rb +20 -12
- data/lib/ruby_lsp/server.rb +2 -2
- data/lib/ruby_lsp/setup_bundler.rb +18 -8
- data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +10 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90c12f93d4750216ef84e046fe970f0ccc8edd7567943b9f4be02dc3828fd151
|
|
4
|
+
data.tar.gz: ef810d7d1599f9539474e87067c5972ef2aa3a6143452494e9d03857d64771e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a6ff236ecaabfd5e927a6f57f47d97ad6822dc647ce3531c57565adfc05fd592167decbabfa2ac3420d8294f193188103c7aa3a468b5f92dca7b18a92bb1b87
|
|
7
|
+
data.tar.gz: e34f78b58b73536faecf0bafb0f7a620bbbc3122c8fb8d1b526112b000055b78fe6d153ff0bf1bbd2300aaaffb3f5a6327fd7ef4248332b39ebb5a7c13fe94d5
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.26.
|
|
1
|
+
0.26.3
|
|
@@ -195,12 +195,13 @@ module RubyIndexer
|
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
# Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
|
|
198
|
-
#: (String? query) -> Array[Entry]
|
|
199
|
-
def fuzzy_search(query)
|
|
198
|
+
#: (String? query) ?{ (Entry) -> bool? } -> Array[Entry]
|
|
199
|
+
def fuzzy_search(query, &condition)
|
|
200
200
|
unless query
|
|
201
201
|
entries = @entries.filter_map do |_name, entries|
|
|
202
202
|
next if entries.first.is_a?(Entry::SingletonClass)
|
|
203
203
|
|
|
204
|
+
entries = entries.select(&condition) if condition
|
|
204
205
|
entries
|
|
205
206
|
end
|
|
206
207
|
|
|
@@ -212,6 +213,9 @@ module RubyIndexer
|
|
|
212
213
|
results = @entries.filter_map do |name, entries|
|
|
213
214
|
next if entries.first.is_a?(Entry::SingletonClass)
|
|
214
215
|
|
|
216
|
+
entries = entries.select(&condition) if condition
|
|
217
|
+
next if entries.empty?
|
|
218
|
+
|
|
215
219
|
similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
|
|
216
220
|
[entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
|
|
217
221
|
end
|
|
@@ -424,7 +428,10 @@ module RubyIndexer
|
|
|
424
428
|
(parts.length - 1).downto(0) do |i|
|
|
425
429
|
current_name = parts[0..i] #: as !nil
|
|
426
430
|
.join("::")
|
|
427
|
-
|
|
431
|
+
|
|
432
|
+
entry = unless seen_names.include?(current_name)
|
|
433
|
+
@entries[current_name]&.first
|
|
434
|
+
end
|
|
428
435
|
|
|
429
436
|
case entry
|
|
430
437
|
when Entry::ConstantAlias
|
|
@@ -904,6 +911,9 @@ module RubyIndexer
|
|
|
904
911
|
target = resolve(entry.target, entry.nesting, seen_names)
|
|
905
912
|
return entry unless target
|
|
906
913
|
|
|
914
|
+
# Self referential alias can be unresolved we should bail out from resolving
|
|
915
|
+
return entry if target.first == entry
|
|
916
|
+
|
|
907
917
|
target_name = target.first #: as !nil
|
|
908
918
|
.name
|
|
909
919
|
resolved_alias = Entry::ConstantAlias.new(target_name, entry)
|
|
@@ -1023,11 +1033,19 @@ module RubyIndexer
|
|
|
1023
1033
|
name_parts.join("::")
|
|
1024
1034
|
end
|
|
1025
1035
|
|
|
1036
|
+
# Tries to return direct entry from index then non seen canonicalized alias or nil
|
|
1026
1037
|
#: (String full_name, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
|
|
1027
1038
|
def direct_or_aliased_constant(full_name, seen_names)
|
|
1028
|
-
entries = @entries[full_name]
|
|
1039
|
+
if (entries = @entries[full_name])
|
|
1040
|
+
return entries.map do |e|
|
|
1041
|
+
e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e
|
|
1042
|
+
end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])?
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
aliased = follow_aliased_namespace(full_name, seen_names)
|
|
1046
|
+
return if full_name == aliased || seen_names.include?(aliased)
|
|
1029
1047
|
|
|
1030
|
-
entries&.map do |e|
|
|
1048
|
+
@entries[aliased]&.map do |e|
|
|
1031
1049
|
e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e
|
|
1032
1050
|
end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])?
|
|
1033
1051
|
end
|
|
@@ -1152,6 +1152,46 @@ module RubyIndexer
|
|
|
1152
1152
|
assert_equal(2, foo_entry.location.start_line)
|
|
1153
1153
|
end
|
|
1154
1154
|
|
|
1155
|
+
def test_resolving_self_referential_constant_alias
|
|
1156
|
+
index(<<~RUBY)
|
|
1157
|
+
module A
|
|
1158
|
+
module B
|
|
1159
|
+
class C
|
|
1160
|
+
end
|
|
1161
|
+
end
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
module A
|
|
1165
|
+
module D
|
|
1166
|
+
B = B::C
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
1169
|
+
RUBY
|
|
1170
|
+
|
|
1171
|
+
entry = @index.resolve("A::D::B", [])&.first #: as Entry::ConstantAlias
|
|
1172
|
+
|
|
1173
|
+
assert_kind_of(RubyIndexer::Entry::ConstantAlias, entry)
|
|
1174
|
+
assert_equal(10, entry.location.start_line)
|
|
1175
|
+
assert_equal("A::B::C", entry.target)
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
def test_resolving_non_existing_self_referential_constant_alias
|
|
1179
|
+
index(<<~RUBY)
|
|
1180
|
+
module Foo
|
|
1181
|
+
SomeClass = ::SomeClass
|
|
1182
|
+
UNRESOLVED = SomeClass::CONSTANT
|
|
1183
|
+
end
|
|
1184
|
+
RUBY
|
|
1185
|
+
|
|
1186
|
+
entry = @index.resolve("Foo::UNRESOLVED", [])&.first #: as Entry::UnresolvedConstantAlias
|
|
1187
|
+
assert_kind_of(Entry::UnresolvedConstantAlias, entry)
|
|
1188
|
+
assert_equal(3, entry.location.start_line)
|
|
1189
|
+
assert_equal("SomeClass::CONSTANT", entry.target)
|
|
1190
|
+
|
|
1191
|
+
entry = @index.resolve("SomeClass::CONSTANT", ["Foo"])
|
|
1192
|
+
refute(entry)
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1155
1195
|
def test_resolving_qualified_references
|
|
1156
1196
|
index(<<~RUBY)
|
|
1157
1197
|
module Namespace
|
data/lib/ruby_lsp/base_server.rb
CHANGED
|
@@ -83,7 +83,7 @@ module RubyLsp
|
|
|
83
83
|
# The following requests need to be executed in the main thread directly to avoid concurrency issues. Everything
|
|
84
84
|
# else is pushed into the incoming queue
|
|
85
85
|
case method
|
|
86
|
-
when "initialize", "initialized", "rubyLsp/diagnoseState"
|
|
86
|
+
when "initialize", "initialized", "rubyLsp/diagnoseState", "$/cancelRequest"
|
|
87
87
|
process_message(message)
|
|
88
88
|
when "shutdown"
|
|
89
89
|
@global_state.synchronize do
|
|
@@ -103,7 +103,7 @@ module RubyLsp
|
|
|
103
103
|
# This method is only intended to be used in tests! Pops the latest response that would be sent to the client
|
|
104
104
|
#: -> untyped
|
|
105
105
|
def pop_response
|
|
106
|
-
@outgoing_queue.pop
|
|
106
|
+
@outgoing_queue.pop(timeout: 20) || raise("No message received from server")
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# This method is only intended to be used in tests! Pushes a message to the incoming queue directly
|
|
@@ -154,20 +154,29 @@ module RubyLsp
|
|
|
154
154
|
def new_worker
|
|
155
155
|
Thread.new do
|
|
156
156
|
while (message = @incoming_queue.pop)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if id && @cancelled_requests.include?(id)
|
|
162
|
-
send_message(Result.new(id: id, response: nil))
|
|
163
|
-
@cancelled_requests.delete(id)
|
|
164
|
-
next
|
|
165
|
-
end
|
|
166
|
-
end
|
|
157
|
+
handle_incoming_message(message)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
167
161
|
|
|
168
|
-
|
|
162
|
+
#: (Hash[Symbol, untyped]) -> void
|
|
163
|
+
def handle_incoming_message(message)
|
|
164
|
+
id = message[:id]
|
|
165
|
+
|
|
166
|
+
# Check if the request was cancelled before trying to process it
|
|
167
|
+
@global_state.synchronize do
|
|
168
|
+
if id && @cancelled_requests.include?(id)
|
|
169
|
+
send_message(Error.new(
|
|
170
|
+
id: id,
|
|
171
|
+
code: Constant::ErrorCodes::REQUEST_CANCELLED,
|
|
172
|
+
message: "Request #{id} was cancelled",
|
|
173
|
+
))
|
|
174
|
+
@cancelled_requests.delete(id)
|
|
175
|
+
return
|
|
169
176
|
end
|
|
170
177
|
end
|
|
178
|
+
|
|
179
|
+
process_message(message)
|
|
171
180
|
end
|
|
172
181
|
|
|
173
182
|
#: ((Result | Error | Notification | Request) message) -> void
|
|
@@ -92,7 +92,8 @@ module RubyLsp
|
|
|
92
92
|
Prism::RequiredParameterNode, Prism::RestParameterNode
|
|
93
93
|
[target, node_value(target)]
|
|
94
94
|
when Prism::ModuleNode, Prism::ClassNode, Prism::SingletonClassNode, Prism::DefNode, Prism::CaseNode,
|
|
95
|
-
Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::IfNode, Prism::UnlessNode
|
|
95
|
+
Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::IfNode, Prism::UnlessNode, Prism::BlockNode,
|
|
96
|
+
Prism::LambdaNode, Prism::BeginNode
|
|
96
97
|
[target, nil]
|
|
97
98
|
end
|
|
98
99
|
|
|
@@ -157,6 +158,9 @@ module RubyLsp
|
|
|
157
158
|
:on_for_node_enter,
|
|
158
159
|
:on_if_node_enter,
|
|
159
160
|
:on_unless_node_enter,
|
|
161
|
+
:on_block_node_enter,
|
|
162
|
+
:on_lambda_node_enter,
|
|
163
|
+
:on_begin_node_enter,
|
|
160
164
|
)
|
|
161
165
|
end
|
|
162
166
|
end
|
|
@@ -551,6 +555,27 @@ module RubyLsp
|
|
|
551
555
|
add_matching_end_highlights(node.keyword_loc, node.end_keyword_loc)
|
|
552
556
|
end
|
|
553
557
|
|
|
558
|
+
#: (Prism::BlockNode node) -> void
|
|
559
|
+
def on_block_node_enter(node)
|
|
560
|
+
return unless @target.is_a?(Prism::BlockNode)
|
|
561
|
+
|
|
562
|
+
add_matching_end_highlights(node.opening_loc, node.closing_loc)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
#: (Prism::LambdaNode node) -> void
|
|
566
|
+
def on_lambda_node_enter(node)
|
|
567
|
+
return unless @target.is_a?(Prism::LambdaNode)
|
|
568
|
+
|
|
569
|
+
add_matching_end_highlights(node.opening_loc, node.closing_loc)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
#: (Prism::BeginNode node) -> void
|
|
573
|
+
def on_begin_node_enter(node)
|
|
574
|
+
return unless @target.is_a?(Prism::BeginNode)
|
|
575
|
+
|
|
576
|
+
add_matching_end_highlights(node.begin_keyword_loc, node.end_keyword_loc)
|
|
577
|
+
end
|
|
578
|
+
|
|
554
579
|
private
|
|
555
580
|
|
|
556
581
|
#: (Prism::Node node, Array[singleton(Prism::Node)] classes) -> bool?
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "ruby_lsp/requests/support/source_uri"
|
|
5
|
+
require "ruby_lsp/requests/support/package_url"
|
|
5
6
|
|
|
6
7
|
module RubyLsp
|
|
7
8
|
module Listeners
|
|
@@ -102,19 +103,58 @@ module RubyLsp
|
|
|
102
103
|
comment = @lines_to_comments[node.location.start_line - 1]
|
|
103
104
|
return unless comment
|
|
104
105
|
|
|
105
|
-
match = comment.location.slice.match(%r{source://.*#\d
|
|
106
|
+
match = comment.location.slice.match(%r{(source://.*#\d+|pkg:gem/.*#.*)$})
|
|
106
107
|
return unless match
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
109
|
+
uri_string = match[0] #: as !nil
|
|
110
|
+
|
|
111
|
+
file_path, line_number = if uri_string.start_with?("pkg:gem/")
|
|
112
|
+
parse_package_url(uri_string)
|
|
113
|
+
else
|
|
114
|
+
parse_source_uri(uri_string)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
return unless file_path
|
|
118
|
+
|
|
119
|
+
@response_builder << Interface::DocumentLink.new(
|
|
120
|
+
range: range_from_location(comment.location),
|
|
121
|
+
target: "file://#{file_path}##{line_number}",
|
|
122
|
+
tooltip: "Jump to #{file_path}##{line_number}",
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
#: (String uri_string) -> [String, String]?
|
|
127
|
+
def parse_package_url(uri_string)
|
|
128
|
+
purl = PackageURL.parse(uri_string) #: as PackageURL?
|
|
129
|
+
return unless purl
|
|
130
|
+
|
|
131
|
+
gem_version = resolve_version(purl.version, purl.name)
|
|
132
|
+
return if gem_version.nil?
|
|
133
|
+
|
|
134
|
+
path, line_number = purl.subpath.split(":", 2)
|
|
135
|
+
return unless path
|
|
136
|
+
|
|
137
|
+
gem_name = purl.name
|
|
138
|
+
return unless gem_name
|
|
139
|
+
|
|
140
|
+
file_path = self.class.gem_paths.dig(gem_name, gem_version, CGI.unescape(path))
|
|
141
|
+
return if file_path.nil?
|
|
142
|
+
|
|
143
|
+
[file_path, line_number]
|
|
144
|
+
rescue PackageURL::InvalidPackageURL
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#: (String uri_string) -> [String, String]?
|
|
149
|
+
def parse_source_uri(uri_string)
|
|
150
|
+
uri = begin
|
|
151
|
+
URI(uri_string)
|
|
112
152
|
rescue URI::Error
|
|
113
153
|
nil
|
|
114
154
|
end #: as URI::Source?
|
|
115
155
|
return unless uri
|
|
116
156
|
|
|
117
|
-
gem_version = resolve_version(uri)
|
|
157
|
+
gem_version = resolve_version(uri.gem_version, uri.gem_name)
|
|
118
158
|
return if gem_version.nil?
|
|
119
159
|
|
|
120
160
|
path = uri.path
|
|
@@ -126,28 +166,20 @@ module RubyLsp
|
|
|
126
166
|
file_path = self.class.gem_paths.dig(gem_name, gem_version, CGI.unescape(path))
|
|
127
167
|
return if file_path.nil?
|
|
128
168
|
|
|
129
|
-
|
|
130
|
-
range: range_from_location(comment.location),
|
|
131
|
-
target: "file://#{file_path}##{uri.line_number}",
|
|
132
|
-
tooltip: "Jump to #{file_path}##{uri.line_number}",
|
|
133
|
-
)
|
|
169
|
+
[file_path, uri.line_number || "0"]
|
|
134
170
|
end
|
|
135
171
|
|
|
136
172
|
# Try to figure out the gem version for a source:// link. The order of precedence is:
|
|
137
173
|
# 1. The version in the URI
|
|
138
174
|
# 2. The version in the RBI file name
|
|
139
175
|
# 3. The version from the gemspec
|
|
140
|
-
#: (
|
|
141
|
-
def resolve_version(
|
|
142
|
-
version = uri.gem_version
|
|
176
|
+
#: (String? version, String? gem_name) -> String?
|
|
177
|
+
def resolve_version(version, gem_name)
|
|
143
178
|
return version unless version.nil? || version.empty?
|
|
144
179
|
|
|
145
180
|
return @gem_version unless @gem_version.nil? || @gem_version.empty?
|
|
146
181
|
|
|
147
|
-
gem_name
|
|
148
|
-
return unless gem_name
|
|
149
|
-
|
|
150
|
-
GEM_TO_VERSION_MAP[gem_name]
|
|
182
|
+
GEM_TO_VERSION_MAP[gem_name.to_s]
|
|
151
183
|
end
|
|
152
184
|
end
|
|
153
185
|
end
|
|
@@ -58,13 +58,11 @@ module RubyLsp
|
|
|
58
58
|
|
|
59
59
|
#: (Prism::CallNode) -> void
|
|
60
60
|
def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
|
|
61
|
-
return unless in_spec_context?
|
|
62
|
-
|
|
63
61
|
case node.name
|
|
64
62
|
when :describe
|
|
65
63
|
handle_describe(node)
|
|
66
64
|
when :it, :specify
|
|
67
|
-
handle_example(node)
|
|
65
|
+
handle_example(node) if in_spec_context?
|
|
68
66
|
end
|
|
69
67
|
end
|
|
70
68
|
|
|
@@ -117,10 +115,7 @@ module RubyLsp
|
|
|
117
115
|
|
|
118
116
|
#: (Prism::CallNode) -> void
|
|
119
117
|
def handle_example(node)
|
|
120
|
-
|
|
121
|
-
# We are not guaranteed to discover examples in the exact order using static analysis, so we use the line number
|
|
122
|
-
# instead. Note that anonymous examples mixed with meta-programming will not be handled correctly
|
|
123
|
-
description = extract_description(node) || "anonymous"
|
|
118
|
+
description = extract_it_description(node)
|
|
124
119
|
line = node.location.start_line - 1
|
|
125
120
|
parent = latest_group
|
|
126
121
|
return unless parent.is_a?(Requests::Support::TestItem)
|
|
@@ -141,16 +136,37 @@ module RubyLsp
|
|
|
141
136
|
|
|
142
137
|
#: (Prism::CallNode) -> String?
|
|
143
138
|
def extract_description(node)
|
|
139
|
+
arguments = node.arguments&.arguments
|
|
140
|
+
return unless arguments
|
|
141
|
+
|
|
142
|
+
parts = arguments.map { |arg| extract_argument_content(arg) }
|
|
143
|
+
return if parts.empty?
|
|
144
|
+
|
|
145
|
+
parts.join("::")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#: (Prism::CallNode) -> String
|
|
149
|
+
def extract_it_description(node)
|
|
150
|
+
# Minitest formats the descriptions into test method names by using the count of examples with the description
|
|
151
|
+
# We are not guaranteed to discover examples in the exact order using static analysis, so we use the line number
|
|
152
|
+
# instead. Note that anonymous examples mixed with meta-programming will not be handled correctly
|
|
144
153
|
first_argument = node.arguments&.arguments&.first
|
|
145
|
-
return unless first_argument
|
|
154
|
+
return "anonymous" unless first_argument
|
|
155
|
+
|
|
156
|
+
extract_argument_content(first_argument) || "anonymous"
|
|
157
|
+
end
|
|
146
158
|
|
|
147
|
-
|
|
159
|
+
#: (Prism::Node) -> String?
|
|
160
|
+
def extract_argument_content(arg)
|
|
161
|
+
case arg
|
|
148
162
|
when Prism::StringNode
|
|
149
|
-
|
|
163
|
+
arg.content
|
|
164
|
+
when Prism::SymbolNode
|
|
165
|
+
arg.value
|
|
150
166
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
151
|
-
constant_name(
|
|
167
|
+
constant_name(arg)
|
|
152
168
|
else
|
|
153
|
-
|
|
169
|
+
arg.slice
|
|
154
170
|
end
|
|
155
171
|
end
|
|
156
172
|
|
|
@@ -190,12 +206,13 @@ module RubyLsp
|
|
|
190
206
|
end
|
|
191
207
|
|
|
192
208
|
# Specs only using describes
|
|
193
|
-
|
|
194
|
-
return unless
|
|
209
|
+
first_group_index = @spec_group_id_stack.index { |i| i.is_a?(DescribeGroup) }
|
|
210
|
+
return unless first_group_index
|
|
195
211
|
|
|
212
|
+
first_group = @spec_group_id_stack[first_group_index] #: as !nil
|
|
196
213
|
item = @response_builder[first_group.id] #: as !nil
|
|
197
214
|
|
|
198
|
-
@spec_group_id_stack[1..] #: as !nil
|
|
215
|
+
@spec_group_id_stack[first_group_index + 1..] #: as !nil
|
|
199
216
|
.each do |group|
|
|
200
217
|
next unless group.is_a?(DescribeGroup)
|
|
201
218
|
|
|
@@ -61,11 +61,26 @@ module RubyLsp
|
|
|
61
61
|
|
|
62
62
|
#: (Prism::ClassNode node, String fully_qualified_name) -> Array[String]
|
|
63
63
|
def calc_attached_ancestors(node, fully_qualified_name)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
superclass = node.superclass
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
ancestors = @index.linearized_ancestors_of(fully_qualified_name)
|
|
68
|
+
# If the project has no bundle and a test class inherits from `Minitest::Test`, the linearized ancestors will
|
|
69
|
+
# not include the parent class because we never indexed it in the first place. Here we add the superclass
|
|
70
|
+
# directly, so that we can support running tests in projects without a bundle
|
|
71
|
+
return ancestors if ancestors.length > 1
|
|
72
|
+
|
|
73
|
+
# If all we found is the class itself, then manually include the parent class
|
|
74
|
+
if ancestors.first == fully_qualified_name && superclass
|
|
75
|
+
return [*ancestors, superclass.slice]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
ancestors
|
|
79
|
+
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
80
|
+
# When there are dynamic parts in the constant path, we will not have indexed the namespace. We can still
|
|
81
|
+
# provide test functionality if the class inherits directly from Test::Unit::TestCase or Minitest::Test
|
|
82
|
+
[superclass&.slice].compact
|
|
83
|
+
end
|
|
69
84
|
end
|
|
70
85
|
|
|
71
86
|
#: (Prism::ConstantPathNode | Prism::ConstantReadNode | Prism::ConstantPathTargetNode | Prism::CallNode | Prism::MissingNode node) -> String
|
|
@@ -33,13 +33,15 @@ module RubyLsp
|
|
|
33
33
|
|
|
34
34
|
if tags.include?("test_dir")
|
|
35
35
|
if children.empty?
|
|
36
|
-
full_files.concat(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
full_files.concat(
|
|
37
|
+
Dir.glob(
|
|
38
|
+
"#{path}/**/{*_test,test_*,*_spec}.rb",
|
|
39
|
+
File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
|
|
40
|
+
).map! { |p| Shellwords.escape(p) },
|
|
41
|
+
)
|
|
40
42
|
end
|
|
41
43
|
elsif tags.include?("test_file")
|
|
42
|
-
full_files << path if children.empty?
|
|
44
|
+
full_files << Shellwords.escape(path) if children.empty?
|
|
43
45
|
elsif tags.include?("test_group")
|
|
44
46
|
# If all of the children of the current test group are other groups, then there's no need to add it to the
|
|
45
47
|
# aggregated examples
|
|
@@ -114,7 +116,7 @@ module RubyLsp
|
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
load_path = spec?(file_path) ? "-Ispec" : "-Itest"
|
|
117
|
-
"#{COMMAND} #{load_path} #{file_path} --name \"/#{regex}/\""
|
|
119
|
+
"#{COMMAND} #{load_path} #{Shellwords.escape(file_path)} --name \"/#{regex}/\""
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
#: (String, Hash[String, Hash[Symbol, untyped]]) -> Array[String]
|
|
@@ -125,7 +127,7 @@ module RubyLsp
|
|
|
125
127
|
Shellwords.escape(TestDiscovery::DYNAMIC_REFERENCE_MARKER),
|
|
126
128
|
".*",
|
|
127
129
|
)
|
|
128
|
-
command = +"#{COMMAND} -Itest #{file_path} --testcase \"/^#{group_regex}\\$/\""
|
|
130
|
+
command = +"#{COMMAND} -Itest #{Shellwords.escape(file_path)} --testcase \"/^#{group_regex}\\$/\""
|
|
129
131
|
|
|
130
132
|
unless examples.empty?
|
|
131
133
|
command << if examples.length == 1
|
|
@@ -145,7 +147,7 @@ module RubyLsp
|
|
|
145
147
|
MINITEST_REPORTER_PATH = File.expand_path("../test_reporters/minitest_reporter.rb", __dir__) #: String
|
|
146
148
|
TEST_UNIT_REPORTER_PATH = File.expand_path("../test_reporters/test_unit_reporter.rb", __dir__) #: String
|
|
147
149
|
BASE_COMMAND = begin
|
|
148
|
-
Bundler.
|
|
150
|
+
Bundler.with_unbundled_env { Bundler.default_lockfile }
|
|
149
151
|
"bundle exec ruby"
|
|
150
152
|
rescue Bundler::GemfileNotFound
|
|
151
153
|
"ruby"
|
|
@@ -35,24 +35,78 @@ module RubyLsp
|
|
|
35
35
|
|
|
36
36
|
#: -> Array[String]
|
|
37
37
|
def find_relevant_paths
|
|
38
|
-
|
|
38
|
+
patterns = relevant_filename_patterns
|
|
39
|
+
|
|
40
|
+
candidate_paths = patterns.flat_map do |pattern|
|
|
41
|
+
Dir.glob(File.join(search_root, "**", pattern))
|
|
42
|
+
end
|
|
43
|
+
|
|
39
44
|
return [] if candidate_paths.empty?
|
|
40
45
|
|
|
41
|
-
find_most_similar_with_jaccard(candidate_paths).map { File.
|
|
46
|
+
find_most_similar_with_jaccard(candidate_paths).map { |path| File.expand_path(path, @workspace_path) }
|
|
42
47
|
end
|
|
43
48
|
|
|
49
|
+
# Determine the search roots based on the closest test directories.
|
|
50
|
+
# This scopes the search to reduce the number of files that need to be checked.
|
|
44
51
|
#: -> String
|
|
45
|
-
def
|
|
46
|
-
|
|
52
|
+
def search_root
|
|
53
|
+
current_path = File.join(".", @path)
|
|
54
|
+
current_dir = File.dirname(current_path)
|
|
55
|
+
while current_dir != "."
|
|
56
|
+
dir_basename = File.basename(current_dir)
|
|
57
|
+
|
|
58
|
+
# If current directory is a test directory, return its parent as search root
|
|
59
|
+
if TEST_KEYWORDS.include?(dir_basename)
|
|
60
|
+
return File.dirname(current_dir)
|
|
61
|
+
end
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
# Search the test directories by walking up the directory tree
|
|
64
|
+
begin
|
|
65
|
+
contains_test_dir = Dir
|
|
66
|
+
.entries(current_dir)
|
|
67
|
+
.filter { |entry| TEST_KEYWORDS.include?(entry) }
|
|
68
|
+
.any? { |entry| File.directory?(File.join(current_dir, entry)) }
|
|
69
|
+
|
|
70
|
+
return current_dir if contains_test_dir
|
|
71
|
+
rescue Errno::EACCES, Errno::ENOENT
|
|
72
|
+
# Skip directories we can't read
|
|
53
73
|
end
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
# Move up one level
|
|
76
|
+
parent_dir = File.dirname(current_dir)
|
|
77
|
+
current_dir = parent_dir
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
"."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
#: -> Array[String]
|
|
84
|
+
def relevant_filename_patterns
|
|
85
|
+
extension = File.extname(@path)
|
|
86
|
+
input_basename = File.basename(@path, extension)
|
|
87
|
+
|
|
88
|
+
if input_basename.match?(TEST_PATTERN)
|
|
89
|
+
# Test file -> find implementation
|
|
90
|
+
base = input_basename.gsub(TEST_PATTERN, "")
|
|
91
|
+
parent_dir = File.basename(File.dirname(@path))
|
|
92
|
+
|
|
93
|
+
# If test file is in a directory matching the implementation name
|
|
94
|
+
# (e.g., go_to_relevant_file/test_go_to_relevant_file_a.rb)
|
|
95
|
+
# return patterns for both the base file name and the parent directory name
|
|
96
|
+
if base.include?(parent_dir) && base != parent_dir
|
|
97
|
+
["#{base}#{extension}", "#{parent_dir}#{extension}"]
|
|
98
|
+
else
|
|
99
|
+
["#{base}#{extension}"]
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
# Implementation file -> find tests (including in matching directory)
|
|
103
|
+
[
|
|
104
|
+
"{#{TEST_PREFIX_GLOB}}#{input_basename}#{extension}",
|
|
105
|
+
"#{input_basename}{#{TEST_SUFFIX_GLOB}}#{extension}",
|
|
106
|
+
"#{input_basename}/{#{TEST_PREFIX_GLOB}}*#{extension}",
|
|
107
|
+
"#{input_basename}/*{#{TEST_SUFFIX_GLOB}}#{extension}",
|
|
108
|
+
]
|
|
109
|
+
end
|
|
56
110
|
end
|
|
57
111
|
|
|
58
112
|
# Using the Jaccard algorithm to determine the similarity between the
|
|
@@ -140,21 +140,25 @@ module RubyLsp
|
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
-
#: (RubyIndexer::Entry entry) -> Integer
|
|
143
|
+
#: (RubyIndexer::Entry entry) -> Integer
|
|
144
144
|
def kind_for_entry(entry)
|
|
145
145
|
case entry
|
|
146
146
|
when RubyIndexer::Entry::Class
|
|
147
147
|
Constant::SymbolKind::CLASS
|
|
148
148
|
when RubyIndexer::Entry::Module
|
|
149
149
|
Constant::SymbolKind::NAMESPACE
|
|
150
|
-
when RubyIndexer::Entry::Constant
|
|
150
|
+
when RubyIndexer::Entry::Constant, RubyIndexer::Entry::UnresolvedConstantAlias, RubyIndexer::Entry::ConstantAlias
|
|
151
151
|
Constant::SymbolKind::CONSTANT
|
|
152
|
-
when RubyIndexer::Entry::Method
|
|
152
|
+
when RubyIndexer::Entry::Method, RubyIndexer::Entry::UnresolvedMethodAlias, RubyIndexer::Entry::MethodAlias
|
|
153
153
|
entry.name == "initialize" ? Constant::SymbolKind::CONSTRUCTOR : Constant::SymbolKind::METHOD
|
|
154
154
|
when RubyIndexer::Entry::Accessor
|
|
155
155
|
Constant::SymbolKind::PROPERTY
|
|
156
|
-
when RubyIndexer::Entry::InstanceVariable
|
|
156
|
+
when RubyIndexer::Entry::InstanceVariable, RubyIndexer::Entry::ClassVariable
|
|
157
157
|
Constant::SymbolKind::FIELD
|
|
158
|
+
when RubyIndexer::Entry::GlobalVariable
|
|
159
|
+
Constant::SymbolKind::VARIABLE
|
|
160
|
+
else
|
|
161
|
+
Constant::SymbolKind::NULL
|
|
158
162
|
end
|
|
159
163
|
end
|
|
160
164
|
end
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This is a copy of the implementation from the `package_url` gem with the
|
|
5
|
+
# following license. Original source can be found at:
|
|
6
|
+
# https://github.com/package-url/packageurl-ruby/blob/main/lib/package_url.rb
|
|
7
|
+
|
|
8
|
+
# MIT License
|
|
9
|
+
#
|
|
10
|
+
# Copyright (c) 2021 package-url
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
# furnished to do so, subject to the following conditions:
|
|
18
|
+
#
|
|
19
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
# copies or substantial portions of the Software.
|
|
21
|
+
#
|
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
# SOFTWARE.
|
|
29
|
+
|
|
30
|
+
require "uri"
|
|
31
|
+
|
|
32
|
+
# A package URL, or _purl_, is a URL string used to
|
|
33
|
+
# identify and locate a software package in a mostly universal and uniform way
|
|
34
|
+
# across programing languages, package managers, packaging conventions, tools,
|
|
35
|
+
# APIs and databases.
|
|
36
|
+
#
|
|
37
|
+
# A purl is a URL composed of seven components:
|
|
38
|
+
#
|
|
39
|
+
# ```
|
|
40
|
+
# scheme:type/namespace/name@version?qualifiers#subpath
|
|
41
|
+
# ```
|
|
42
|
+
#
|
|
43
|
+
# For example,
|
|
44
|
+
# the package URL for this Ruby package at version 0.1.0 is
|
|
45
|
+
# `pkg:ruby/mattt/packageurl-ruby@0.1.0`.
|
|
46
|
+
module RubyLsp
|
|
47
|
+
class PackageURL
|
|
48
|
+
# Raised when attempting to parse an invalid package URL string.
|
|
49
|
+
# @see #parse
|
|
50
|
+
class InvalidPackageURL < ArgumentError; end
|
|
51
|
+
|
|
52
|
+
# The URL scheme, which has a constant value of `"pkg"`.
|
|
53
|
+
def scheme
|
|
54
|
+
"pkg"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The package type or protocol, such as `"gem"`, `"npm"`, and `"github"`.
|
|
58
|
+
attr_reader :type
|
|
59
|
+
|
|
60
|
+
# A name prefix, specific to the type of package.
|
|
61
|
+
# For example, an npm scope, a Docker image owner, or a GitHub user.
|
|
62
|
+
attr_reader :namespace
|
|
63
|
+
|
|
64
|
+
# The name of the package.
|
|
65
|
+
attr_reader :name
|
|
66
|
+
|
|
67
|
+
# The version of the package.
|
|
68
|
+
attr_reader :version
|
|
69
|
+
|
|
70
|
+
# Extra qualifying data for a package, specific to the type of package.
|
|
71
|
+
# For example, the operating system or architecture.
|
|
72
|
+
attr_reader :qualifiers
|
|
73
|
+
|
|
74
|
+
# An extra subpath within a package, relative to the package root.
|
|
75
|
+
attr_reader :subpath
|
|
76
|
+
|
|
77
|
+
# Constructs a package URL from its components
|
|
78
|
+
# @param type [String] The package type or protocol.
|
|
79
|
+
# @param namespace [String] A name prefix, specific to the type of package.
|
|
80
|
+
# @param name [String] The name of the package.
|
|
81
|
+
# @param version [String] The version of the package.
|
|
82
|
+
# @param qualifiers [Hash] Extra qualifying data for a package, specific to the type of package.
|
|
83
|
+
# @param subpath [String] An extra subpath within a package, relative to the package root.
|
|
84
|
+
def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil)
|
|
85
|
+
raise ArgumentError, "type is required" if type.nil? || type.empty?
|
|
86
|
+
raise ArgumentError, "name is required" if name.nil? || name.empty?
|
|
87
|
+
|
|
88
|
+
@type = type.downcase
|
|
89
|
+
@namespace = namespace
|
|
90
|
+
@name = name
|
|
91
|
+
@version = version
|
|
92
|
+
@qualifiers = qualifiers
|
|
93
|
+
@subpath = subpath
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Creates a new PackageURL from a string.
|
|
97
|
+
# @param [String] string The package URL string.
|
|
98
|
+
# @raise [InvalidPackageURL] If the string is not a valid package URL.
|
|
99
|
+
# @return [PackageURL]
|
|
100
|
+
def self.parse(string)
|
|
101
|
+
components = {
|
|
102
|
+
type: nil,
|
|
103
|
+
namespace: nil,
|
|
104
|
+
name: nil,
|
|
105
|
+
version: nil,
|
|
106
|
+
qualifiers: nil,
|
|
107
|
+
subpath: nil,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Split the purl string once from right on '#'
|
|
111
|
+
# - The left side is the remainder
|
|
112
|
+
# - Strip the right side from leading and trailing '/'
|
|
113
|
+
# - Split this on '/'
|
|
114
|
+
# - Discard any empty string segment from that split
|
|
115
|
+
# - Discard any '.' or '..' segment from that split
|
|
116
|
+
# - Percent-decode each segment
|
|
117
|
+
# - UTF-8-decode each segment if needed in your programming language
|
|
118
|
+
# - Join segments back with a '/'
|
|
119
|
+
# - This is the subpath
|
|
120
|
+
case string.rpartition("#")
|
|
121
|
+
in String => remainder, separator, String => subpath unless separator.empty?
|
|
122
|
+
subpath_components = []
|
|
123
|
+
subpath.split("/").each do |segment|
|
|
124
|
+
next if segment.empty? || segment == "." || segment == ".."
|
|
125
|
+
|
|
126
|
+
subpath_components << URI.decode_www_form_component(segment)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
components[:subpath] = subpath_components.compact.join("/")
|
|
130
|
+
|
|
131
|
+
string = remainder
|
|
132
|
+
else
|
|
133
|
+
components[:subpath] = nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Split the remainder once from right on '?'
|
|
137
|
+
# - The left side is the remainder
|
|
138
|
+
# - The right side is the qualifiers string
|
|
139
|
+
# - Split the qualifiers on '&'. Each part is a key=value pair
|
|
140
|
+
# - For each pair, split the key=value once from left on '=':
|
|
141
|
+
# - The key is the lowercase left side
|
|
142
|
+
# - The value is the percent-decoded right side
|
|
143
|
+
# - UTF-8-decode the value if needed in your programming language
|
|
144
|
+
# - Discard any key/value pairs where the value is empty
|
|
145
|
+
# - If the key is checksums,
|
|
146
|
+
# split the value on ',' to create a list of checksums
|
|
147
|
+
# - This list of key/value is the qualifiers object
|
|
148
|
+
case string.rpartition("?")
|
|
149
|
+
in String => remainder, separator, String => qualifiers unless separator.empty?
|
|
150
|
+
components[:qualifiers] = {}
|
|
151
|
+
|
|
152
|
+
qualifiers.split("&").each do |pair|
|
|
153
|
+
case pair.partition("=")
|
|
154
|
+
in String => key, separator, String => value unless separator.empty?
|
|
155
|
+
key = key.downcase
|
|
156
|
+
value = URI.decode_www_form_component(value)
|
|
157
|
+
next if value.empty?
|
|
158
|
+
|
|
159
|
+
components[:qualifiers][key] = case key
|
|
160
|
+
when "checksums"
|
|
161
|
+
value.split(",")
|
|
162
|
+
else
|
|
163
|
+
value
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
next
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
string = remainder
|
|
171
|
+
else
|
|
172
|
+
components[:qualifiers] = nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Split the remainder once from left on ':'
|
|
176
|
+
# - The left side lowercased is the scheme
|
|
177
|
+
# - The right side is the remainder
|
|
178
|
+
case string.partition(":")
|
|
179
|
+
in "pkg", separator, String => remainder unless separator.empty?
|
|
180
|
+
string = remainder
|
|
181
|
+
else
|
|
182
|
+
raise InvalidPackageURL, 'invalid or missing "pkg:" URL scheme'
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Strip the remainder from leading and trailing '/'
|
|
186
|
+
# Use gsub to remove ALL leading slashes instead of just one
|
|
187
|
+
string = string.gsub(%r{^/+}, "").delete_suffix("/")
|
|
188
|
+
# - Split this once from left on '/'
|
|
189
|
+
# - The left side lowercased is the type
|
|
190
|
+
# - The right side is the remainder
|
|
191
|
+
case string.partition("/")
|
|
192
|
+
in String => type, separator, remainder unless separator.empty?
|
|
193
|
+
components[:type] = type
|
|
194
|
+
|
|
195
|
+
string = remainder
|
|
196
|
+
else
|
|
197
|
+
raise InvalidPackageURL, "invalid or missing package type"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Split the remainder once from right on '@'
|
|
201
|
+
# - The left side is the remainder
|
|
202
|
+
# - Percent-decode the right side. This is the version.
|
|
203
|
+
# - UTF-8-decode the version if needed in your programming language
|
|
204
|
+
# - This is the version
|
|
205
|
+
case string.rpartition("@")
|
|
206
|
+
in String => remainder, separator, String => version unless separator.empty?
|
|
207
|
+
components[:version] = URI.decode_www_form_component(version)
|
|
208
|
+
|
|
209
|
+
string = remainder
|
|
210
|
+
else
|
|
211
|
+
components[:version] = nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Split the remainder once from right on '/'
|
|
215
|
+
# - The left side is the remainder
|
|
216
|
+
# - Percent-decode the right side. This is the name
|
|
217
|
+
# - UTF-8-decode this name if needed in your programming language
|
|
218
|
+
# - Apply type-specific normalization to the name if needed
|
|
219
|
+
# - This is the name
|
|
220
|
+
case string.rpartition("/")
|
|
221
|
+
in String => remainder, separator, String => name unless separator.empty?
|
|
222
|
+
components[:name] = URI.decode_www_form_component(name)
|
|
223
|
+
|
|
224
|
+
# Split the remainder on '/'
|
|
225
|
+
# - Discard any empty segment from that split
|
|
226
|
+
# - Percent-decode each segment
|
|
227
|
+
# - UTF-8-decode the each segment if needed in your programming language
|
|
228
|
+
# - Apply type-specific normalization to each segment if needed
|
|
229
|
+
# - Join segments back with a '/'
|
|
230
|
+
# - This is the namespace
|
|
231
|
+
components[:namespace] = remainder.split("/").map { |s| URI.decode_www_form_component(s) }.compact.join("/")
|
|
232
|
+
in _, _, String => name
|
|
233
|
+
components[:name] = URI.decode_www_form_component(name)
|
|
234
|
+
components[:namespace] = nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Ensure type and name are not nil before creating the PackageURL instance
|
|
238
|
+
raise InvalidPackageURL, "missing package type" if components[:type].nil?
|
|
239
|
+
raise InvalidPackageURL, "missing package name" if components[:name].nil?
|
|
240
|
+
|
|
241
|
+
# Create a new PackageURL with validated components
|
|
242
|
+
type = components[:type] || "" # This ensures type is never nil
|
|
243
|
+
name = components[:name] || "" # This ensures name is never nil
|
|
244
|
+
|
|
245
|
+
new(
|
|
246
|
+
type: type,
|
|
247
|
+
name: name,
|
|
248
|
+
namespace: components[:namespace],
|
|
249
|
+
version: components[:version],
|
|
250
|
+
qualifiers: components[:qualifiers],
|
|
251
|
+
subpath: components[:subpath],
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns a hash containing the
|
|
256
|
+
# scheme, type, namespace, name, version, qualifiers, and subpath components
|
|
257
|
+
# of the package URL.
|
|
258
|
+
def to_h
|
|
259
|
+
{
|
|
260
|
+
scheme: scheme,
|
|
261
|
+
type: @type,
|
|
262
|
+
namespace: @namespace,
|
|
263
|
+
name: @name,
|
|
264
|
+
version: @version,
|
|
265
|
+
qualifiers: @qualifiers,
|
|
266
|
+
subpath: @subpath,
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Returns a string representation of the package URL.
|
|
271
|
+
# Package URL representations are created according to the instructions from
|
|
272
|
+
# https://github.com/package-url/purl-spec/blob/0b1559f76b79829e789c4f20e6d832c7314762c5/PURL-SPECIFICATION.rst#how-to-build-purl-string-from-its-components.
|
|
273
|
+
def to_s
|
|
274
|
+
# Start a purl string with the "pkg:" scheme as a lowercase ASCII string
|
|
275
|
+
purl = "pkg:"
|
|
276
|
+
|
|
277
|
+
# Append the type string to the purl as a lowercase ASCII string
|
|
278
|
+
# Append '/' to the purl
|
|
279
|
+
|
|
280
|
+
purl += @type
|
|
281
|
+
purl += "/"
|
|
282
|
+
|
|
283
|
+
# If the namespace is not empty:
|
|
284
|
+
# - Strip the namespace from leading and trailing '/'
|
|
285
|
+
# - Split on '/' as segments
|
|
286
|
+
# - Apply type-specific normalization to each segment if needed
|
|
287
|
+
# - UTF-8-encode each segment if needed in your programming language
|
|
288
|
+
# - Percent-encode each segment
|
|
289
|
+
# - Join the segments with '/'
|
|
290
|
+
# - Append this to the purl
|
|
291
|
+
# - Append '/' to the purl
|
|
292
|
+
# - Strip the name from leading and trailing '/'
|
|
293
|
+
# - Apply type-specific normalization to the name if needed
|
|
294
|
+
# - UTF-8-encode the name if needed in your programming language
|
|
295
|
+
# - Append the percent-encoded name to the purl
|
|
296
|
+
#
|
|
297
|
+
# If the namespace is empty:
|
|
298
|
+
# - Apply type-specific normalization to the name if needed
|
|
299
|
+
# - UTF-8-encode the name if needed in your programming language
|
|
300
|
+
# - Append the percent-encoded name to the purl
|
|
301
|
+
case @namespace
|
|
302
|
+
in String => namespace unless namespace.empty?
|
|
303
|
+
segments = []
|
|
304
|
+
@namespace.delete_prefix("/").delete_suffix("/").split("/").each do |segment|
|
|
305
|
+
next if segment.empty?
|
|
306
|
+
|
|
307
|
+
segments << URI.encode_www_form_component(segment)
|
|
308
|
+
end
|
|
309
|
+
purl += segments.join("/")
|
|
310
|
+
|
|
311
|
+
purl += "/"
|
|
312
|
+
purl += URI.encode_www_form_component(@name.delete_prefix("/").delete_suffix("/"))
|
|
313
|
+
else
|
|
314
|
+
purl += URI.encode_www_form_component(@name)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# If the version is not empty:
|
|
318
|
+
# - Append '@' to the purl
|
|
319
|
+
# - UTF-8-encode the version if needed in your programming language
|
|
320
|
+
# - Append the percent-encoded version to the purl
|
|
321
|
+
case @version
|
|
322
|
+
in String => version unless version.empty?
|
|
323
|
+
purl += "@"
|
|
324
|
+
purl += URI.encode_www_form_component(@version)
|
|
325
|
+
else
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# If the qualifiers are not empty and not composed only of key/value pairs
|
|
330
|
+
# where the value is empty:
|
|
331
|
+
# - Append '?' to the purl
|
|
332
|
+
# - Build a list from all key/value pair:
|
|
333
|
+
# - discard any pair where the value is empty.
|
|
334
|
+
# - UTF-8-encode each value if needed in your programming language
|
|
335
|
+
# - If the key is checksums and this is a list of checksums
|
|
336
|
+
# join this list with a ',' to create this qualifier value
|
|
337
|
+
# - create a string by joining the lowercased key,
|
|
338
|
+
# the equal '=' sign and the percent-encoded value to create a qualifier
|
|
339
|
+
# - sort this list of qualifier strings lexicographically
|
|
340
|
+
# - join this list of qualifier strings with a '&' ampersand
|
|
341
|
+
# - Append this string to the purl
|
|
342
|
+
case @qualifiers
|
|
343
|
+
in Hash => qualifiers unless qualifiers.empty?
|
|
344
|
+
list = []
|
|
345
|
+
qualifiers.each do |key, value|
|
|
346
|
+
next if value.empty?
|
|
347
|
+
|
|
348
|
+
list << case [key, value]
|
|
349
|
+
in "checksums", Array => checksums
|
|
350
|
+
"#{key.downcase}=#{checksums.join(",")}"
|
|
351
|
+
else
|
|
352
|
+
"#{key.downcase}=#{URI.encode_www_form_component(value)}"
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
unless list.empty?
|
|
357
|
+
purl += "?"
|
|
358
|
+
purl += list.sort.join("&")
|
|
359
|
+
end
|
|
360
|
+
else
|
|
361
|
+
nil
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# If the subpath is not empty and not composed only of
|
|
365
|
+
# empty, '.' and '..' segments:
|
|
366
|
+
# - Append '#' to the purl
|
|
367
|
+
# - Strip the subpath from leading and trailing '/'
|
|
368
|
+
# - Split this on '/' as segments
|
|
369
|
+
# - Discard empty, '.' and '..' segments
|
|
370
|
+
# - Percent-encode each segment
|
|
371
|
+
# - UTF-8-encode each segment if needed in your programming language
|
|
372
|
+
# - Join the segments with '/'
|
|
373
|
+
# - Append this to the purl
|
|
374
|
+
case @subpath
|
|
375
|
+
in String => subpath unless subpath.empty?
|
|
376
|
+
segments = []
|
|
377
|
+
subpath.delete_prefix("/").delete_suffix("/").split("/").each do |segment|
|
|
378
|
+
next if segment.empty? || segment == "." || segment == ".."
|
|
379
|
+
|
|
380
|
+
# Custom encoding for URL fragment segments:
|
|
381
|
+
# 1. Explicitly encode % as %25 to prevent double-encoding issues
|
|
382
|
+
# 2. Percent-encode special characters according to URL fragment rules
|
|
383
|
+
# 3. This ensures proper round-trip encoding/decoding with the parse method
|
|
384
|
+
segments << segment.gsub(/%|[^A-Za-z0-9\-\._~]/) do |m|
|
|
385
|
+
m == "%" ? "%25" : format("%%%02X", m.ord)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
unless segments.empty?
|
|
390
|
+
purl += "#"
|
|
391
|
+
purl += segments.join("/")
|
|
392
|
+
end
|
|
393
|
+
else
|
|
394
|
+
nil
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
purl
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Returns an array containing the
|
|
401
|
+
# scheme, type, namespace, name, version, qualifiers, and subpath components
|
|
402
|
+
# of the package URL.
|
|
403
|
+
def deconstruct
|
|
404
|
+
[scheme, @type, @namespace, @name, @version, @qualifiers, @subpath]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Returns a hash containing the
|
|
408
|
+
# scheme, type, namespace, name, version, qualifiers, and subpath components
|
|
409
|
+
# of the package URL.
|
|
410
|
+
def deconstruct_keys(_keys)
|
|
411
|
+
to_h
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
@@ -61,6 +61,14 @@ module RubyLsp
|
|
|
61
61
|
"RuboCop::Formatter::BaseFormatter", # Suppress any output by using the base formatter
|
|
62
62
|
] #: Array[String]
|
|
63
63
|
|
|
64
|
+
# Functionality was introduced in 1.75.0 but had issues with autocorrect
|
|
65
|
+
REUSE_PRISM_RESULT = begin
|
|
66
|
+
gem("rubocop", ">= 1.80.1")
|
|
67
|
+
true
|
|
68
|
+
rescue LoadError
|
|
69
|
+
false
|
|
70
|
+
end #: bool
|
|
71
|
+
|
|
64
72
|
#: Array[::RuboCop::Cop::Offense]
|
|
65
73
|
attr_reader :offenses
|
|
66
74
|
|
|
@@ -81,7 +89,7 @@ module RubyLsp
|
|
|
81
89
|
@offenses = [] #: Array[::RuboCop::Cop::Offense]
|
|
82
90
|
@errors = [] #: Array[String]
|
|
83
91
|
@warnings = [] #: Array[String]
|
|
84
|
-
|
|
92
|
+
@prism_result = nil #: Prism::ParseLexResult?
|
|
85
93
|
|
|
86
94
|
args += DEFAULT_ARGS
|
|
87
95
|
rubocop_options = ::RuboCop::Options.new.parse(args).first
|
|
@@ -101,11 +109,7 @@ module RubyLsp
|
|
|
101
109
|
@warnings = []
|
|
102
110
|
@offenses = []
|
|
103
111
|
@options[:stdin] = contents
|
|
104
|
-
|
|
105
|
-
# Setting the Prism result before running the RuboCop runner makes it reuse the existing AST and avoids
|
|
106
|
-
# double-parsing. Unfortunately, this leads to a bunch of cops failing to execute properly under LSP mode.
|
|
107
|
-
# Uncomment this once reusing the Prism result is more stable
|
|
108
|
-
# @prism_result = prism_result
|
|
112
|
+
@prism_result = prism_result if REUSE_PRISM_RESULT
|
|
109
113
|
|
|
110
114
|
super([path])
|
|
111
115
|
|
|
@@ -20,17 +20,7 @@ module RubyLsp
|
|
|
20
20
|
# @override
|
|
21
21
|
#: -> Array[Interface::WorkspaceSymbol]
|
|
22
22
|
def perform
|
|
23
|
-
|
|
24
|
-
uri = entry.uri
|
|
25
|
-
file_path = uri.full_path
|
|
26
|
-
|
|
27
|
-
# We only show symbols declared in the workspace
|
|
28
|
-
in_dependencies = file_path && !not_in_dependencies?(file_path)
|
|
29
|
-
next if in_dependencies
|
|
30
|
-
|
|
31
|
-
# We should never show private symbols when searching the entire workspace
|
|
32
|
-
next if entry.private?
|
|
33
|
-
|
|
23
|
+
fuzzy_search.filter_map do |entry|
|
|
34
24
|
kind = kind_for_entry(entry)
|
|
35
25
|
loc = entry.location
|
|
36
26
|
|
|
@@ -44,7 +34,7 @@ module RubyLsp
|
|
|
44
34
|
container_name: container.join("::"),
|
|
45
35
|
kind: kind,
|
|
46
36
|
location: Interface::Location.new(
|
|
47
|
-
uri: uri.to_s,
|
|
37
|
+
uri: entry.uri.to_s,
|
|
48
38
|
range: Interface::Range.new(
|
|
49
39
|
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
|
|
50
40
|
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
|
|
@@ -53,6 +43,24 @@ module RubyLsp
|
|
|
53
43
|
)
|
|
54
44
|
end
|
|
55
45
|
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
#: -> Array[RubyIndexer::Entry]
|
|
50
|
+
def fuzzy_search
|
|
51
|
+
@index.fuzzy_search(@query) do |entry|
|
|
52
|
+
file_path = entry.uri.full_path
|
|
53
|
+
|
|
54
|
+
# We only show symbols declared in the workspace
|
|
55
|
+
in_dependencies = file_path && !not_in_dependencies?(file_path)
|
|
56
|
+
next if in_dependencies
|
|
57
|
+
|
|
58
|
+
# We should never show private symbols when searching the entire workspace
|
|
59
|
+
next if entry.private?
|
|
60
|
+
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
56
64
|
end
|
|
57
65
|
end
|
|
58
66
|
end
|
data/lib/ruby_lsp/server.rb
CHANGED
|
@@ -303,7 +303,7 @@ module RubyLsp
|
|
|
303
303
|
@current_request_id,
|
|
304
304
|
Interface::RelativePattern.new(
|
|
305
305
|
base_uri: @global_state.workspace_uri.to_s,
|
|
306
|
-
pattern: "{.rubocop.yml,.rubocop}",
|
|
306
|
+
pattern: "{.rubocop.yml,.rubocop,.rubocop_todo.yml}",
|
|
307
307
|
),
|
|
308
308
|
registration_id: "rubocop-watcher",
|
|
309
309
|
))
|
|
@@ -1045,7 +1045,7 @@ module RubyLsp
|
|
|
1045
1045
|
|
|
1046
1046
|
file_name = File.basename(file_path)
|
|
1047
1047
|
|
|
1048
|
-
if file_name == ".rubocop.yml" || file_name == ".rubocop"
|
|
1048
|
+
if file_name == ".rubocop.yml" || file_name == ".rubocop" || file_name == ".rubocop_todo.yml"
|
|
1049
1049
|
handle_rubocop_config_change(uri)
|
|
1050
1050
|
end
|
|
1051
1051
|
end
|
|
@@ -234,13 +234,14 @@ module RubyLsp
|
|
|
234
234
|
# If no error occurred, then clear previous errors
|
|
235
235
|
@error_path.delete if @error_path.exist?
|
|
236
236
|
$stderr.puts("Ruby LSP> Composed bundle installation complete")
|
|
237
|
-
rescue Errno::EPIPE
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
# it does not represent an actual error.
|
|
237
|
+
rescue Errno::EPIPE, Bundler::HTTPError
|
|
238
|
+
# There are cases where we expect certain errors to happen occasionally, and we don't want to write them to
|
|
239
|
+
# a file, which would report to telemetry on the next launch.
|
|
241
240
|
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
241
|
+
# - The $stderr pipe might be closed by the client, for example when closing the editor during running bundle
|
|
242
|
+
# install. This situation may happen because, while running bundle install, the server is not yet ready to
|
|
243
|
+
# receive shutdown requests and we may continue doing work until the process is killed.
|
|
244
|
+
# - Bundler might also encounter a network error.
|
|
244
245
|
@error_path.delete if @error_path.exist?
|
|
245
246
|
rescue => e
|
|
246
247
|
# Write the error object to a file so that we can read it from the parent process
|
|
@@ -269,12 +270,21 @@ module RubyLsp
|
|
|
269
270
|
#: (Hash[String, String] env, ?force_install: bool) -> Hash[String, String]
|
|
270
271
|
def run_bundle_install_directly(env, force_install: false)
|
|
271
272
|
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
|
|
273
|
+
|
|
274
|
+
# The should_bundle_update? check needs to run on the original Bundler environment, but everything else (like
|
|
275
|
+
# updating or running install) requires the modified environment. Here we compute the check ahead of time and
|
|
276
|
+
# merge the environment to ensure correct results.
|
|
277
|
+
#
|
|
278
|
+
# The symptoms of having these operations in the wrong order is seeing unwanted modifications in the application's
|
|
279
|
+
# main lockfile because we accidentally run update on the main bundle instead of the composed one.
|
|
280
|
+
needs_update = should_bundle_update?
|
|
281
|
+
ENV.merge!(env)
|
|
282
|
+
|
|
272
283
|
return update(env) if @needs_update_path.exist?
|
|
273
284
|
|
|
274
285
|
# The ENV can only be merged after checking if an update is required because we depend on the original value of
|
|
275
286
|
# ENV["BUNDLE_GEMFILE"], which gets overridden after the merge
|
|
276
|
-
FileUtils.touch(@needs_update_path) if
|
|
277
|
-
ENV.merge!(env)
|
|
287
|
+
FileUtils.touch(@needs_update_path) if needs_update
|
|
278
288
|
|
|
279
289
|
$stderr.puts("Ruby LSP> Checking if the composed bundle is satisfied...")
|
|
280
290
|
missing_gems = bundle_check
|
|
@@ -35,10 +35,10 @@ module RubyLsp
|
|
|
35
35
|
@io = begin
|
|
36
36
|
# The environment variable is only used for tests. The extension always writes to the temporary file
|
|
37
37
|
if port
|
|
38
|
-
|
|
38
|
+
socket(port)
|
|
39
39
|
elsif File.exist?(port_db_path)
|
|
40
40
|
db = JSON.load_file(port_db_path)
|
|
41
|
-
|
|
41
|
+
socket(db[Dir.pwd])
|
|
42
42
|
else
|
|
43
43
|
# For tests that don't spawn the TCP server
|
|
44
44
|
require "stringio"
|
|
@@ -209,6 +209,14 @@ module RubyLsp
|
|
|
209
209
|
|
|
210
210
|
private
|
|
211
211
|
|
|
212
|
+
#: (String) -> TCPSocket
|
|
213
|
+
def socket(port)
|
|
214
|
+
socket = TCPSocket.new("localhost", port)
|
|
215
|
+
socket.binmode
|
|
216
|
+
socket.sync = true
|
|
217
|
+
socket
|
|
218
|
+
end
|
|
219
|
+
|
|
212
220
|
#: (String?, **untyped) -> void
|
|
213
221
|
def send_message(method_name, **params)
|
|
214
222
|
json_message = { method: method_name, params: params }.to_json
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-lsp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.26.
|
|
4
|
+
version: 0.26.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
@@ -163,6 +163,7 @@ files:
|
|
|
163
163
|
- lib/ruby_lsp/requests/support/annotation.rb
|
|
164
164
|
- lib/ruby_lsp/requests/support/common.rb
|
|
165
165
|
- lib/ruby_lsp/requests/support/formatter.rb
|
|
166
|
+
- lib/ruby_lsp/requests/support/package_url.rb
|
|
166
167
|
- lib/ruby_lsp/requests/support/rubocop_diagnostic.rb
|
|
167
168
|
- lib/ruby_lsp/requests/support/rubocop_formatter.rb
|
|
168
169
|
- lib/ruby_lsp/requests/support/rubocop_runner.rb
|
|
@@ -216,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
216
217
|
- !ruby/object:Gem::Version
|
|
217
218
|
version: '0'
|
|
218
219
|
requirements: []
|
|
219
|
-
rubygems_version: 3.
|
|
220
|
+
rubygems_version: 3.7.2
|
|
220
221
|
specification_version: 4
|
|
221
222
|
summary: An opinionated language server for Ruby
|
|
222
223
|
test_files: []
|