dependabot-cargo 0.384.0 → 0.386.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce6cbf95fb2a0787ae301e7c335ee65cc944996b5219eaba62874cdff315ffe4
4
- data.tar.gz: 7b359e82973b40e2fc92e2c53e8c7eb9928ba1bded71a1e283849b4d91f7ffc5
3
+ metadata.gz: 342398b3a35a615f1b23ae103f2a61e3c11ab24fb92b2eb3f6227b68cdae6600
4
+ data.tar.gz: a087cc721a765e9b8fd424a6972e1f110895947e36048906832a6f7acc59baed
5
5
  SHA512:
6
- metadata.gz: cee18fdefcd2ec3d06a882b08ab5f19da946e83eb38133e3a94273078baf2a460514a8e7e65281ae4d193a3643920e44f23eca9b3b4cddfef6a66ca174b730d1
7
- data.tar.gz: 1dbadf2bbad402e5862d30e81e89e4add62dce33e7e59661d9330d55b49a8b390db34a96b26c205a346e7a56e3ff643c1ed2095714033904c5dde157a176ab54
6
+ metadata.gz: 236e947dcc2a2a2674432ac934826740dd42591dd0568211059e11bc79d5f3875c70f63aedab2f03f5351170e3d2a9dd7537385e2c3fd240f5c02621a4624973
7
+ data.tar.gz: affa0255ca4605212a1a233cc636893532aa5b5743d94fd4a1fb1d75e70bc52ce36fb245c92cd19e5c13787906702137be32492cbc5b08327bb19875ff7ecfe0
@@ -28,7 +28,7 @@ module Dependabot
28
28
  "Repo must contain a Cargo.toml."
29
29
  end
30
30
 
