dependabot-maven 0.308.0 → 0.309.0

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.
@@ -0,0 +1,485 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+ require "excon"
6
+ require "nokogiri"
7
+ require "dependabot/registry_client"
8
+ require "dependabot/package/package_release"
9
+ require "dependabot/package/package_details"
10
+ require "dependabot/maven/file_parser/repositories_finder"
11
+ require "dependabot/maven/version"
12
+ require "dependabot/maven/requirement"
13
+ require "dependabot/maven/utils/auth_headers_finder"
14
+ require "sorbet-runtime"
15
+
16
+ module Dependabot
17
+ module Maven
18
+ module Package
19
+ class PackageDetailsFetcher
20
+ extend T::Sig
21
+
22
+ META_DATE_XML = T.let("maven-metadata.xml", String)
23
+ REPOSITORY_TYPE = T.let("maven_repository", String)
24
+ URL_KEY = T.let("url", String)
25
+ AUTH_HEADERS_KEY = T.let("auth_headers", String)
26
+
27
+ sig do
28
+ params(
29
+ dependency: Dependabot::Dependency,
30
+ dependency_files: T::Array[Dependabot::DependencyFile],
31
+ credentials: T::Array[Dependabot::Credential]
32
+ ).void
33
+ end
34
+ def initialize(dependency:, dependency_files:, credentials:) # rubocop:disable Metrics/AbcSize
35
+ @dependency = dependency
36
+ @dependency_files = dependency_files
37
+ @credentials = credentials
38
+
39
+ @forbidden_urls = T.let([], T::Array[String])
40
+ @pom_repository_details = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
41
+ @dependency_metadata = T.let({}, T::Hash[T.untyped, Nokogiri::XML::Document])
42
+ @dependency_metadata_from_html = T.let({}, T::Hash[T.untyped, Nokogiri::HTML::Document])
43
+ @repository_finder = T.let(nil, T.nilable(Maven::FileParser::RepositoriesFinder))
44
+ @repositories = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
45
+ @released_check = T.let({}, T::Hash[Dependabot::Version, T::Boolean])
46
+ @auth_headers_finder = T.let(nil, T.nilable(Utils::AuthHeadersFinder))
47
+ @dependency_parts = T.let(nil, T.nilable([String, String]))
48
+ @version_details = T.let(nil, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]))
49
+ @package_details = T.let(nil, T.nilable(Dependabot::Package::PackageDetails))
50
+ end
51
+
52
+ sig { returns(Dependabot::Dependency) }
53
+ attr_reader :dependency
54
+
55
+ sig { returns(T::Array[T.untyped]) }
56
+ attr_reader :dependency_files
57
+
58
+ sig { returns(T::Array[T.untyped]) }
59
+ attr_reader :credentials
60
+ sig { returns(T::Array[T.untyped]) }
61
+ attr_reader :forbidden_urls
62
+
63
+ sig { returns(Dependabot::Package::PackageDetails) }
64
+ def fetch
65
+ return @package_details if @package_details
66
+
67
+ releases = versions.map do |version_details|
68
+ Dependabot::Package::PackageRelease.new(
69
+ version: version_details.fetch(:version),
70
+ released_at: version_details.fetch(:release_date, nil),
71
+ url: version_details.fetch(:source_url)
72
+ )
73
+ end
74
+
75
+ @package_details = Dependabot::Package::PackageDetails.new(
76
+ dependency: dependency,
77
+ releases: releases
78
+ )
79
+
80
+ @package_details
81
+ end
82
+
83
+ sig { returns(T::Array[T.untyped]) }
84
+ def releases
85
+ fetch.releases
86
+ end
87
+
88
+ sig { params(version: Dependabot::Version).returns(T::Boolean) }
89
+ def released?(version)
90
+ released_check?(version)
91
+ end
92
+
93
+ private
94
+
95
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
96
+ def versions
97
+ return @version_details if @version_details
98
+
99
+ @version_details = versions_details_from_xml
100
+
101
+ begin
102
+ versions_details_hash = versions_details_hash_from_html if @version_details.any?
103
+
104
+ if versions_details_hash
105
+ @version_details = @version_details.map do |version_details|
106
+ version = version_details[:version].to_s
107
+ version_details_hash = versions_details_hash[version]
108
+
109
+ next version_details unless version_details_hash
110
+
111
+ release_date = version_details_hash[:release_date]
112
+
113
+ next version_details unless release_date
114
+
115
+ version_details.merge(
116
+ release_date: version_details_hash[:release_date],
117
+ source_url: version_details[:source_url]
118
+ )
119
+ end
120
+ end
121
+ rescue StandardError => e
122
+ Dependabot.logger.error("Error fetching version details from HTML: #{e.message}")
123
+ end
124
+
125
+ @version_details = @version_details.sort_by { |details| details.fetch(:version) }
126
+ @version_details
127
+ end
128
+
129
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
130
+ def versions_details_from_xml
131
+ forbidden_urls.clear
132
+ version_details = repositories.flat_map do |repository_details|
133
+ url = repository_details.fetch(URL_KEY)
134
+ xml = dependency_metadata(repository_details)
135
+ next [] if xml.nil?
136
+
137
+ break extract_metadata_from_xml(xml, url)
138
+ end
139
+
140
+ raise PrivateSourceAuthenticationFailure, forbidden_urls.first if version_details.none? && forbidden_urls.any?
141
+
142
+ version_details
143
+ end
144
+
145
+ sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
146
+ def versions_details_hash_from_html
147
+ forbidden_urls.clear
148
+
149
+ # Iterate over repositories and fetch the first valid result
150
+ versions_detail_hash = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]])
151
+ repositories.each do |repository_details|
152
+ html = dependency_metadata_from_html(repository_details)
153
+
154
+ # Skip if no HTML data is found
155
+ next if html.nil?
156
+
157
+ # Break and return result from the first valid HTML
158
+ versions_detail_hash = extract_version_details_from_html(html)
159
+
160
+ break if versions_detail_hash.any?
161
+ end
162
+
163
+ # If no version details were found, but there are forbidden URLs, raise an error
164
+ if versions_detail_hash.any? && forbidden_urls.any?
165
+ raise PrivateSourceAuthenticationFailure,
166
+ forbidden_urls.first
167
+ end
168
+
169
+ # Return the populated version details hash (may be empty if no valid repositories)
170
+ versions_detail_hash
171
+ end
172
+
173
+ sig { params(version: Dependabot::Version).returns(T::Boolean) }
174
+ def released_check?(version)
175
+ @released_check[version] ||=
176
+ repositories.any? do |repository_details|
177
+ url = repository_details.fetch(URL_KEY)
178
+ auth_headers = repository_details.fetch(AUTH_HEADERS_KEY)
179
+ response = Dependabot::RegistryClient.head(
180
+ url: dependency_files_url(url, version),
181
+ headers: auth_headers
182
+ )
183
+
184
+ response.status < 400
185
+ rescue Excon::Error::Socket, Excon::Error::Timeout,
186
+ Excon::Error::TooManyRedirects
187
+ false
188
+ rescue URI::InvalidURIError => e
189
+ raise DependencyFileNotResolvable, e.message
190
+ end
191
+ end
192
+
193
+ # Extracts version details from the HTML document.
194
+ sig do
195
+ params(html_doc: Nokogiri::HTML::Document)
196
+ .returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
197
+ end
198
+ def extract_version_details_from_html(html_doc)
199
+ versions_detail_hash = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]])
200
+
201
+ html_doc.css("a[title]").each do |link|
202
+ version_string = link["title"]
203
+ version = version_string.gsub(%r{/$}, "") # Remove trailing slash
204
+
205
+ # Release date should be located after the version, and it is within the same <pre> block
206
+ raw_date_text = link.next.text.strip.split("\n").last.strip # Extract the last part of the text
207
+
208
+ # Parse the date and time properly (YYYY-MM-DD HH:MM)
209
+ release_date = begin
210
+ Time.parse(raw_date_text)
211
+ rescue StandardError
212
+ nil
213
+ end
214
+
215
+ next unless version && version_class.correct?(version)
216
+
217
+ versions_detail_hash[version] = {
218
+ release_date: release_date
219
+ }
220
+ end
221
+ versions_detail_hash
222
+ end
223
+
224
+ # Extracts version details from the XML document.
225
+ sig do
226
+ params(
227
+ xml: Nokogiri::XML::Document,
228
+ url: String
229
+ ).returns(T::Array[T::Hash[Symbol, T.untyped]])
230
+ end
231
+ def extract_metadata_from_xml(xml, url)
232
+ xml.css("versions > version")
233
+ .select { |node| version_class.correct?(node.content) }
234
+ .map { |node| version_class.new(node.content) }
235
+ .map { |version| { version: version, source_url: url } }
236
+ end
237
+
238
+ sig { params(repository_details: T::Hash[String, T.untyped]).returns(T.nilable(Nokogiri::XML::Document)) }
239
+ def fetch_dependency_metadata(repository_details)
240
+ url = repository_details.fetch(URL_KEY)
241
+ auth_headers = repository_details.fetch(AUTH_HEADERS_KEY)
242
+ response = Dependabot::RegistryClient.get(
243
+ url: dependency_metadata_url(url),
244
+ headers: auth_headers
245
+ )
246
+ check_response(response, url)
247
+ return unless response.status < 400
248
+
249
+ Nokogiri::XML(response.body)
250
+ rescue URI::InvalidURIError
251
+ nil
252
+ rescue Excon::Error::Socket, Excon::Error::Timeout,
253
+ Excon::Error::TooManyRedirects => e
254
+ handle_registry_error(url, e, response)
255
+ nil
256
+ end
257
+
258
+ sig { returns(T::Array[T::Hash[String, T.untyped]]) }
259
+ def repositories
260
+ return @repositories if @repositories
261
+
262
+ @repositories = credentials_repository_details
263
+ pom_repository_details.each do |repo|
264
+ @repositories << repo unless @repositories.any? do |r|
265
+ r[URL_KEY] == repo[URL_KEY]
266
+ end
267
+ end
268
+ @repositories
269
+ end
270
+
271
+ sig { params(repository_details: T::Hash[String, T.untyped]).returns(T.nilable(Nokogiri::XML::Document)) }
272
+ def dependency_metadata(repository_details)
273
+ repository_key = repository_details.hash
274
+ return @dependency_metadata[repository_key] if @dependency_metadata.key?(repository_key)
275
+
276
+ xml_document = fetch_dependency_metadata(repository_details)
277
+
278
+ @dependency_metadata[repository_key] ||= xml_document if xml_document
279
+ @dependency_metadata[repository_key]
280
+ end
281
+
282
+ sig { params(repository_details: T::Hash[String, T.untyped]).returns(T.nilable(Nokogiri::HTML::Document)) }
283
+ def dependency_metadata_from_html(repository_details)
284
+ repository_key = repository_details.hash
285
+ return @dependency_metadata_from_html[repository_key] if @dependency_metadata_from_html.key?(repository_key)
286
+
287
+ html_document = fetch_dependency_metadata_from_html(repository_details)
288
+
289
+ @dependency_metadata_from_html[repository_key] ||= html_document if html_document
290
+ @dependency_metadata_from_html[repository_key]
291
+ end
292
+
293
+ sig { params(response: Excon::Response, repository_url: String).void }
294
+ def check_response(response, repository_url)
295
+ return unless [401, 403].include?(response.status)
296
+ return if @forbidden_urls.include?(repository_url)
297
+ return if central_repo_urls.include?(repository_url)
298
+
299
+ @forbidden_urls << repository_url
300
+ end
301
+
302
+ sig do
303
+ params(
304
+ repository_details: T::Hash[String, T.untyped]
305
+ ).returns(T.nilable(Nokogiri::HTML::Document))
306
+ end
307
+ def fetch_dependency_metadata_from_html(repository_details)
308
+ url = repository_details.fetch(URL_KEY)
309
+ auth_headers = repository_details.fetch(AUTH_HEADERS_KEY)
310
+ response = Dependabot::RegistryClient.get(
311
+ url: dependency_base_url(url),
312
+ headers: auth_headers
313
+ )
314
+ check_response(response, url)
315
+ return unless response.status < 400
316
+
317
+ Nokogiri::HTML(response.body)
318
+ rescue URI::InvalidURIError
319
+ nil
320
+ rescue Excon::Error::Socket, Excon::Error::Timeout,
321
+ Excon::Error::TooManyRedirects => e
322
+ handle_registry_error(url, e, response)
323
+ nil
324
+ end
325
+
326
+ sig { returns(Maven::FileParser::RepositoriesFinder) }
327
+ def repository_finder
328
+ return @repository_finder if @repository_finder
329
+
330
+ @repository_finder =
331
+ Maven::FileParser::RepositoriesFinder.new(
332
+ pom_fetcher: Maven::FileParser::PomFetcher.new(dependency_files: dependency_files),
333
+ dependency_files: dependency_files,
334
+ credentials: credentials
335
+ )
336
+ @repository_finder
337
+ end
338
+
339
+ # Returns the repository details for the POM file.
340
+ # Example:
341
+ # repository_url: https://repo.maven.apache.org/maven2
342
+ # returns: [{ "url" => "https://repo.maven.apache.org/maven2", "auth_headers" => {} }]
343
+ sig { returns(T::Array[T::Hash[String, T.untyped]]) }
344
+ def pom_repository_details
345
+ return @pom_repository_details if @pom_repository_details
346
+
347
+ @pom_repository_details =
348
+ repository_finder
349
+ .repository_urls(pom: T.must(pom))
350
+ .map do |url|
351
+ { URL_KEY => url, AUTH_HEADERS_KEY => {} }
352
+ end
353
+ @pom_repository_details
354
+ end
355
+
356
+ # Returns the POM file for the dependency, if it exists.
357
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
358
+ def pom
359
+ filename = dependency.requirements.first&.fetch(:file)
360
+ dependency_files.find { |f| f.name == filename }
361
+ end
362
+
363
+ # Constructs the URL for the dependency's metadata file (maven-metadata.xml).
364
+ #
365
+ # Example:
366
+ # repository_url: https://repo.maven.apache.org/maven2
367
+ # returns: https://repo.maven.apache.org/maven2/com/google/guava/guava/maven-metadata.xml
368
+ sig { params(repository_url: String).returns(String) }
369
+ def dependency_metadata_url(repository_url)
370
+ "#{dependency_base_url(repository_url)}/#{META_DATE_XML}"
371
+ end
372
+
373
+ # Constructs the URL for the dependency files, including version and artifact information.
374
+ #
375
+ # Example:
376
+ # repository_url: https://repo.maven.apache.org/maven2
377
+ # version: 23.6-jre
378
+ # artifact_id: guava
379
+ # group_id: com.google.guava
380
+ # classifier: nil
381
+ # type: jar
382
+ # returns: https://repo.maven.apache.org/maven2/com/google/guava/guava/23.6-jre/guava-23.6-jre.jar
383
+ # https://repo.maven.apache.org/maven2/com/google/guava/guava/23.7-jre/-23.7-jre.jar
384
+ sig { params(repository_url: String, version: Dependabot::Version).returns(String) }
385
+ def dependency_files_url(repository_url, version)
386
+ _, artifact_id = dependency_parts
387
+ base_url = dependency_base_url(repository_url)
388
+ type = dependency.requirements.first&.dig(:metadata, :packaging_type)
389
+ classifier = dependency.requirements.first&.dig(:metadata, :classifier)
390
+ actual_classifier = classifier.nil? ? "" : "-#{classifier}"
391
+
392
+ "#{base_url}/#{version.to_semver}/" \
393
+ "#{artifact_id}-#{version.to_semver}#{actual_classifier}.#{type}"
394
+ end
395
+
396
+ # # Constructs the full URL by combining the repository URL, group path, and artifact ID
397
+ #
398
+ # Example:
399
+ # repository_url: https://repo.maven.apache.org/maven2
400
+ # group_path: com/google/guava
401
+ # artifact_id: guava
402
+ # returns: https://repo.maven.apache.org/maven2/com/google/guava/guava
403
+ sig { params(repository_url: String).returns(String) }
404
+ def dependency_base_url(repository_url)
405
+ group_path, artifact_id = dependency_parts
406
+
407
+ "#{repository_url}/#{group_path}/#{artifact_id}"
408
+ end
409
+
410
+ # Splits the dependency name into its group path and artifact ID.
411
+ #
412
+ # Example:
413
+ # dependency.name: com.google.guava:guava
414
+ # returns: ["com/google/guava", "guava"]
415
+ sig { returns(T.nilable([String, String])) }
416
+ def dependency_parts
417
+ return @dependency_parts if @dependency_parts
418
+
419
+ group_id, artifact_id = dependency.name.split(":")
420
+ group_path = group_id&.tr(".", "/")
421
+ @dependency_parts = [T.must(group_path), T.must(artifact_id)]
422
+ @dependency_parts
423
+ end
424
+
425
+ sig { returns(T::Array[T.untyped]) }
426
+ def credentials_repository_details
427
+ credentials
428
+ .select { |cred| cred["type"] == REPOSITORY_TYPE && cred[URL_KEY] }
429
+ .map do |cred|
430
+ url_value = cred.fetch(URL_KEY).gsub(%r{/+$}, "")
431
+ {
432
+ URL_KEY => url_value,
433
+ AUTH_HEADERS_KEY => auth_headers(url_value)
434
+ }
435
+ end
436
+ end
437
+
438
+ sig { returns(T.class_of(Dependabot::Version)) }
439
+ def version_class
440
+ dependency.version_class
441
+ end
442
+
443
+ sig { returns(T::Array[String]) }
444
+ def central_repo_urls
445
+ central_url_without_protocol = repository_finder.central_repo_url.gsub(%r{^.*://}, "")
446
+
447
+ %w(http:// https://).map { |p| p + central_url_without_protocol }
448
+ end
449
+
450
+ sig { returns(Utils::AuthHeadersFinder) }
451
+ def auth_headers_finder
452
+ return @auth_headers_finder if @auth_headers_finder
453
+
454
+ @auth_headers_finder = Utils::AuthHeadersFinder.new(credentials)
455
+ @auth_headers_finder
456
+ end
457
+
458
+ sig { params(maven_repo_url: String).returns(T::Hash[String, String]) }
459
+ def auth_headers(maven_repo_url)
460
+ auth_headers_finder.auth_headers(maven_repo_url)
461
+ end
462
+
463
+ sig do
464
+ params(
465
+ url: String,
466
+ error: Excon::Error,
467
+ response: T.nilable(Excon::Response)
468
+ ).void
469
+ end
470
+ def handle_registry_error(url, error, response)
471
+ return unless central_repo_urls.include?(url)
472
+
473
+ response_status = response&.status || 0
474
+ response_body = if response
475
+ "RegistryError: #{response.status} response status with body #{response.body}"
476
+ else
477
+ "RegistryError: #{error.message}"
478
+ end
479
+
480
+ raise RegistryError.new(response_status, response_body)
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -13,12 +13,13 @@ module Dependabot
13
13
  extend T::Sig
14
14
 
15
15
  quoted = OPS.keys.map { |k| Regexp.quote k }.join("|")
16
- OR_SYNTAX = /(?<=\]|\)),/
17
- PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{Maven::Version::VERSION_PATTERN})\\s*".freeze
18
- PATTERN = /\A#{PATTERN_RAW}\z/
16
+ OR_SYNTAX = T.let(/(?<=\]|\)),/, Regexp)
17
+ PATTERN_RAW = T.let("\\s*(#{quoted})?\\s*(#{Maven::Version::VERSION_PATTERN})\\s*".freeze, String)
18
+ PATTERN = T.let(/\A#{PATTERN_RAW}\z/, Regexp)
19
19
  # Like PATTERN, but the leading operator is required
20
- RUBY_STYLE_PATTERN = /\A\s*(#{quoted})\s*(#{Maven::Version::VERSION_PATTERN})\s*\z/
20
+ RUBY_STYLE_PATTERN = T.let(/\A\s*(#{quoted})\s*(#{Maven::Version::VERSION_PATTERN})\s*\z/, Regexp)
21
21
 
22
+ sig { params(obj: T.any(String, Gem::Version)).returns(T::Array[T.any(String, T.untyped)]) }
22
23
  def self.parse(obj)
23
24
  return ["=", Maven::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
24
25
 
@@ -39,6 +40,7 @@ module Dependabot
39
40
  end
40
41
  end
41
42
 
43
+ sig { params(requirements: T.untyped).void }
42
44
  def initialize(*requirements)
43
45
  requirements = requirements.flatten.flat_map do |req_string|
44
46
  convert_java_constraint_to_ruby_constraint(req_string)
@@ -47,6 +49,7 @@ module Dependabot
47
49
  super(requirements)
48
50
  end
49
51
 
52
+ sig { params(version: T.untyped).returns(T::Boolean) }
50
53
  def satisfied_by?(version)
51
54
  version = Maven::Version.new(version.to_s)
52
55
  super
@@ -54,18 +57,25 @@ module Dependabot
54
57
 
55
58
  private
56
59
 
60
+ sig { params(req_string: T.nilable(String)).returns(T::Array[String]) }
57
61
  def self.split_java_requirement(req_string)
58
- return [req_string] unless req_string.match?(OR_SYNTAX)
62
+ return [req_string || ""] unless req_string&.match?(OR_SYNTAX)
59
63
 
60
64
  req_string.split(OR_SYNTAX).flat_map do |str|
61
65
  next str if str.start_with?("(", "[")
62
66
 
63
67
  exacts, *rest = str.split(/,(?=\[|\()/)
64
- [*exacts.split(","), *rest]
68
+ [*T.must(exacts).split(","), *rest]
65
69
  end
66
70
  end
67
71
  private_class_method :split_java_requirement
68
72
 
73
+ sig do
74
+ params(
75
+ req_string: T.nilable(String)
76
+ )
77
+ .returns(T.nilable(T.any(T::Array[String], T::Array[T.nilable(String)])))
78
+ end
69
79
  def convert_java_constraint_to_ruby_constraint(req_string)
70
80
  return unless req_string
71
81
 
@@ -86,26 +96,32 @@ module Dependabot
86
96
  end
87
97
  end
88
98
 
99
+ # rubocop:disable Metrics/PerceivedComplexity
100
+ sig { params(req_string: String).returns(T::Array[T.nilable(String)]) }
89
101
  def convert_java_range_to_ruby_range(req_string)
90
- lower_b, upper_b = req_string.split(",").map(&:strip)
102
+ parts = req_string.split(",").map(&:strip)
103
+ lower_b = T.let(parts[0], T.nilable(String))
104
+ upper_b = T.let(parts[1], T.nilable(String))
91
105
 
92
106
  lower_b =
93
- if ["(", "["].include?(lower_b) then nil
94
- elsif lower_b.start_with?("(") then "> #{lower_b.sub(/\(\s*/, '')}"
95
- else
107
+ if lower_b && ["(", "["].include?(lower_b) then nil
108
+ elsif lower_b&.start_with?("(") then "> #{lower_b.sub(/\(\s*/, '')}"
109
+ elsif lower_b
96
110
  ">= #{lower_b.sub(/\[\s*/, '').strip}"
97
111
  end
98
112
 
99
113
  upper_b =
100
- if [")", "]"].include?(upper_b) then nil
101
- elsif upper_b.end_with?(")") then "< #{upper_b.sub(/\s*\)/, '')}"
102
- else
114
+ if upper_b && [")", "]"].include?(upper_b) then nil
115
+ elsif upper_b&.end_with?(")") then "< #{upper_b.sub(/\s*\)/, '')}"
116
+ elsif upper_b
103
117
  "<= #{upper_b.sub(/\s*\]/, '').strip}"
104
118
  end
105
119
 
106
120
  [lower_b, upper_b].compact
107
121
  end
122
+ # rubocop:enable Metrics/PerceivedComplexity
108
123
 
124
+ sig { params(req_string: T.nilable(String)).returns(T.nilable(String)) }
109
125
  def convert_java_equals_req_to_ruby(req_string)
110
126
  return convert_wildcard_req(req_string) if req_string&.end_with?("+")
111
127
 
@@ -115,8 +131,9 @@ module Dependabot
115
131
  req_string.gsub(/[\[\]\(\)]/, "")
116
132
  end
117
133
 
134
+ sig { params(req_string: T.nilable(String)).returns(String) }
118
135
  def convert_wildcard_req(req_string)
119
- version = req_string.split("+").first
136
+ version = req_string&.split("+")&.first
120
137
  return ">= 0" if version.nil? || version.empty?
121
138
 
122
139
  version += "0" if version.end_with?(".")