dependabot-nuget 0.248.0 → 0.249.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,8 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
4
6
  require "dependabot/update_checkers/base"
5
7
  require "dependabot/nuget/file_parser"
6
8
 
@@ -8,28 +10,47 @@ module Dependabot
8
10
  module Nuget
9
11
  class UpdateChecker < Dependabot::UpdateCheckers::Base
10
12
  class PropertyUpdater
13
+ extend T::Sig
14
+
11
15
  require_relative "version_finder"
12
16
  require_relative "requirements_updater"
13
17
  require_relative "dependency_finder"
14
18
 
19
+ sig do
20
+ params(
21
+ dependency: Dependabot::Dependency,
22
+ dependency_files: T::Array[Dependabot::DependencyFile],
23
+ credentials: T::Array[Dependabot::Credential],
24
+ target_version_details: T.nilable(T::Hash[Symbol, String]),
25
+ ignored_versions: T::Array[String],
26
+ repo_contents_path: T.nilable(String),
27
+ raise_on_ignored: T::Boolean
28
+ ).void
29
+ end
15
30
  def initialize(dependency:, dependency_files:, credentials:,
16
31
  target_version_details:, ignored_versions:,
17
- raise_on_ignored: false, repo_contents_path:)
32
+ repo_contents_path:, raise_on_ignored: false)
18
33
  @dependency = dependency
19
34
  @dependency_files = dependency_files
20
35
  @credentials = credentials
21
36
  @ignored_versions = ignored_versions
22
37
  @raise_on_ignored = raise_on_ignored
23
- @target_version = target_version_details&.fetch(:version)
24
- @source_details = target_version_details
25
- &.slice(:nuspec_url, :repo_url, :source_url)
38
+ @target_version = T.let(
39
+ target_version_details&.fetch(:version),
40
+ T.nilable(T.any(String, Dependabot::Nuget::Version))
41
+ )
42
+ @source_details = T.let(
43
+ target_version_details&.slice(:nuspec_url, :repo_url, :source_url),
44
+ T.nilable(T::Hash[Symbol, String])
45
+ )
26
46
  @repo_contents_path = repo_contents_path
27
47
  end
28
48
 
49
+ sig { returns(T::Boolean) }
29
50
  def update_possible?
30
51
  return false unless target_version
31
52
 
32
- @update_possible ||=
53
+ @update_possible ||= T.let(
33
54
  dependencies_using_property.all? do |dep|
34
55
  versions = VersionFinder.new(
35
56
  dependency: dep,
@@ -42,42 +63,73 @@ module Dependabot
42
63
  ).versions.map { |v| v.fetch(:version) }
43
64
 
44
65
  versions.include?(target_version) || versions.none?
45
- end
66
+ end,
67
+ T.nilable(T::Boolean)
68
+ )
46
69
  end
47
70
 
71
+ sig { returns(T::Array[Dependabot::Dependency]) }
48
72
  def updated_dependencies
49
73
  raise "Update not possible!" unless update_possible?
50
74
 
51
- @updated_dependencies ||= begin
52
- dependencies = {}
53
-
54
- dependencies_using_property.each do |dep|
55
- # Only keep one copy of each dependency, the one with the highest target version.
56
- visited_dependency = dependencies[dep.name.downcase]
57
- next unless visited_dependency.nil? || visited_dependency.numeric_version < target_version
58
-
59
- updated_dependency = Dependency.new(
60
- name: dep.name,
61
- version: target_version.to_s,
62
- requirements: updated_requirements(dep),
63
- previous_version: dep.version,
64
- previous_requirements: dep.requirements,
65
- package_manager: dep.package_manager
66
- )
67
- dependencies[updated_dependency.name.downcase] = updated_dependency
68
- # Add peer dependencies to the list of updated dependencies.
69
- process_updated_peer_dependencies(updated_dependency, dependencies)
70
- end
75
+ @updated_dependencies ||= T.let(
76
+ begin
77
+ dependencies = T.let({}, T::Hash[String, Dependabot::Dependency])
78
+
79
+ dependencies_using_property.each do |dep|
80
+ # Only keep one copy of each dependency, the one with the highest target version.
81
+ visited_dependency = dependencies[dep.name.downcase]
82
+ next unless visited_dependency.nil? || T.must(visited_dependency.numeric_version) < target_version
83
+
84
+ updated_dependency = Dependency.new(
85
+ name: dep.name,
86
+ version: target_version.to_s,
87
+ requirements: updated_requirements(dep),
88
+ previous_version: dep.version,
89
+ previous_requirements: dep.requirements,
90
+ package_manager: dep.package_manager
91
+ )
92
+ dependencies[updated_dependency.name.downcase] = updated_dependency
93
+ # Add peer dependencies to the list of updated dependencies.
94
+ process_updated_peer_dependencies(updated_dependency, dependencies)
95
+ end
71
96
 
72
- dependencies.map { |_, dependency| dependency }
73
- end
97
+ dependencies.map { |_, dependency| dependency }
98
+ end,
99
+ T.nilable(T::Array[Dependabot::Dependency])
100
+ )
74
101
  end