31
- sig { override.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
31
+ sig { override.returns(T.nilable(T::Hash[Symbol, T.anything])) }
32
32
  def ecosystem_versions
33
33
  channel = if rust_toolchain
34
34
  TomlRB.parse(T.must(rust_toolchain).content).dig("toolchain", "channel")
@@ -47,7 +47,7 @@ module Dependabot
47
47
  def fetch_files
48
48
  fetched_files = T.let([cargo_toml], T::Array[DependencyFile])
49
49
  fetched_files << T.must(cargo_lock) if cargo_lock
50
- fetched_files << T.must(cargo_config) if cargo_config
50
+ fetched_files.concat(cargo_configs)
51
51
  fetched_files << T.must(rust_toolchain) if rust_toolchain
52
52
  fetched_files += fetch_path_dependency_and_workspace_files
53
53
  parsed_manifest = parsed_file(cargo_toml)
@@ -180,12 +180,14 @@ module Dependabot
180
180
  find_workspace_root(file)
181
181
  end
182
182
 
183
- sig { params(dependencies: T::Hash[T.untyped, T.untyped]).returns(T::Array[String]) }
183
+ sig { params(dependencies: T::Hash[String, T.anything]).returns(T::Array[String]) }
184
184
  def collect_path_dependencies_paths(dependencies)
185
185
  dependencies.filter_map do |_, details|
186
- next unless details.is_a?(Hash) && details["path"]
186
+ details = toml_table(details)
187
+ path = T.cast(details&.fetch("path", nil), T.nilable(String))
188
+ next unless path
187
189
 
188
- File.join(details["path"], "Cargo.toml").delete_prefix("/")
190
+ File.join(path, "Cargo.toml").delete_prefix("/")
189
191
  end
190
192
  end
191
193
 
@@ -194,18 +196,19 @@ module Dependabot
194
196
  def path_dependency_paths_from_file(file)
195
197
  paths = T.let([], T::Array[String])
196
198
 
197
- workspace = parsed_file(file).fetch("workspace", {})
199
+ workspace = toml_table_or_empty(parsed_file(file).fetch("workspace", {}))
198
200
  Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
199
201
  # Paths specified in dependency declaration
200
- paths += collect_path_dependencies_paths(parsed_file(file).fetch(type, {}))
202
+ paths += collect_path_dependencies_paths(toml_table_or_empty(parsed_file(file).fetch(type, {})))
201
203
  # Paths specified as workspace dependencies in workspace root
202
- paths += collect_path_dependencies_paths(workspace.fetch(type, {}))
204
+ paths += collect_path_dependencies_paths(toml_table_or_empty(workspace.fetch(type, {})))
203
205
  end
204
206
 
205
207
  # Paths specified for target-specific dependencies
206
- parsed_file(file).fetch("target", {}).each do |_, t_details|
208
+ toml_table_or_empty(parsed_file(file).fetch("target", {})).each do |_, t_details|
209
+ t_details = toml_table_or_empty(t_details)
207
210
  Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
208
- paths += collect_path_dependencies_paths(t_details.fetch(type, {}))
211
+ paths += collect_path_dependencies_paths(toml_table_or_empty(t_details.fetch(type, {})))
209
212
  end
210
213
  end
211
214
 
@@ -217,20 +220,25 @@ module Dependabot
217
220
  paths = []
218
221
 
219
222
  # Paths specified as replacements
220
- parsed_file(file).fetch("replace", {}).each do |_, details|
221
- next unless details.is_a?(Hash) && details["path"]
223
+ toml_table_or_empty(parsed_file(file).fetch("replace", {})).each do |_, details|
224
+ details = toml_table(details)
225
+ path = T.cast(details&.fetch("path", nil), T.nilable(String))
226
+ next unless path
222
227
 
223
- paths << File.join(details["path"], "Cargo.toml")
228
+ paths << File.join(path, "Cargo.toml")
224
229
  end
225
230
 
226
231
  # Paths specified as patches
227
- parsed_file(file).fetch("patch", {}).each do |_, details|
228
- next unless details.is_a?(Hash)
232
+ toml_table_or_empty(parsed_file(file).fetch("patch", {})).each do |_, details|
233
+ details = toml_table(details)
234
+ next unless details
229
235
 
230
236
  details.each do |_, dep_details|
231
- next unless dep_details.is_a?(Hash) && dep_details["path"]
237
+ dep_details = toml_table(dep_details)
238
+ path = T.cast(dep_details&.fetch("path", nil), T.nilable(String))
239
+ next unless path
232
240
 
233
- paths << File.join(dep_details["path"], "Cargo.toml")
241
+ paths << File.join(path, "Cargo.toml")
234
242
  end
235
243
  end
236
244
 
@@ -239,28 +247,31 @@ module Dependabot
239
247
 
240
248
  # Check if this Cargo manifest uses workspace dependencies
241
249
  # (e.g. dependency = { workspace = true }).
242
- sig { params(parsed_manifest: T::Hash[T.untyped, T.untyped]).returns(T::Boolean) }
250
+ sig { params(parsed_manifest: T::Hash[String, T.anything]).returns(T::Boolean) }
243
251
  def uses_workspace_dependencies?(parsed_manifest)
244
252
  # Check regular dependencies
245
253
  workspace_deps = Cargo::FileParser::DEPENDENCY_TYPES.any? do |type|
246
- deps = parsed_manifest.fetch(type, {})
254
+ deps = toml_table_or_empty(parsed_manifest.fetch(type, {}))
247
255
  deps.any? do |_, details|
248
- next false unless details.is_a?(Hash)
256
+ details = toml_table(details)
257
+ next false unless details
249
258
 
250
- details["workspace"] == true
259
+ T.cast(details["workspace"], T.nilable(Object)) == true
251
260
  end
252
261
  end
253
262
 
254
263
  return true if workspace_deps
255
264
 
256
265
  # Check target-specific dependencies
257
- parsed_manifest.fetch("target", {}).any? do |_, target_details|
266
+ toml_table_or_empty(parsed_manifest.fetch("target", {})).any? do |_, target_details|
267
+ target_details = toml_table_or_empty(target_details)
258
268
  Cargo::FileParser::DEPENDENCY_TYPES.any? do |type|
259
- deps = target_details.fetch(type, {})
269
+ deps = toml_table_or_empty(target_details.fetch(type, {}))
260
270
  deps.any? do |_, details|
261
- next false unless details.is_a?(Hash)
271
+ details = toml_table(details)
272
+ next false unless details
262
273
 
263
- details["workspace"] == true
274
+ T.cast(details["workspace"], T.nilable(Object)) == true
264
275
  end
265
276
  end
266
277
  end
@@ -268,13 +279,14 @@ module Dependabot
268
279
 
269
280
  # See if this Cargo manifest inherits any property from a workspace
270
281
  # (e.g. edition = { workspace = true }).
271
- sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Boolean) }
282
+ sig { params(hash: T::Hash[String, T.anything]).returns(T::Boolean) }
272
283
  def workspace_member?(hash)