75
102
 
76
103
  private
77
104
 
78
- attr_reader :dependency, :dependency_files, :target_version,
79
- :source_details, :credentials, :ignored_versions, :repo_contents_path
105
+ sig { returns(Dependabot::Dependency) }
106
+ attr_reader :dependency
107
+
108
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
109
+ attr_reader :dependency_files
110
+
111
+ sig { returns(T.nilable(T.any(String, Dependabot::Nuget::Version))) }
112
+ attr_reader :target_version
80
113
 
114
+ sig { returns(T.nilable(T::Hash[Symbol, String])) }
115
+ attr_reader :source_details
116
+
117
+ sig { returns(T::Array[Dependabot::Credential]) }
118
+ attr_reader :credentials
119
+
120
+ sig { returns(T::Array[String]) }
121
+ attr_reader :ignored_versions
122
+
123
+ sig { returns(T.nilable(String)) }
124
+ attr_reader :repo_contents_path
125
+
126
+ sig do
127
+ params(
128
+ dependency: Dependabot::Dependency,
129
+ dependencies: T::Hash[String, Dependabot::Dependency]
130
+ )
131
+ .returns(T::Array[Dependabot::Dependency])
132
+ end
81
133
  def process_updated_peer_dependencies(dependency, dependencies)
82
134
  DependencyFinder.new(
83
135
  dependency: dependency,
@@ -87,36 +139,48 @@ module Dependabot
87
139
  ).updated_peer_dependencies.each do |peer_dependency|
88
140
  # Only keep one copy of each dependency, the one with the highest target version.
89
141
  visited_dependency = dependencies[peer_dependency.name.downcase]
90
- next unless visited_dependency.nil? || visited_dependency.numeric_version < peer_dependency.numeric_version
142
+ unless visited_dependency.nil? ||
143
+ T.must(visited_dependency.numeric_version) < peer_dependency.numeric_version
144
+ next
145
+ end
91
146
 
92
147
  dependencies[peer_dependency.name.downcase] = peer_dependency
93
148
  end
94
149
  end
95
150
 
151
+ sig { returns(T::Array[Dependabot::Dependency]) }
96
152
  def dependencies_using_property
97
153
  @dependencies_using_property ||=
98
- Nuget::FileParser.new(
99
- dependency_files: dependency_files,
100
- source: nil
101
- ).parse.select do |dep|
102
- dep.requirements.any? do |r|
103
- r.dig(:metadata, :property_name) == property_name
104
- end
105
- end
154
+ T.let(
155
+ Nuget::FileParser.new(
156
+ dependency_files: dependency_files,
157
+ source: nil
158
+ ).parse.select do |dep|
159
+ dep.requirements.any? do |r|
160
+ r.dig(:metadata, :property_name) == property_name
161
+ end
162
+ end,
163
+ T.nilable(T::Array[Dependabot::Dependency])
164
+ )
106
165
  end
107
166
 
167
+ sig { returns(String) }
108
168
  def property_name
109
- @property_name ||= dependency.requirements
110
- .find { |r| r.dig(:metadata, :property_name) }
111
- &.dig(:metadata, :property_name)
169
+ @property_name ||= T.let(
170
+ dependency.requirements
171
+ .find { |r| r.dig(:metadata, :property_name) }
172
+ &.dig(:metadata, :property_name),
173
+ T.nilable(String)
174
+ )
112
175
 
113
176
  raise "No requirement with a property name!" unless @property_name
114
177
 
115
178
  @property_name
116
179
  end
117
180
 
181
+ sig { params(dep: Dependabot::Dependency).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
118
182
  def updated_requirements(dep)
119
- @updated_requirements ||= {}
183
+ @updated_requirements ||= T.let({}, T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, T.untyped]]]))
120
184
  @updated_requirements[dep.name] ||=
121
185
  RequirementsUpdater.new(
122
186
  requirements: dep.requirements,
@@ -1,8 +1,10 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "excon"
5
5
  require "nokogiri"
6
+ require "sorbet-runtime"
7
+
6
8
  require "dependabot/errors"
7
9
  require "dependabot/update_checkers/base"
8
10
  require "dependabot/registry_client"
@@ -12,23 +14,34 @@ require "dependabot/nuget/http_response_helpers"
12
14
  module Dependabot
13
15
  module Nuget
14
16
  class RepositoryFinder
17
+ extend T::Sig
18
+
15
19
  DEFAULT_REPOSITORY_URL = "https://api.nuget.org/v3/index.json"
16
20
  DEFAULT_REPOSITORY_API_KEY = "nuget.org"
17
21
 
22
+ sig do
23
+ params(
24
+ dependency: Dependabot::Dependency,
25
+ credentials: T::Array[Dependabot::Credential],
26
+ config_files: T::Array[Dependabot::DependencyFile]
27
+ ).void
28
+ end
18
29
  def initialize(dependency:, credentials:, config_files: [])
19
30
  @dependency = dependency
20
31
  @credentials = credentials
21
32
  @config_files = config_files
22
33
  end
23
34
 
35
+ sig { returns(T::Array[T::Hash[Symbol, String]]) }
24
36
  def dependency_urls
25
37
  find_dependency_urls
26
38
  end
27
39
 
40
+ sig { returns(T::Array[T::Hash[Symbol, String]]) }
28
41
  def known_repositories
29
42
  return @known_repositories if @known_repositories
30
43
 
31
- @known_repositories = []
44
+ @known_repositories ||= T.let([], T.nilable(T::Array[T::Hash[Symbol, String]]))
32
45
  @known_repositories += credential_repositories
33
46
  @known_repositories += config_file_repositories
34
47
 
@@ -40,6 +53,7 @@ module Dependabot
40
53
  @known_repositories.uniq
41
54
  end
42
55
 
56
+ sig { params(dependency_name: String).returns(T::Hash[Symbol, T.untyped]) }
43
57
  def self.get_default_repository_details(dependency_name)
44
58
  {
45
59
  base_url: "https://api.nuget.org/v3-flatcontainer/",
@@ -56,21 +70,33 @@ module Dependabot
56
70
 
57
71
  private
58
72
 
59
- attr_reader :dependency, :credentials, :config_files
73
+ sig { returns(Dependabot::Dependency) }
74
+ attr_reader :dependency
75
+
76
+ sig { returns(T::Array[Dependabot::Credential]) }
77
+ attr_reader :credentials
78
+
79
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
80
+ attr_reader :config_files
60
81
 
82
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
61
83
  def find_dependency_urls
62
84
  @find_dependency_urls ||=
63
- known_repositories.flat_map do |details|
64
- if details.fetch(:url) == DEFAULT_REPOSITORY_URL
65
- # Save a request for the default URL, since we already know how
66
- # it addresses packages
67
- next default_repository_details
68
- end
85
+ T.let(
86
+ known_repositories.flat_map do |details|
87
+ if details.fetch(:url) == DEFAULT_REPOSITORY_URL
88
+ # Save a request for the default URL, since we already know how
89
+ # it addresses packages
90
+ next default_repository_details
91
+ end
69
92
 
70
- build_url_for_details(details)
71
- end.compact.uniq
93
+ build_url_for_details(details)
94
+ end.compact.uniq,
95
+ T.nilable(T::Array[T::Hash[Symbol, T.untyped]])
96
+ )
72
97
  end
73
98
 
99
+ sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
74
100
  def build_url_for_details(repo_details)
75
101
  url = repo_details.fetch(:url)
76
102
  url_obj = URI.parse(url)
@@ -86,6 +112,7 @@ module Dependabot
86
112
  details
87
113
  end
88
114
 
115
+ sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
89
116
  def build_url_for_details_remote(repo_details)
90
117
  response = get_repo_metadata(repo_details)
91
118
  check_repo_response(response, repo_details)
@@ -118,11 +145,12 @@ module Dependabot
118
145
 
119
146
  details
120
147
  rescue JSON::ParserError
121
- build_v2_url(response, repo_details)
148
+ build_v2_url(T.must(response), repo_details)
122
149
  rescue Excon::Error::Timeout, Excon::Error::Socket
123
150
  handle_timeout(repo_metadata_url: repo_details.fetch(:url))
124
151
  end
125
152
 
153
+ sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(Excon::Response) }
126
154
  def get_repo_metadata(repo_details)
127
155
  url = repo_details.fetch(:url)
128
156
  cache = CacheManager.cache("repo_finder_metadatacache")
@@ -138,6 +166,7 @@ module Dependabot
138
166
  end
139
167
  end
140
168
 
169
+ sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) }
141
170
  def base_url_from_v3_metadata(metadata)