273
284
  hash.each do |key, value|
285
+ value = T.cast(value, T.nilable(Object))
274
286
  if key == "workspace" && value == true
275
287
  return true
276
288
  elsif value.is_a?(Hash)
277
- return workspace_member?(value)
289
+ return workspace_member?(toml_table_or_empty(value))
278
290
  end
279
291
  end
280
292
  false
@@ -289,7 +301,8 @@ module Dependabot
289
301
  def find_workspace_root(workspace_member)
290
302
  current_dir = workspace_member.name.rpartition("/").first
291
303
 
292
- workspace_root_dir = parsed_file(workspace_member).dig("package", "workspace")
304
+ package = toml_table_or_empty(parsed_file(workspace_member)["package"])
305
+ workspace_root_dir = T.cast(package["workspace"], T.nilable(String))
293
306
  unless workspace_root_dir.nil?
294
307
  workspace_root = fetch_file_from_host(
295
308
  File.join(current_dir, workspace_root_dir, "Cargo.toml"),
@@ -322,11 +335,10 @@ module Dependabot
322
335
 
323
336
  sig { params(file: Dependabot::DependencyFile).returns(T::Array[String]) }
324
337
  def workspace_dependency_paths_from_file(file)
325
- if parsed_file(file)["workspace"] && !parsed_file(file)["workspace"].key?("members")
326
- return path_dependency_paths_from_file(file)
327
- end
338
+ workspace = toml_table(parsed_file(file)["workspace"])
339
+ return path_dependency_paths_from_file(file) if workspace && !workspace.key?("members")
328
340
 
329
- workspace_paths = parsed_file(file).dig("workspace", "members")
341
+ workspace_paths = T.cast(workspace&.fetch("members", nil), T.nilable(T::Array[String]))
330
342
  return [] unless workspace_paths&.any?
331
343
 
332
344
  # Expand any workspace paths that specify a `*`
@@ -335,9 +347,7 @@ module Dependabot
335
347
  end
336
348
 
337
349
  # Excluded paths, to be subtracted for the workspaces array
338
- excluded_paths =
339
- (parsed_file(file).dig("workspace", "excluded_paths") || []) +
340
- (parsed_file(file).dig("workspace", "exclude") || [])
350
+ excluded_paths = workspace_excluded_paths(workspace)
341
351
 
342
352
  (workspace_paths - excluded_paths).map do |path|
343
353
  File.join(path, "Cargo.toml")
@@ -346,59 +356,40 @@ module Dependabot
346
356
 
347
357
  # Check whether a path is required or not. It will not be required if
348
358
  # an alternative source (i.e., a git source) is also specified
349
- # rubocop:disable Metrics/CyclomaticComplexity
350
359
  # rubocop:disable Metrics/PerceivedComplexity
351
- # rubocop:disable Metrics/AbcSize
352
360
  sig { params(file: Dependabot::DependencyFile, path: String).returns(T::Boolean) }
353
361
  def required_path?(file, path)
354
362
  # Paths specified in dependency declaration
355
363
  Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
356
- parsed_file(file).fetch(type, {}).each do |_, details|
357
- next unless details.is_a?(Hash)
358
- next unless details["path"]
359
- next unless path == File.join(details["path"], "Cargo.toml")
360
-
361
- return true if details["git"].nil?
364
+ toml_table_or_empty(parsed_file(file).fetch(type, {})).each do |_, details|
365
+ return true if required_dependency_details?(details, path)
362
366
  end
363
367
  end
364
368
 
365
369
  # Paths specified for target-specific dependencies
366
- parsed_file(file).fetch("target", {}).each do |_, t_details|
370
+ toml_table_or_empty(parsed_file(file).fetch("target", {})).each do |_, t_details|
371
+ t_details = toml_table_or_empty(t_details)
367
372
  Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
368
- t_details.fetch(type, {}).each do |_, details|
369
- next unless details.is_a?(Hash)
370
- next unless details["path"]
371
- next unless path == File.join(details["path"], "Cargo.toml")
372
-
373
- return true if details["git"].nil?
373
+ toml_table_or_empty(t_details.fetch(type, {})).each do |_, details|
374
+ return true if required_dependency_details?(details, path)
374
375
  end
375
376
  end
376
377
  end
377
378
 
378
379
  # Paths specified for workspace-wide dependencies
379
- workspace = parsed_file(file).fetch("workspace", {})
380
- workspace.fetch("dependencies", {}).each do |_, details|
381
- next unless details.is_a?(Hash)
382
- next unless details["path"]
383
- next unless path == File.join(details["path"], "Cargo.toml")
384
-
385
- return true if details["git"].nil?
380
+ workspace = toml_table_or_empty(parsed_file(file).fetch("workspace", {}))
381
+ toml_table_or_empty(workspace.fetch("dependencies", {})).each do |_, details|
382
+ return true if required_dependency_details?(details, path)
386
383
  end
387
384
 
388
385
  # Paths specified as replacements
389
- parsed_file(file).fetch("replace", {}).each do |_, details|
390
- next unless details.is_a?(Hash)
391
- next unless details["path"]
392
- next unless path == File.join(details["path"], "Cargo.toml")
393
-
394
- return true if details["git"].nil?
386
+ toml_table_or_empty(parsed_file(file).fetch("replace", {})).each do |_, details|
387
+ return true if required_dependency_details?(details, path)
395
388
  end
396
389
 
397
390
  false
398
391
  end
399
- # rubocop:enable Metrics/AbcSize
400
392
  # rubocop:enable Metrics/PerceivedComplexity
401
- # rubocop:enable Metrics/CyclomaticComplexity
402
393
 
403
394
  sig { params(path: String).returns(T::Array[String]) }
404
395
  def expand_workspaces(path)
@@ -411,13 +402,44 @@ module Dependabot
411
402
  .select { |filename| File.fnmatch?(path, filename) }
412
403
  end
413
404
 
414
- sig { params(file: Dependabot::DependencyFile).returns(T::Hash[T.untyped, T.untyped]) }
405
+ sig { params(file: Dependabot::DependencyFile).returns(T::Hash[String, T.anything]) }
415
406
  def parsed_file(file)
416
407
  TomlRB.parse(file.content)
417
408
  rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
418
409
  raise Dependabot::DependencyFileNotParseable, file.path
419
410
  end
420
411
 
412
+ sig { params(workspace: T.nilable(T::Hash[String, T.anything])).returns(T::Array[String]) }
413
+ def workspace_excluded_paths(workspace)
414
+ (T.cast(workspace&.fetch("excluded_paths", nil), T.nilable(T::Array[String])) || []) +
415
+ (T.cast(workspace&.fetch("exclude", nil), T.nilable(T::Array[String])) || [])
416
+ end
417
+
418
+ sig { params(details: T.anything, path: String).returns(T::Boolean) }
419
+ def required_dependency_details?(details, path)
420
+ details = toml_table(details)
421
+ dependency_path = T.cast(details&.fetch("path", nil), T.nilable(String))
422
+ return false unless details && dependency_path
423
+ return false unless path == File.join(dependency_path, "Cargo.toml")
424
+
425
+ T.cast(details["git"], T.nilable(Object)).nil?
426
+ end
427
+
428
+ # Returns the value as a TOML table (Hash) when it is one, otherwise nil.
429
+ # TOML dependency values are polymorphic (e.g. `foo = "1.0"` is a String
430
+ # while `foo = { version = "1.0" }` is a table), so callers must narrow at
431
+ # runtime rather than assert a type with T.cast (which would raise).
432
+ sig { params(value: T.anything).returns(T.nilable(T::Hash[String, T.anything])) }
433
+ def toml_table(value)
434
+ obj = T.cast(value, T.nilable(Object))
435
+ obj.is_a?(Hash) ? obj : nil
436
+ end
437
+
438
+ sig { params(value: T.anything).returns(T::Hash[String, T.anything]) }
439
+ def toml_table_or_empty(value)
440
+ toml_table(value) || {}
441
+ end
442
+
421
443
  sig { returns(Dependabot::DependencyFile) }
422
444
  def cargo_toml
423
445
  @cargo_toml ||= T.let(fetch_file_from_host("Cargo.toml"), T.nilable(Dependabot::DependencyFile))
@@ -428,19 +450,53 @@ module Dependabot
428
450
  @cargo_lock ||= T.let(fetch_file_if_present("Cargo.lock"), T.nilable(Dependabot::DependencyFile))
429
451
  end
430
452
 
453
+ # Cargo merges configuration hierarchically: it reads `.cargo/config.toml`
454
+ # from the package directory *and* every ancestor directory up to the repo
455
+ # root, combining them. We therefore collect all of them so that registries
456
+ # (and other settings) defined higher up the tree are not dropped.
457
+ #
458
+ # The package-directory config keeps the canonical name `.cargo/config.toml`
459
+ # (single-config consumers rely on this). Ancestor configs keep their
460
+ # relative `../` paths so they are written to the correct location and can
461
+ # be merged by Cargo. When the package directory has no config of its own,
462
+ # the nearest ancestor is promoted to the canonical name to preserve the
463
+ # historical behaviour that downstream consumers depend on.
464
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
465
+ def cargo_configs
466
+ return T.must(@cargo_configs) if defined?(@cargo_configs)
467
+
468
+ configs = T.let([], T::Array[Dependabot::DependencyFile])
469
+ local = local_cargo_config
470
+ parents = parent_dir_cargo_configs
471
+
472
+ if local
473
+ configs << local
474
+ configs.concat(parents)
475
+ elsif parents.any?
476
+ effective = T.must(parents.first)
477
+ effective.name = ".cargo/config.toml"
478
+ configs << effective
479
+ configs.concat(parents.drop(1))
480
+ end
481
+
482
+ @cargo_configs = T.let(configs, T.nilable(T::Array[Dependabot::DependencyFile]))
483
+ configs
484
+ end
485
+
431
486
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
432
487
  def cargo_config
433
- return @cargo_config if defined?(@cargo_config)
488
+ cargo_configs.find { |f| f.name == ".cargo/config.toml" }
489
+ end
434
490
 
435
- @cargo_config = fetch_support_file(".cargo/config.toml")
436
- @cargo_config ||= T.let(
491
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
492
+ def local_cargo_config
493
+ return @local_cargo_config if defined?(@local_cargo_config)
494
+
495
+ @local_cargo_config = fetch_support_file(".cargo/config.toml")
496
+ @local_cargo_config ||= T.let(
437
497
  fetch_support_file(".cargo/config")&.tap { |f| f.name = ".cargo/config.toml" },
438
498
  T.nilable(Dependabot::DependencyFile)
439
499
  )
440
- @cargo_config ||= T.let(
441
- fetch_cargo_config_from_parent_dirs,
442
- T.nilable(Dependabot::DependencyFile)
443
- )
444
500
  end
445
501
 
446
502
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
@@ -470,22 +526,26 @@ module Dependabot
470
526
  super.tap { |f| f.name = Pathname.new(f.name).cleanpath.to_s.gsub(%r{^/+}, "") }
471
527
  end
472
528
 
473
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
474
- def fetch_cargo_config_from_parent_dirs
475
- return nil if directory.empty?
529
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
530
+ def parent_dir_cargo_configs
531
+ return T.must(@parent_dir_cargo_configs) if defined?(@parent_dir_cargo_configs)
532
+
533
+ configs = T.let([], T::Array[Dependabot::DependencyFile])
534
+ @parent_dir_cargo_configs = T.let(configs, T.nilable(T::Array[Dependabot::DependencyFile]))
535
+ return configs if directory.empty?
476
536
 
477
537
  # Count directory depth to determine how many levels to search up
478
538
  depth = directory.split("/").count { |s| !s.empty? }
479
- return nil if depth.zero?
539
+ return configs if depth.zero?
480
540
 
481
- # Try each parent directory level
541
+ # Collect a config from every ancestor directory that has one, nearest first
482
542
  depth.times do |i|
483
543
  parent_path = ([".."] * (i + 1)).join("/")
484
544
  config = try_fetch_config_at_path(parent_path)
485
- return config if config
545
+ configs << config if config
486
546
  end
487
547
 
488
- nil
548
+ configs
489
549
  end
490
550
 
491
551
  sig { params(parent_path: String).returns(T.nilable(Dependabot::DependencyFile)) }
@@ -499,7 +559,9 @@ module Dependabot
499
559
  )
500
560
  Dependabot.logger.debug("Successfully fetched config from: #{full_path}")
501
561
  config.support_file = true
502
- config.name = ".cargo/config.toml"
562
+ # Normalise `.cargo/config` to `.cargo/config.toml` while preserving the
563
+ # relative `../` path so the file is written to the right location.
564
+ config.name = Pathname.new(File.join(parent_path, ".cargo/config.toml")).cleanpath.to_s
503
565
  return config
504
566
  rescue Dependabot::DependencyFileNotFound
505
567
  Dependabot.logger.debug("No config found at: #{full_path}")
@@ -105,9 +105,8 @@ module Dependabot
105
105
  def check_rust_workspace_root
106
106
  cargo_toml = dependency_files.find { |f| f.name == "Cargo.toml" }
107
107
  workspace_root = parsed_file(T.must(cargo_toml))
108
- return unless workspace_root.is_a?(Hash)
109
108
 
110
- workspace_config = workspace_root.dig("package", "workspace")
109
+ workspace_config = T.cast(toml_table_or_empty(workspace_root["package"])["workspace"], T.nilable(String))
111
110
  return unless workspace_config
112
111
 
113
112
  msg = "This project is part of a Rust workspace but is not the " \
@@ -129,10 +128,10 @@ module Dependabot
129
128
 
130
129
  manifest_files.each do |file|
131
130
  parsed_content = parsed_file(file)
132
- next unless parsed_content.is_a?(Hash)
133
131
 
134
132
  DEPENDENCY_TYPES.each do |type|
135
- parsed_content.fetch(type, {}).each do |name, requirement|
133
+ toml_table_or_empty(parsed_content.fetch(type, {})).each do |name, requirement|
134
+ requirement = dependency_declaration(requirement)
136
135
  # Skip workspace-inherited dependencies (similar to pnpm catalog)
137
136
  # Only skip if workspace is exactly boolean true
138
137
  next if requirement.is_a?(Hash) && requirement["workspace"] == true
@@ -143,8 +142,10 @@ module Dependabot
143
142
  dependency_set << build_dependency(name, requirement, type, file)
144
143
  end
145
144
 
146
- parsed_content.fetch("target", {}).each do |_, t_details|
147
- t_details.fetch(type, {}).each do |name, requirement|
145
+ toml_table_or_empty(parsed_content.fetch("target", {})).each do |_, t_details|
146
+ t_details = toml_table_or_empty(t_details)
147
+ toml_table_or_empty(t_details.fetch(type, {})).each do |name, requirement|
148
+ requirement = dependency_declaration(requirement)
148
149
  # Skip workspace-inherited dependencies
149
150
  # Only skip if workspace is exactly boolean true
150
151
  next if requirement.is_a?(Hash) && requirement["workspace"] == true
@@ -158,8 +159,9 @@ module Dependabot
158
159
  end
159
160
  end
160
161
 
161
- workspace = parsed_content.fetch("workspace", {})
162
- workspace.fetch("dependencies", {}).each do |name, requirement|
162
+ workspace = toml_table_or_empty(parsed_content.fetch("workspace", {}))
163
+ toml_table_or_empty(workspace.fetch("dependencies", {})).each do |name, requirement|
164
+ requirement = dependency_declaration(requirement)
163
165
  next unless name == name_from_declaration(name, requirement)
164
166
  next if lockfile && !version_from_lockfile(name, requirement)
165
167
 
@@ -202,15 +204,14 @@ module Dependabot
202
204
  return dependency_set unless lockfile
203
205
 
204
206
  lockfile_content = parsed_file(T.must(lockfile))
205
- return dependency_set unless lockfile_content.is_a?(Hash)
206
207
 
207
- lockfile_content.fetch("package", []).each do |package_details|
208
- next unless package_details["source"]
208
+ T.cast(lockfile_content.fetch("package", []), T::Array[T::Hash[String, T.anything]]).each do |package_details|
209
+ next unless T.cast(package_details["source"], T.nilable(String))
209
210
 
210
211
  # TODO: This isn't quite right, as it will only give us one
211
212
  # version of each dependency (when in fact there are many)
212
213
  dependency_set << Dependency.new(
213
- name: package_details["name"],
214
+ name: T.cast(package_details["name"], String),
214
215
  version: version_from_lockfile_details(package_details),
215
216
  package_manager: "cargo",
216
217
  requirements: []
@@ -224,10 +225,9 @@ module Dependabot
224
225
  def patched_dependencies
225
226
  root_manifest = manifest_files.find { |f| f.name == "Cargo.toml" }
226
227
  parsed_content = parsed_file(T.must(root_manifest))
227
- return [] unless parsed_content.is_a?(Hash)
228
228
  return [] unless parsed_content["patch"]
229
229
 
230
- parsed_content["patch"].values.flat_map(&:keys)
230
+ toml_table_or_empty(parsed_content["patch"]).values.flat_map { |patch| toml_table_or_empty(patch).keys }
231
231
  end
232
232
 
233
233
  sig { params(declaration: T.any(String, T::Hash[String, String])).returns(T.nilable(String)) }
@@ -332,10 +332,18 @@ module Dependabot
332
332
 
333
333
  sig { params(key_name: String).returns(T.nilable(String)) }
334
334
  def cargo_config_from_file(key_name)
335
- config_file = cargo_config
336
- return nil unless config_file
335
+ # Cargo merges `.cargo/config.toml` hierarchically, with nearer configs
336
+ # taking precedence over ancestors. Search the package-directory config
337
+ # first, then each ancestor config, returning the first match.
338
+ cargo_config_files.each do |config_file|
339
+ value = key_name.split(".").reduce(parsed_file(config_file)) do |current, key|
340
+ toml_table_or_empty(current)[key]
341
+ end
342
+ value = T.cast(value, T.nilable(String))
343
+ return value if value
344
+ end
337
345
 
338
- parsed_file(config_file).dig(*key_name.split("."))
346
+ nil
339
347
  end
340
348
 
341
349
  sig { params(name: String, declaration: T.any(String, T::Hash[String, String])).returns(T.nilable(String)) }
@@ -343,29 +351,28 @@ module Dependabot
343
351
  return unless lockfile
344
352
 
345
353
  lockfile_content = parsed_file(T.must(lockfile))
346
- return unless lockfile_content.is_a?(Hash)
347
354
 
348
355
  candidate_packages =
349
- lockfile_content.fetch("package", [])
350
- .select { |p| p["name"] == name }
356
+ lockfile_packages(lockfile_content)
357
+ .select { |p| package_field(p, "name") == name }
351
358
 
352
359
  if (req = requirement_from_declaration(declaration))
353
360
  req = Cargo::Requirement.new(req)
354
361
 
355
362
  candidate_packages =
356
363
  candidate_packages
357
- .select { |p| req.satisfied_by?(version_class.new(p["version"])) }
364
+ .select { |p| req.satisfied_by?(version_class.new(T.must(package_field(p, "version")))) }
358
365
  end
359
366
 
360
367
  candidate_packages =
361
368
  candidate_packages
362
369
  .select do |p|
363
- git_req?(declaration) ^ !p["source"]&.start_with?("git+")
370
+ git_req?(declaration) ^ !package_field(p, "source")&.start_with?("git+")
364
371
  end
365
372
 
366
373
  package =
367
374
  candidate_packages
368
- .max_by { |p| version_class.new(p["version"]) }
375
+ .max_by { |p| version_class.new(T.must(package_field(p, "version"))) }
369
376
 
370
377
  return unless package
371
378
 
@@ -387,11 +394,13 @@ module Dependabot
387
394
  }
388
395
  end
389
396
 
390
- sig { params(package_details: T::Hash[String, String]).returns(String) }
397
+ sig { params(package_details: T::Hash[String, T.anything]).returns(String) }
391
398
  def version_from_lockfile_details(package_details)
392
- return T.must(package_details["version"]) unless package_details["source"]&.start_with?("git+")
399
+ source = T.cast(package_details["source"], T.nilable(String))
400
+ version = T.cast(package_details["version"], String)
401
+ return version unless source&.start_with?("git+")
393
402
 
394
- T.must(T.must(package_details["source"]).split("#").last)
403
+ T.must(source.split("#").last)
395
404
  end
396
405
 
397
406
  sig { override.void }
@@ -399,14 +408,36 @@ module Dependabot
399
408
  raise "No Cargo.toml!" unless get_original_file("Cargo.toml")
400
409
  end
401
410
 
402
- sig { params(file: DependencyFile).returns(T.untyped) }
411
+ sig { params(file: DependencyFile).returns(T::Hash[String, T.anything]) }
403
412
  def parsed_file(file)
404
- @parsed_file ||= T.let({}, T.nilable(T::Hash[String, T.untyped]))
413
+ @parsed_file ||= T.let({}, T.nilable(T::Hash[String, T::Hash[String, T.anything]]))
405
414
  @parsed_file[file.name] ||= TomlRB.parse(file.content)
406
415
  rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
407
416
  raise Dependabot::DependencyFileNotParseable, file.path
408
417
  end
409
418
 
419
+ sig { params(lockfile_content: T::Hash[String, T.anything]).returns(T::Array[T::Hash[String, T.anything]]) }
420
+ def lockfile_packages(lockfile_content)
421
+ T.cast(lockfile_content.fetch("package", []), T::Array[T::Hash[String, T.anything]])
422
+ end
423
+
424
+ sig { params(package_details: T::Hash[String, T.anything], field: String).returns(T.nilable(String)) }
425
+ def package_field(package_details, field)
426
+ T.cast(package_details[field], T.nilable(String))
427
+ end
428
+
429
+ sig { params(value: T.anything).returns(T::Hash[String, T.anything]) }
430
+ def toml_table_or_empty(value)
431
+ (obj = T.cast(value, T.nilable(Object))).is_a?(Hash) ? obj : {}
432
+ end
433
+
434
+ sig { params(value: T.anything).returns(T.any(String, T::Hash[String, String])) }
435
+ def dependency_declaration(value)
436
+ return T.cast(value, String) if T.cast(value, T.nilable(Object)).is_a?(String)
437
+
438
+ T.cast(value, T::Hash[String, String])
439
+ end
440
+
410
441
  sig { returns(T::Array[Dependabot::DependencyFile]) }
411
442
  def manifest_files
412
443
  @manifest_files ||= T.let(
@@ -422,9 +453,18 @@ module Dependabot
422
453
  @lockfile ||= T.let(get_original_file("Cargo.lock"), T.nilable(Dependabot::DependencyFile))
423
454
  end
424
455
 
425
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
426
- def cargo_config
427
- @cargo_config ||= T.let(get_original_file(".cargo/config.toml"), T.nilable(Dependabot::DependencyFile))
456
+ # All `.cargo/config.toml` files in the fetched set, ordered nearest-first
457
+ # (package directory config, then successive ancestors). Ancestor configs
458
+ # carry relative `../` names, so we order by `../` depth to reflect Cargo's
459
+ # nearest-wins precedence.
460
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
461
+ def cargo_config_files
462
+ @cargo_config_files ||= T.let(
463
+ dependency_files
464
+ .select { |f| f.name.end_with?(".cargo/config.toml") }
465
+ .sort_by { |f| f.name.scan("../").count },
466
+ T.nilable(T::Array[Dependabot::DependencyFile])
467
+ )
428
468
  end
429
469
 
430
470
  sig { returns(T.class_of(Dependabot::Version)) }