142
171
  metadata
143
172
  .fetch("resources", [])
@@ -145,6 +174,7 @@ module Dependabot
145
174
  &.fetch("@id")
146
175
  end
147
176
 
177
+ sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) }
148
178
  def registration_url_from_v3_metadata(metadata)
149
179
  allowed_registration_types = %w(
150
180
  RegistrationsBaseUrl
@@ -159,6 +189,7 @@ module Dependabot
159
189
  &.fetch("@id")
160
190
  end
161
191
 
192
+ sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) }
162
193
  def search_url_from_v3_metadata(metadata)
163
194
  # allowable values from here: https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#versioning
164
195
  allowed_search_types = %w(
@@ -173,6 +204,13 @@ module Dependabot
173
204
  &.fetch("@id")
174
205
  end
175
206
 
207
+ sig do
208
+ params(
209
+ response: Excon::Response,
210
+ repo_details: T::Hash[Symbol, T.untyped]
211
+ )
212
+ .returns(T::Hash[Symbol, T.untyped])
213
+ end
176
214
  def build_v2_url(response, repo_details)
177
215
  doc = Nokogiri::XML(response.body)
178
216
 
@@ -194,6 +232,7 @@ module Dependabot
194
232
  }
195
233
  end
196
234
 
235
+ sig { params(response: Excon::Response, details: T::Hash[Symbol, T.untyped]).void }
197
236
  def check_repo_response(response, details)
198
237
  return unless [401, 402, 403].include?(response.status)
199
238
  raise if details.fetch(:url) == DEFAULT_REPOSITORY_URL
@@ -201,19 +240,25 @@ module Dependabot
201
240
  raise PrivateSourceAuthenticationFailure, details.fetch(:url)
202
241
  end
203
242
 
243
+ sig { params(repo_metadata_url: String).returns(T.noreturn) }
204
244
  def handle_timeout(repo_metadata_url:)
205
245
  raise if repo_metadata_url == DEFAULT_REPOSITORY_URL
206
246
 
207
247
  raise PrivateSourceTimedOut, repo_metadata_url
208
248
  end
209
249
 
250
+ sig { returns(T::Array[T::Hash[Symbol, String]]) }
210
251
  def credential_repositories
211
252
  @credential_repositories ||=
212
- credentials
213
- .select { |cred| cred["type"] == "nuget_feed" && cred["url"] }
214
- .map { |c| { url: c.fetch("url"), token: c["token"] } }
253
+ T.let(
254
+ credentials
255
+ .select { |cred| cred["type"] == "nuget_feed" && cred["url"] }
256
+ .map { |c| { url: c.fetch("url"), token: c["token"] } },
257
+ T.nilable(T::Array[T::Hash[Symbol, String]])
258
+ )
215
259
  end
216
260
 
261
+ sig { returns(T::Array[T::Hash[Symbol, String]]) }
217
262
  def config_file_repositories
218
263
  config_files.flat_map { |file| repos_from_config_file(file) }
219
264
  end
@@ -222,13 +267,14 @@ module Dependabot
222
267
  # rubocop:disable Metrics/PerceivedComplexity
223
268
  # rubocop:disable Metrics/MethodLength
224
269
  # rubocop:disable Metrics/AbcSize
270
+ sig { params(config_file: Dependabot::DependencyFile).returns(T::Array[T::Hash[Symbol, String]]) }
225
271
  def repos_from_config_file(config_file)
226
272
  doc = Nokogiri::XML(config_file.content)
227
273
  doc.remove_namespaces!
228
274
  # analogous to having a root config with the default repository
229
275
  base_sources = [{ url: DEFAULT_REPOSITORY_URL, key: "nuget.org" }]
230
276
 
231
- sources = []
277
+ sources = T.let([], T::Array[T::Hash[Symbol, String]])
232
278
 
233
279
  # regular package sources
234
280
  doc.css("configuration > packageSources").children.each do |node|
@@ -269,6 +315,23 @@ module Dependabot
269
315
  known_urls.include?(s.fetch(:url))
270
316
  end
271
317
 
318
+ # filter out based on packageSourceMapping
319
+ package_mapping_elements = doc.xpath("/configuration/packageSourceMapping/packageSource/package[@pattern]")
320
+ matching_package_elements = package_mapping_elements.select do |package_element|
321
+ pattern = package_element.attribute("pattern").value
322
+
323
+ # reusing this function for a case insensitive GLOB pattern patch (e.g., "Microsoft.Azure.*")
324
+ File.fnmatch(pattern, @dependency.name, File::FNM_CASEFOLD)
325
+ end
326
+ longest_matching_package_element = matching_package_elements.max_by do |package_element|
327
+ package_element.attribute("pattern").value.length
328
+ end
329
+ matching_key = longest_matching_package_element&.parent&.attribute("key")&.value
330
+ if matching_key
331
+ # found a matching source, only keep that one
332
+ sources.select! { |s| s.fetch(:key) == matching_key }
333
+ end
334
+
272
335
  add_config_file_credentials(sources: sources, doc: doc)
273
336
  sources.each { |details| details.delete(:key) }
274
337
 
@@ -279,11 +342,13 @@ module Dependabot
279
342
  # rubocop:enable Metrics/PerceivedComplexity
280
343
  # rubocop:enable Metrics/CyclomaticComplexity
281
344
 
345
+ sig { returns(T::Hash[Symbol, T.untyped]) }
282
346
  def default_repository_details
283
347
  RepositoryFinder.get_default_repository_details(dependency.name)
284
348
  end
285
349
 
286
350
  # rubocop:disable Metrics/PerceivedComplexity
351
+ sig { params(doc: Nokogiri::XML::Document).returns(T::Array[String]) }
287
352
  def disabled_sources(doc)
288
353
  doc.css("configuration > disabledPackageSources > add").filter_map do |node|
289
354
  value = node.attribute("value")&.value ||
@@ -298,6 +363,13 @@ module Dependabot
298
363
  # rubocop:enable Metrics/PerceivedComplexity
299
364
 
300
365
  # rubocop:disable Metrics/PerceivedComplexity
366
+ sig do
367
+ params(
368
+ sources: T::Array[T::Hash[Symbol, T.nilable(String)]],
369
+ doc: Nokogiri::XML::Document
370
+ )
371
+ .void
372
+ end
301
373
  def add_config_file_credentials(sources:, doc:)
302
374
  sources.each do |source_details|
303
375
  key = source_details.fetch(:key)
@@ -329,11 +401,10 @@ module Dependabot
329
401
  # Any non-ascii characters in the tag with cause a syntax error
330
402
  next source_details[:token] = nil
331
403
  end
332
-
333
- sources
334
404
  end
335
405
  # rubocop:enable Metrics/PerceivedComplexity
336
406
 
407
+ sig { params(string: String).returns(String) }
337
408
  def expand_windows_style_environment_variables(string)
338
409
  # NuGet.Config files can have Windows-style environment variables that need to be replaced
339
410
  # https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#using-environment-variables
@@ -352,6 +423,7 @@ module Dependabot
352
423
  end
353
424
  end
354
425
 
426
+ sig { params(token: T.nilable(String)).returns(T::Hash[String, String]) }
355
427
  def auth_header_for_token(token)
356
428
  return {} unless token
357
429
 
@@ -1,6 +1,8 @@
1
- # typed: true
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
4
6
  require "dependabot/update_checkers/base"
5
7
  require "dependabot/nuget/version"
6
8
  require "dependabot/nuget/requirement"
@@ -10,19 +12,22 @@ require "dependabot/shared_helpers"
10
12
  module Dependabot
11
13
  module Nuget
12
14
  class TfmComparer
15
+ extend T::Sig
16
+
17
+ sig { params(project_tfms: T::Array[String], package_tfms: T::Array[String]).returns(T::Boolean) }
13
18
  def self.are_frameworks_compatible?(project_tfms, package_tfms)
14
19
  return false if package_tfms.empty?
15
20
  return false if project_tfms.empty?
16
21
 
17
22
  key = "project_ftms:#{project_tfms.sort.join(',')}:package_tfms:#{package_tfms.sort.join(',')}".downcase
18
23
 
19
- @cached_framework_check ||= {}
24
+ @cached_framework_check ||= T.let({}, T.nilable(T::Hash[String, T::Boolean]))
20
25
  unless @cached_framework_check.key?(key)
21
26
  @cached_framework_check[key] =
22
27
  NativeHelpers.run_nuget_framework_check(project_tfms,
23
28
  package_tfms)
24
29
  end
25
- @cached_framework_check[key]
30
+ T.must(@cached_framework_check[key])
26
31
  end
27
32
  end
28
33
  end