dependabot-pub 0.398.0 → 0.399.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: d3f347c5026b0a38c8b5dcd1fe34d5bf086c5232a298d8e4ee53a2a2c77675ee
4
- data.tar.gz: c63bbc442dd89bd2e88381c1f8636783a96ea1e3ce4dcf8e1107182922e575c3
3
+ metadata.gz: bccb4cb21417d15087e804ed056b2ff00451cf1c908fd8cdfb40f7709fd02ce8
4
+ data.tar.gz: 685f8a0c5df291c2be68371b3c798343e61f23cd17ea5745efc879315dd3e178
5
5
  SHA512:
6
- metadata.gz: 5da945c408909041abb77106389039e7d6131086dc5516411d071a248779d9cdcdd1264680f6f768dac9bb54eb6730cdcfc857537b5bcee8233042a71f4ebae0
7
- data.tar.gz: 795b73580e3d48ddf10362fc1e89bc10f8dd964022434eacbeea5f3feb91cb7478ccf9d1edaa8d3d79e132a07e167358d7bae21d119b7e496290f2eec0176688
6
+ metadata.gz: af4a96a16577145fa0f6c3a9baf56c447f90dbade75678af5e8037faf926da725ecf3e4370dfae19e0f7f923c25aa984fb148aae146be7dacb361628809000a7
7
+ data.tar.gz: f615713050dddd07821622e4fd15a059b5c0470179d44236b901855c2db0feddc3fcbf89612ab681c71d4966f6ef5b355ccb73f777cc5d09411accb0188d609a
@@ -0,0 +1,167 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/pub/json_value_parser"
6
+ require "dependabot/shared_helpers"
7
+
8
+ module Dependabot
9
+ module Pub
10
+ module DependencyServicesResult
11
+ extend T::Sig
12
+
13
+ class ListedDependency < T::ImmutableStruct
14
+ const :name, String
15
+ const :version, String
16
+ const :kind, String
17
+ const :constraint, T.nilable(String)
18
+ const :source, T.nilable(JsonValueParser::ObjectHash)
19
+ end
20
+
21
+ class DependencyUpdate < T::ImmutableStruct
22
+ const :name, String
23
+ const :version, T.nilable(String)
24
+ const :kind, String
25
+ const :previous_version, T.nilable(String)
26
+ const :previous_constraint, T.nilable(String)
27
+ const :constraint_bumped, T.nilable(String)
28
+ const :constraint_bumped_if_needed, T.nilable(String)
29
+ const :constraint_widened, T.nilable(String)
30
+ end
31
+
32
+ class ReportEntry < T::ImmutableStruct
33
+ const :name, String
34
+ const :version, String
35
+ const :latest, T.nilable(String)
36
+ const :compatible, T::Array[DependencyUpdate]
37
+ const :single_breaking, T::Array[DependencyUpdate]
38
+ const :multi_breaking, T::Array[DependencyUpdate]
39
+ const :smallest_update, T.nilable(T::Array[DependencyUpdate])
40
+ end
41
+
42
+ class Report < T::ImmutableStruct
43
+ const :dependencies, T::Array[ReportEntry]
44
+ const :cache_content, String
45
+ end
46
+
47
+ sig { params(content: String).returns(T::Array[ListedDependency]) }
48
+ def self.list_from_json(content)
49
+ context = "dependency_services list"
50
+ root = JsonValueParser.object(JsonValueParser.parse(content, context), context)
51
+ JsonValueParser.array(root["dependencies"], "#{context}.dependencies").each_with_index.map do |value, index|
52
+ entry_context = "#{context}.dependencies[#{index}]"
53
+ fields = JsonValueParser.object(value, entry_context)
54
+ ListedDependency.new(
55
+ name: JsonValueParser.string(fields["name"], "#{entry_context}.name"),
56
+ version: JsonValueParser.string(fields["version"], "#{entry_context}.version"),
57
+ kind: JsonValueParser.string(fields["kind"], "#{entry_context}.kind"),
58
+ constraint: JsonValueParser.optional_string(fields["constraint"], "#{entry_context}.constraint"),
59
+ source: JsonValueParser.optional_object(fields["source"], "#{entry_context}.source")
60
+ )
61
+ end
62
+ rescue JsonValueParser::InvalidValue => e
63
+ invalid_result("dependency_services list", e.message)
64
+ end
65
+
66
+ sig { params(content: String).returns(Report) }
67
+ def self.report_from_json(content)
68
+ context = "dependency_services report"
69
+ root = JsonValueParser.object(JsonValueParser.parse(content, context), context)
70
+ build_report(root["dependencies"], "#{context}.dependencies")
71
+ rescue JsonValueParser::InvalidValue => e
72
+ invalid_result("dependency_services report", e.message)
73
+ end
74
+
75
+ sig { params(content: String).returns(Report) }
76
+ def self.report_from_cache(content)
77
+ context = "dependency_services report cache"
78
+ build_report(JsonValueParser.parse(content, context), context)
79
+ rescue JsonValueParser::InvalidValue => e
80
+ invalid_result("dependency_services report cache", e.message)
81
+ end
82
+
83
+ sig { params(reports: T::Array[ReportEntry], name: String).returns(ReportEntry) }
84
+ def self.find_report(reports, name)
85
+ report = reports.find { |entry| entry.name == name }
86
+ return report if report
87
+
88
+ invalid_result(
89
+ "dependency_services report",
90
+ "dependency_services report does not include the requested dependency"
91
+ )
92
+ end
93
+
94
+ sig { params(context: String, message: String).returns(T.noreturn) }
95
+ def self.invalid_result(context, message)
96
+ raise SharedHelpers::HelperSubprocessFailed.new(
97
+ message: message,
98
+ error_class: "TypeError",
99
+ error_context: { function: context }
100
+ ),
101
+ cause: nil
102
+ end
103
+
104
+ sig { params(value: Object, context: String).returns(Report) }
105
+ def self.build_report(value, context)
106
+ raw_entries = JsonValueParser.array(value, context)
107
+ entries = raw_entries.each_with_index.map do |entry, index|
108
+ report_entry(entry, "#{context}[#{index}]")
109
+ end
110
+ Report.new(dependencies: entries, cache_content: JSON.generate(raw_entries))
111
+ end
112
+ private_class_method :build_report
113
+
114
+ sig { params(value: Object, context: String).returns(ReportEntry) }
115
+ def self.report_entry(value, context)
116
+ fields = JsonValueParser.object(value, context)
117
+ ReportEntry.new(
118
+ name: JsonValueParser.string(fields["name"], "#{context}.name"),
119
+ version: JsonValueParser.string(fields["version"], "#{context}.version"),
120
+ latest: JsonValueParser.optional_string(fields["latest"], "#{context}.latest"),
121
+ compatible: updates(fields["compatible"], "#{context}.compatible"),
122
+ single_breaking: updates(fields["singleBreaking"], "#{context}.singleBreaking"),
123
+ multi_breaking: updates(fields["multiBreaking"], "#{context}.multiBreaking"),
124
+ smallest_update: if fields.key?("smallestUpdate")
125
+ updates(
126
+ fields["smallestUpdate"],
127
+ "#{context}.smallestUpdate"
128
+ )
129
+ end
130
+ )
131
+ end
132
+ private_class_method :report_entry
133
+
134
+ sig { params(value: Object, context: String).returns(T::Array[DependencyUpdate]) }
135
+ def self.updates(value, context)
136
+ JsonValueParser.array(value, context).each_with_index.map do |entry, index|
137
+ entry_context = "#{context}[#{index}]"
138
+ fields = JsonValueParser.object(entry, entry_context)
139
+ DependencyUpdate.new(
140
+ name: JsonValueParser.string(fields["name"], "#{entry_context}.name"),
141
+ version: JsonValueParser.optional_string(fields["version"], "#{entry_context}.version"),
142
+ kind: JsonValueParser.string(fields["kind"], "#{entry_context}.kind"),
143
+ previous_version: JsonValueParser.optional_string(
144
+ fields["previousVersion"],
145
+ "#{entry_context}.previousVersion"
146
+ ),
147
+ previous_constraint: JsonValueParser.optional_string(
148
+ fields["previousConstraint"], "#{entry_context}.previousConstraint"
149
+ ),
150
+ constraint_bumped: JsonValueParser.optional_string(
151
+ fields["constraintBumped"],
152
+ "#{entry_context}.constraintBumped"
153
+ ),
154
+ constraint_bumped_if_needed: JsonValueParser.optional_string(
155
+ fields["constraintBumpedIfNeeded"], "#{entry_context}.constraintBumpedIfNeeded"
156
+ ),
157
+ constraint_widened: JsonValueParser.optional_string(
158
+ fields["constraintWidened"],
159
+ "#{entry_context}.constraintWidened"
160
+ )
161
+ )
162
+ end
163
+ end
164
+ private_class_method :updates
165
+ end
166
+ end
167
+ end
@@ -82,9 +82,9 @@ module Dependabot
82
82
  raise "No pubspec.yaml!" unless get_original_file("pubspec.yaml")
83
83
  end
84
84
 
85
- sig { returns(T::Array[T::Hash[String, Object]]) }
85
+ sig { returns(T::Array[DependencyServicesResult::ListedDependency]) }
86
86
  def list
87
- @list ||= T.let(dependency_services_list, T.nilable(T::Array[T::Hash[String, Object]]))
87
+ @list ||= T.let(dependency_services_list, T.nilable(T::Array[DependencyServicesResult::ListedDependency]))
88
88
  end
89
89
  end
90
90
  end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "digest"
@@ -8,6 +8,8 @@ require "sorbet-runtime"
8
8
 
9
9
  require "dependabot/errors"
10
10
  require "dependabot/logger"
11
+ require "dependabot/pub/dependency_services_result"
12
+ require "dependabot/pub/package/registry_package"
11
13
  require "dependabot/pub/requirement"
12
14
  require "dependabot/pub/requirement_source"
13
15
  require "dependabot/requirements_update_strategy"
@@ -23,13 +25,19 @@ module Dependabot
23
25
 
24
26
  abstract!
25
27
 
28
+ class SdkVersions < T::ImmutableStruct
29
+ const :flutter, String
30
+ const :dart, String
31
+ const :channel, T.nilable(String), default: nil
32
+ end
33
+
26
34
  sig { abstract.returns(T::Array[Dependabot::Credential]) }
27
35
  def credentials; end
28
36
 
29
37
  sig { abstract.returns(T::Array[Dependabot::DependencyFile]) }
30
38
  def dependency_files; end
31
39
 
32
- sig { abstract.returns(T::Hash[Symbol, T.untyped]) }
40
+ sig { abstract.returns(T::Hash[Symbol, T.anything]) }
33
41
  def options; end
34
42
 
35
43
  sig { returns(String) }
@@ -42,7 +50,7 @@ module Dependabot
42
50
  dir: T.any(Pathname, String),
43
51
  url: T.nilable(String)
44
52
  )
45
- .returns(T.nilable(T::Hash[String, T.untyped]))
53
+ .returns(T.nilable(SdkVersions))
46
54
  end
47
55
  def self.run_infer_sdk_versions(dir, url: nil)
48
56
  env = {}
@@ -51,24 +59,31 @@ module Dependabot
51
59
  stdout, _, status = Open3.capture3(env, cmd, opts, chdir: dir)
52
60
  return nil unless status.success?
53
61
 
54
- JSON.parse(stdout)
62
+ fields = JsonValueParser.object(JsonValueParser.parse(stdout, "infer_sdk_versions"), "infer_sdk_versions")
63
+ SdkVersions.new(
64
+ flutter: JsonValueParser.string(fields["flutter"], "infer_sdk_versions.flutter"),
65
+ dart: JsonValueParser.string(fields["dart"], "infer_sdk_versions.dart"),
66
+ channel: JsonValueParser.string(fields["channel"], "infer_sdk_versions.channel")
67
+ )
68
+ rescue JsonValueParser::InvalidValue => e
69
+ DependencyServicesResult.invalid_result("infer_sdk_versions", e.message)
55
70
  end
56
71
 
57
72
  private
58
73
 
59
- sig { returns(T::Array[T::Hash[String, T.untyped]]) }
74
+ sig { returns(T::Array[DependencyServicesResult::ListedDependency]) }
60
75
  def dependency_services_list
61
- JSON.parse(run_dependency_services("list"))["dependencies"]
76
+ DependencyServicesResult.list_from_json(run_dependency_services("list"))
62
77
  end
63
78
 
64
79
  sig { params(dependency: Dependabot::Dependency).returns(String) }
65
80
  def repository_url(dependency)
66
81
  repository_url = RequirementSource.new(dependency.requirements.first).description_string("url") ||
67
- options[:pub_hosted_url] || "https://pub.dev"
82
+ option_string(:pub_hosted_url) || "https://pub.dev"
68
83
  repository_url.delete_suffix("/")
69
84
  end
70
85
 
71
- sig { params(dependency: Dependabot::Dependency).returns(T::Hash[String, T.untyped]) }
86
+ sig { params(dependency: Dependabot::Dependency).returns(Package::RegistryPackage) }
72
87
  def fetch_package_listing(dependency)
73
88
  # Because we get the security_advisories as a set of constraints, we
74
89
  # fetch the list of all versions and filter them to a list of vulnerable
@@ -77,17 +92,15 @@ module Dependabot
77
92
  # Ideally we would like the helper to be the only one doing requests to
78
93
  # the repository. But this should work for now:
79
94
  response = Dependabot::RegistryClient.get(url: "#{repository_url(dependency)}/api/packages/#{dependency.name}")
80
- JSON.parse(response.body)
95
+ Package::RegistryPackage.from_json(response.body)
81
96
  end
82
97
 
83
98
  sig { params(dependency: Dependabot::Dependency).returns(T::Array[Dependabot::Pub::Version]) }
84
99
  def available_versions(dependency)
85
- fetch_package_listing(dependency)["versions"].map do |v|
86
- Dependabot::Pub::Version.new(v["version"])
87
- end
100
+ fetch_package_listing(dependency).versions
88
101
  end
89
102
 
90
- sig { returns(T::Array[T::Hash[String, T.untyped]]) }
103
+ sig { returns(T::Array[DependencyServicesResult::ReportEntry]) }
91
104
  def dependency_services_report
92
105
  sha256 = Digest::SHA256.new
93
106
  dependency_files.each do |f|
@@ -96,11 +109,11 @@ module Dependabot
96
109
  hash = sha256.hexdigest
97
110
 
98
111
  cache_file = "/tmp/report-#{hash}-pid-#{Process.pid}.json"
99
- return JSON.parse(File.read(cache_file)) if File.file?(cache_file)
112
+ return DependencyServicesResult.report_from_cache(File.read(cache_file)).dependencies if File.file?(cache_file)
100
113
 
101
- report = JSON.parse(run_dependency_services("report"))["dependencies"]
102
- File.write(cache_file, JSON.generate(report))
103
- report
114
+ report = DependencyServicesResult.report_from_json(run_dependency_services("report"))
115
+ File.write(cache_file, report.cache_content)
116
+ report.dependencies
104
117
  end
105
118
 
106
119
  sig do
@@ -177,19 +190,19 @@ module Dependabot
177
190
  ## Detects the right flutter release to use for the pubspec.yaml.
178
191
  ## Then checks it out if it is not already.
179
192
  ## Returns the sdk versions
180
- sig { params(dir: T.any(Pathname, String)).returns(T::Hash[String, String]) }
193
+ sig { params(dir: T.any(Pathname, String)).returns(SdkVersions) }
181
194
  def ensure_right_flutter_release(dir)
182
195
  versions = Helpers.run_infer_sdk_versions(
183
- File.join(dir, dependency_files.first&.directory),
184
- url: options[:flutter_releases_url]
196
+ File.join(dir, T.must(dependency_files.first).directory),
197
+ url: option_string(:flutter_releases_url)
185
198
  )
186
199
  flutter_ref =
187
200
  if versions
188
201
  Dependabot.logger.info(
189
- "Installing the Flutter SDK version: #{versions['flutter']} " \
190
- "from channel #{versions['channel']} with Dart #{versions['dart']}"
202
+ "Installing the Flutter SDK version: #{versions.flutter} " \
203
+ "from channel #{versions.channel} with Dart #{versions.dart}"
191
204
  )
192
- "refs/tags/#{versions['flutter']}"
205
+ "refs/tags/#{versions.flutter}"
193
206
  else
194
207
  Dependabot.logger.info(
195
208
  "Failed to infer the flutter version. Attempting to use latest stable release."
@@ -218,7 +231,7 @@ module Dependabot
218
231
  end
219
232
 
220
233
  # Runs `flutter version` and returns the dart and flutter version numbers in a map.
221
- sig { returns(T::Hash[String, String]) }
234
+ sig { returns(SdkVersions) }
222
235
  def run_flutter_version
223
236
  Dependabot.logger.info "Running `flutter --version`"
224
237
  # Run `flutter --version --machine` to get the current flutter version.
@@ -234,20 +247,18 @@ module Dependabot
234
247
  "Running 'flutter --version --machine' failed: #{stderr}"
235
248
  end
236
249
 
237
- parsed = JSON.parse(stdout)
238
- flutter_version = parsed["frameworkVersion"]
239
- dart_version = parsed["dartSdkVersion"]&.split&.first
240
- unless flutter_version && dart_version
241
- raise Dependabot::DependabotError,
242
- "Bad output from `flutter --version`: #{stdout}"
243
- end
250
+ context = "flutter --version"
251
+ parsed = JsonValueParser.object(JsonValueParser.parse(stdout, context), context)
252
+ flutter_version = JsonValueParser.string(parsed["frameworkVersion"], "#{context}.frameworkVersion")
253
+ dart_version = JsonValueParser.string(parsed["dartSdkVersion"], "#{context}.dartSdkVersion").split.first
254
+ raise JsonValueParser::InvalidValue, "#{context}.dartSdkVersion must contain a version" unless dart_version
255
+
244
256
  Dependabot.logger.info(
245
257
  "Installed the Flutter SDK version: #{flutter_version} with Dart #{dart_version}."
246
258
  )
247
- {
248
- "flutter" => flutter_version,
249
- "dart" => dart_version
250
- }
259
+ SdkVersions.new(flutter: flutter_version, dart: dart_version)
260
+ rescue JsonValueParser::InvalidValue => e
261
+ DependencyServicesResult.invalid_result("flutter --version", e.message)
251
262
  end
252
263
 
253
264
  sig do
@@ -273,12 +284,12 @@ module Dependabot
273
284
  "PUB_ENVIRONMENT" => "dependabot",
274
285
  "FLUTTER_ROOT" => "/tmp/flutter",
275
286
  "DART_ROOT" => "/tmp/flutter/bin/cache/dart-sdk",
276
- "PUB_HOSTED_URL" => options[:pub_hosted_url],
287
+ "PUB_HOSTED_URL" => option_string(:pub_hosted_url),
277
288
  # This variable will make the solver run assuming that Dart SDK version.
278
289
  # TODO(sigurdm): Would be nice to have a better handle for fixing the dart sdk version.
279
- "_PUB_TEST_SDK_VERSION" => sdk_versions["dart"]
290
+ "_PUB_TEST_SDK_VERSION" => sdk_versions.dart
280
291
  }
281
- command_dir = File.join(temp_dir, dependency_files.first&.directory)
292
+ command_dir = File.join(temp_dir, T.must(dependency_files.first).directory)
282
293
 
283
294
  stdout, stderr, status = Open3.capture3(
284
295
  env.compact,
@@ -311,25 +322,19 @@ module Dependabot
311
322
  end
312
323
 
313
324
  # Parses a dependency as listed by `dependency_services list`.
314
- sig { params(json: T::Hash[String, T.untyped]).returns(Dependabot::Dependency) }
315
- def parse_listed_dependency(json)
316
- params = {
317
- name: json["name"],
318
- version: json["version"],
319
- package_manager: "pub",
320
- requirements: []
321
- }
325
+ sig { params(entry: DependencyServicesResult::ListedDependency).returns(Dependabot::Dependency) }
326
+ def parse_listed_dependency(entry)
327
+ requirements = []
322
328
 
323
- if json["kind"] != "transitive" && !json["constraint"].nil?
324
- constraint = json["constraint"]
325
- params[:requirements] << {
326
- requirement: constraint,
327
- groups: [json["kind"]],
328
- source: json["source"],
329
+ if entry.kind != "transitive" && !entry.constraint.nil?
330
+ requirements << {
331
+ requirement: entry.constraint,
332
+ groups: [entry.kind],
333
+ source: entry.source,
329
334
  file: "pubspec.yaml"
330
335
  }
331
336
  end
332
- Dependency.new(**params)
337
+ Dependency.new(name: entry.name, version: entry.version, package_manager: "pub", requirements: requirements)
333
338
  end
334
339
 
335
340
  # Parses the updated dependencies returned by
@@ -339,43 +344,41 @@ module Dependabot
339
344
  # used to chose the right updated constraint.
340
345
  sig do
341
346
  params(
342
- json: T::Hash[String, T.untyped],
347
+ entry: DependencyServicesResult::DependencyUpdate,
343
348
  requirements_update_strategy: Dependabot::RequirementsUpdateStrategy
344
349
  )
345
350
  .returns(Dependabot::Dependency)
346
351
  end
347
- def parse_updated_dependency(json, requirements_update_strategy)
352
+ def parse_updated_dependency(entry, requirements_update_strategy)
348
353
  requirements = []
349
- constraint_field = constraint_field_from_update_strategy(requirements_update_strategy)
354
+ constraint = constraint_from_update_strategy(entry, requirements_update_strategy)
350
355
 
351
- if json["kind"] != "transitive" && !json[constraint_field].nil?
352
- constraint = json[constraint_field]
356
+ if entry.kind != "transitive" && !constraint.nil?
353
357
  requirements << {
354
358
  requirement: constraint,
355
- groups: [json["kind"]],
359
+ groups: [entry.kind],
356
360
  source: nil, # TODO: Expose some information about the source
357
361
  file: "pubspec.yaml"
358
362
  }
359
363
  end
360
364
 
361
365
  previous_requirements = []
362
- if json["previousVersion"] && json["kind"] != "transitive" && !json["previousConstraint"].nil?
363
- constraint = json["previousConstraint"]
366
+ if entry.previous_version && entry.kind != "transitive" && !entry.previous_constraint.nil?
364
367
  previous_requirements << {
365
- requirement: constraint,
366
- groups: [json["kind"]],
368
+ requirement: entry.previous_constraint,
369
+ groups: [entry.kind],
367
370
  source: nil, # TODO: Expose some information about the source
368
371
  file: "pubspec.yaml"
369
372
  }
370
373
  end
371
374
 
372
375
  Dependency.new(
373
- name: json["name"],
374
- version: json["version"],
376
+ name: entry.name,
377
+ version: entry.version,
375
378
  package_manager: "pub",
376
379
  requirements: requirements,
377
- previous_version: json["previousVersion"],
378
- previous_requirements: json["previousVersion"] ? previous_requirements : nil
380
+ previous_version: entry.previous_version,
381
+ previous_requirements: entry.previous_version ? previous_requirements : nil
379
382
  )
380
383
  end
381
384
 
@@ -383,23 +386,35 @@ module Dependabot
383
386
  # strategies.
384
387
  sig do
385
388
  params(
389
+ entry: DependencyServicesResult::DependencyUpdate,
386
390
  requirements_update_strategy: Dependabot::RequirementsUpdateStrategy
387
391
  )
388
- .returns(String)
392
+ .returns(T.nilable(String))
389
393
  end
390
- def constraint_field_from_update_strategy(requirements_update_strategy)
394
+ def constraint_from_update_strategy(entry, requirements_update_strategy)
391
395
  case requirements_update_strategy
392
396
  when RequirementsUpdateStrategy::WidenRanges
393
- "constraintWidened"
397
+ entry.constraint_widened
394
398
  when RequirementsUpdateStrategy::BumpVersions
395
- "constraintBumped"
399
+ entry.constraint_bumped
396
400
  when RequirementsUpdateStrategy::BumpVersionsIfNecessary
397
- "constraintBumpedIfNeeded"
401
+ entry.constraint_bumped_if_needed
398
402
  else
399
403
  raise "Unexpected requirements_update_strategy #{requirements_update_strategy}"
400
404
  end
401
405
  end
402
406
 
407
+ sig { params(key: Symbol).returns(T.nilable(String)) }
408
+ def option_string(key)
409
+ value = options[key]
410
+
411
+ case value
412
+ when String then value
413
+ when nil then nil
414
+ else raise TypeError, "Pub option #{key} must be a string or nil"
415
+ end
416
+ end
417
+
403
418
  sig do
404
419
  params(
405
420
  dependencies: T.nilable(T::Array[Dependabot::Dependency])
@@ -407,29 +422,19 @@ module Dependabot
407
422
  .returns(T.nilable(String))
408
423
  end
409
424
  def dependencies_to_json(dependencies)
410
- if dependencies.nil?
411
- nil
412
- else
413
- deps = dependencies.map do |d|
414
- source = d.requirements.empty? ? nil : d.requirements.first&.[](:source)
415
- obj = {
416
- "name" => d.name,
417
- "version" => d.version,
418
- "source" => source
419
- }
420
-
421
- unless d.requirements.nil? || d.requirements.empty?
422
- obj["constraint"] =
423
- d.requirements[0]&.[](:requirement).to_s
424
- end
425
- obj
426
- end
427
- JSON.generate(
428
- {
429
- "dependencyChanges" => deps
430
- }
431
- )
425
+ return if dependencies.nil?
426
+
427
+ changes = dependencies.map do |dependency|
428
+ requirement = dependency.requirements.first
429
+ fields = {
430
+ "name" => dependency.name,
431
+ "version" => dependency.version,
432
+ "source" => requirement&.source
433
+ }
434
+ fields["constraint"] = requirement.requirement.to_s if requirement
435
+ fields
432
436
  end
437
+ JSON.generate("dependencyChanges" => changes)
433
438
  end
434
439
  end
435
440
  end
@@ -0,0 +1,65 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "sorbet-runtime"
6
+
7
+ module Dependabot
8
+ module Pub
9
+ module JsonValueParser
10
+ extend T::Sig
11
+
12
+ class InvalidValue < TypeError; end
13
+
14
+ ObjectHash = T.type_alias { T::Hash[String, Object] }
15
+
16
+ sig { params(content: String, context: String).returns(Object) }
17
+ def self.parse(content, context)
18
+ T.cast(JSON.parse(content), Object)
19
+ rescue JSON::ParserError
20
+ raise InvalidValue.new("#{context} must be valid JSON"), cause: nil
21
+ end
22
+
23
+ sig { params(value: Object, context: String).returns(ObjectHash) }
24
+ def self.object(value, context)
25
+ raise InvalidValue, "#{context} must be an object" unless value.is_a?(Hash)
26
+
27
+ value.to_h do |raw_key, raw_value|
28
+ key = T.cast(raw_key, Object)
29
+ raise InvalidValue, "#{context} keys must be strings" unless key.is_a?(String)
30
+
31
+ [key, T.cast(raw_value, Object)]
32
+ end
33
+ end
34
+
35
+ sig { params(value: Object, context: String).returns(T.nilable(ObjectHash)) }
36
+ def self.optional_object(value, context)
37
+ return if value.nil?
38
+
39
+ object(value, context)
40
+ end
41
+
42
+ sig { params(value: Object, context: String).returns(T::Array[Object]) }
43
+ def self.array(value, context)
44
+ raise InvalidValue, "#{context} must be an array" unless value.is_a?(Array)
45
+
46
+ value.map { |entry| T.cast(entry, Object) }
47
+ end
48
+
49
+ sig { params(value: Object, context: String).returns(String) }
50
+ def self.string(value, context)
51
+ return value if value.is_a?(String)
52
+
53
+ raise InvalidValue, "#{context} must be a string"
54
+ end
55
+
56
+ sig { params(value: Object, context: String).returns(T.nilable(String)) }
57
+ def self.optional_string(value, context)
58
+ return if value.nil?
59
+ return value if value.is_a?(String)
60
+
61
+ raise InvalidValue, "#{context} must be a string or nil"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "excon"
@@ -6,6 +6,7 @@ require "sorbet-runtime"
6
6
  require "dependabot/metadata_finders"
7
7
  require "dependabot/metadata_finders/base"
8
8
  require "dependabot/pub/requirement_source"
9
+ require "dependabot/pub/package/registry_package"
9
10
  require "dependabot/registry_client"
10
11
 
11
12
  module Dependabot
@@ -26,20 +27,16 @@ module Dependabot
26
27
  end
27
28
  repository_url = (source.description_string("url") || "https://pub.dev").delete_suffix("/")
28
29
 
29
- listing = repository_listing(repository_url)
30
- repo = listing.dig("latest", "pubspec", "repository")
31
- # The repository field did not always exist in pubspec.yaml, and some
32
- # packages specify a git repository in the "homepage" field.
33
- repo ||= listing.dig("latest", "pubspec", "homepage")
30
+ repo = repository_listing(repository_url).source_url
34
31
  return nil unless repo
35
32
 
36
33
  Source.from_url(repo)
37
34
  end
38
35
 
39
- sig { params(repository_url: String).returns(T::Hash[String, T.untyped]) }
36
+ sig { params(repository_url: String).returns(Package::RegistryPackage) }
40
37
  def repository_listing(repository_url)
41
38
  response = Dependabot::RegistryClient.get(url: "#{repository_url}/api/packages/#{dependency.name}")
42
- JSON.parse(response.body)
39
+ Package::RegistryPackage.from_json(response.body)
43
40
  end
44
41
  end
45
42
  end
@@ -1,8 +1,6 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- require "json"
5
- require "time"
6
4
  require "excon"
7
5
  require "nokogiri"
8
6
  require "sorbet-runtime"
@@ -29,7 +27,7 @@ module Dependabot
29
27
  sig { override.returns(T::Array[Dependabot::DependencyFile]) }
30
28
  attr_reader :dependency_files
31
29
 
32
- sig { override.returns(T::Hash[Symbol, T.untyped]) }
30
+ sig { override.returns(T::Hash[Symbol, T.anything]) }
33
31
  attr_reader :options
34
32
 
35
33
  sig { override.returns(T::Array[Dependabot::Credential]) }
@@ -42,7 +40,7 @@ module Dependabot
42
40
  credentials: T::Array[Dependabot::Credential],
43
41
  ignored_versions: T::Array[String],
44
42
  security_advisories: T::Array[Dependabot::SecurityAdvisory],
45
- options: T::Hash[Symbol, T.untyped]
43
+ options: T::Hash[Symbol, T.anything]
46
44
  )
47
45
  .void
48
46
  end
@@ -62,55 +60,33 @@ module Dependabot
62
60
  @options = options
63
61
  end
64
62
 
65
- sig { returns(T::Array[T::Hash[String, T.untyped]]) }
63
+ sig { returns(T::Array[DependencyServicesResult::ReportEntry]) }
66
64
  def report
67
65
  @report ||= T.let(
68
66
  dependency_services_report,
69
- T.nilable(T::Array[T::Hash[String, T.untyped]])
67
+ T.nilable(T::Array[DependencyServicesResult::ReportEntry])
70
68
  )
71
69
  end
72
70
 
73
- sig { returns(T.any(T::Array[Dependabot::Package::PackageRelease], T.untyped)) }
71
+ sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
74
72
  def package_details_metadata
75
73
  Dependabot.logger.info("Initializing package metadata for \"#{@dependency.name}\"")
76
74
 
77
75
  response = fetch_package_metadata(dependency)
78
76
  return [] if response.status >= 500
79
77
 
80
- versions = JSON.parse(response.body).fetch("versions", [])
81
-
82
78
  # Build the full list up front. If any release can't be parsed (e.g. a
83
79
  # missing or invalid publish date) the partial result is discarded, so
84
80
  # callers can treat a non-empty list as a complete, trustworthy snapshot
85
81
  # and an empty list as "no usable metadata".
86
- versions.map do |v|
87
- package_release(
88
- version: v["version"],
89
- publish_date: Time.parse(v["published"])
90
- )
91
- end
92
- rescue JSON::ParserError
82
+ RegistryPackage.from_json(response.body).releases
83
+ rescue JsonValueParser::InvalidValue
93
84
  Dependabot.logger.error("Failed to parse package metadata")
94
85
  []
95
86
  rescue StandardError => e
96
87
  Dependabot.logger.error("Failed to fetch package metadata #{e.message}")
97
88
  []
98
89
  end
99
-
100
- private
101
-
102
- sig do
103
- params(
104
- version: String,
105
- publish_date: T.nilable(Time)
106
- ).returns(Dependabot::Package::PackageRelease)
107
- end
108
- def package_release(version:, publish_date: nil)
109
- Dependabot::Package::PackageRelease.new(
110
- version: Pub::Version.new(version),
111
- released_at: publish_date
112
- )
113
- end
114
90
  end
115
91
  end
116
92
  end
@@ -0,0 +1,67 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+ require "sorbet-runtime"
6
+ require "dependabot/package/package_release"
7
+ require "dependabot/pub/json_value_parser"
8
+ require "dependabot/pub/version"
9
+
10
+ module Dependabot
11
+ module Pub
12
+ module Package
13
+ class RegistryPackage
14
+ extend T::Sig
15
+
16
+ sig { params(content: String).returns(RegistryPackage) }
17
+ def self.from_json(content)
18
+ new(JsonValueParser.object(JsonValueParser.parse(content, "Pub package"), "Pub package"))
19
+ end
20
+
21
+ sig { params(data: JsonValueParser::ObjectHash).void }
22
+ def initialize(data)
23
+ @data = data
24
+ end
25
+
26
+ sig { returns(T::Array[Dependabot::Pub::Version]) }
27
+ def versions
28
+ version_entries(@data["versions"]).map do |fields|
29
+ Dependabot::Pub::Version.new(JsonValueParser.string(fields["version"], "Pub package version"))
30
+ end
31
+ end
32
+
33
+ sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
34
+ def releases
35
+ version_entries(@data.fetch("versions", [])).map do |fields|
36
+ Dependabot::Package::PackageRelease.new(
37
+ version: Dependabot::Pub::Version.new(JsonValueParser.string(fields["version"], "Pub package version")),
38
+ released_at: Time.parse(JsonValueParser.string(fields["published"], "Pub package published"))
39
+ )
40
+ end
41
+ end
42
+
43
+ sig { returns(T.nilable(String)) }
44
+ def source_url
45
+ latest = JsonValueParser.optional_object(@data["latest"], "Pub package latest")
46
+ return unless latest
47
+
48
+ pubspec = JsonValueParser.optional_object(latest["pubspec"], "Pub package latest.pubspec")
49
+ return unless pubspec
50
+
51
+ value = pubspec["repository"] || pubspec["homepage"]
52
+ # Preserve the existing false-as-absent behavior of optional source metadata.
53
+ JsonValueParser.optional_string(value || nil, "Pub package repository or homepage")
54
+ end
55
+
56
+ private
57
+
58
+ sig { params(value: Object).returns(T::Array[JsonValueParser::ObjectHash]) }
59
+ def version_entries(value)
60
+ JsonValueParser.array(value, "Pub package versions").each_with_index.map do |entry, index|
61
+ JsonValueParser.object(entry, "Pub package versions[#{index}]")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "excon"
@@ -27,7 +27,7 @@ module Dependabot
27
27
  credentials: T::Array[Dependabot::Credential],
28
28
  ignored_versions: T::Array[String],
29
29
  security_advisories: T::Array[Dependabot::SecurityAdvisory],
30
- options: T::Hash[Symbol, T.untyped],
30
+ options: T::Hash[Symbol, T.anything],
31
31
  cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions)
32
32
  ).void
33
33
  end
@@ -49,10 +49,10 @@ module Dependabot
49
49
  @cooldown_options = cooldown_options
50
50
  end
51
51
 
52
- sig { returns(T::Hash[String, T.untyped]) }
52
+ sig { returns(DependencyServicesResult::ReportEntry) }
53
53
  def current_report
54
54
  @current_report ||= T.let(
55
- T.must(
55
+ DependencyServicesResult.find_report(
56
56
  PackageDetailsFetcher.new(
57
57
  dependency: dependency,
58
58
  dependency_files: dependency_files,
@@ -60,15 +60,16 @@ module Dependabot
60
60
  ignored_versions: ignored_versions,
61
61
  security_advisories: security_advisories,
62
62
  options: options
63
- ).report.find { |d| d["name"] == dependency.name }
63
+ ).report,
64
+ dependency.name
64
65
  ),
65
- T.nilable(T::Hash[String, T.untyped])
66
+ T.nilable(DependencyServicesResult::ReportEntry)
66
67
  )
67
68
  end
68
69
 
69
70
  sig { returns(T.nilable(String)) }
70
71
  def latest_version
71
- latest_version = current_report["latest"]
72
+ latest_version = current_report.latest
72
73
  return nil unless latest_version
73
74
 
74
75
  filter_cooldown_versions(latest_version)
@@ -76,36 +77,36 @@ module Dependabot
76
77
 
77
78
  sig { returns(T.nilable(String)) }
78
79
  def latest_resolvable_version
79
- latest_resolvable_version = current_report["singleBreaking"]&.find { |d| d["name"] == dependency.name }
80
- return nil unless latest_resolvable_version
80
+ version = latest_resolvable_update&.version
81
+ return nil unless version
81
82
 
82
- filter_cooldown_versions(latest_resolvable_version["version"])
83
+ filter_cooldown_versions(version)
83
84
  end
84
85
 
85
86
  sig { returns(T.nilable(String)) }
86
87
  def latest_resolvable_version_with_no_unlock
87
- version_with_no_unlock = current_report["compatible"]&.find { |d| d["name"] == dependency.name }
88
- return nil unless version_with_no_unlock
88
+ version = current_report.compatible.find { |entry| entry.name == dependency.name }&.version
89
+ return nil unless version
89
90
 
90
- filter_cooldown_versions(version_with_no_unlock["version"])
91
+ filter_cooldown_versions(version)
91
92
  end
92
93
 
93
94
  sig { returns(T.nilable(String)) }
94
95
  def latest_version_resolvable_with_full_unlock
95
- version_with_full_unlock = current_report["multiBreaking"]&.find { |d| d["name"] == dependency.name }
96
- return nil unless version_with_full_unlock
96
+ version = full_unlock_updates.find { |entry| entry.name == dependency.name }&.version
97
+ return nil unless version
97
98
 
98
- filter_cooldown_versions(version_with_full_unlock["version"])
99
+ filter_cooldown_versions(version)
99
100
  end
100
101
 
101
- sig { returns(T.untyped) }
102
- def latest_version_resolvable_with_full_unlock_hash
103
- current_report["multiBreaking"]
102
+ sig { returns(T::Array[DependencyServicesResult::DependencyUpdate]) }
103
+ def full_unlock_updates
104
+ current_report.multi_breaking
104
105
  end
105
106
 
106
- sig { returns(T.untyped) }
107
- def latest_resolvable_version_hash
108
- current_report["singleBreaking"].find { |d| d["name"] == dependency.name }
107
+ sig { returns(T.nilable(DependencyServicesResult::DependencyUpdate)) }
108
+ def latest_resolvable_update
109
+ current_report.single_breaking.find { |entry| entry.name == dependency.name }
109
110
  end
110
111
 
111
112
  private
@@ -286,7 +287,7 @@ module Dependabot
286
287
  sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
287
288
  attr_reader :security_advisories
288
289
 
289
- sig { returns(T::Hash[Symbol, T.untyped]) }
290
+ sig { returns(T::Hash[Symbol, T.anything]) }
290
291
  attr_reader :options
291
292
 
292
293
  sig { returns(T.nilable(Dependabot::Package::ReleaseCooldownOptions)) }
@@ -21,7 +21,10 @@ module Dependabot
21
21
 
22
22
  sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
23
23
  def latest_version
24
- version = version_unless_ignored(T.must(version_report.latest_version), current_version: dependency.version)
24
+ candidate = version_report.latest_version
25
+ return nil unless candidate
26
+
27
+ version = version_unless_ignored(candidate, current_version: dependency.version)
25
28
 
26
29
  raise AllVersionsIgnored if version.nil? && @raise_on_ignored
27
30
 
@@ -48,10 +51,12 @@ module Dependabot
48
51
  e = dependency_services_smallest_update
49
52
  return nil if e.nil?
50
53
 
51
- upgrade = e.find { |u| u["name"] == dependency.name }
54
+ upgrade = e.find { |u| u.name == dependency.name }
55
+
56
+ version = upgrade&.version
57
+ return unless version
52
58
 
53
- version = T.must(upgrade)["version"]
54
- T.cast(version_unless_ignored(version), Dependabot::Version)
59
+ T.cast(version_unless_ignored(version), T.nilable(Dependabot::Version))
55
60
  end
56
61
 
57
62
  sig { override.returns(T.nilable(Dependabot::Version)) }
@@ -80,16 +85,16 @@ module Dependabot
80
85
 
81
86
  # Ideally we would like to do any upgrade that migrates away from the vulnerability
82
87
  # but this method can only return a single requirement udate.
83
- breaking_changes = updates&.filter { |d| d["previousConstraint"] != d["constraintBumpedIfNeeded"] }
88
+ breaking_changes = updates&.filter { |d| d.previous_constraint != d.constraint_bumped_if_needed }
84
89
 
85
90
  # This security update would require unlocking other packages, which is not currently supported.
86
91
  # Because of that, return original requirements, so that no requirements are actually updated and
87
92
  # the error bubbles up as security_update_not_possible to the user.
88
93
  return dependency.requirements if breaking_changes&.size&.> 1
89
94
 
90
- updates&.find { |u| u["name"] == dependency.name }
95
+ updates&.find { |u| u.name == dependency.name }
91
96
  else
92
- version_report.latest_resolvable_version_hash
97
+ version_report.latest_resolvable_update
93
98
  end
94
99
  return [] unless entry
95
100
 
@@ -100,7 +105,7 @@ module Dependabot
100
105
  private
101
106
 
102
107
  # rubocop:disable Metrics/AbcSize
103
- sig { returns(T.nilable(T::Array[T::Hash[String, T.untyped]])) }
108
+ sig { returns(T.nilable(T::Array[DependencyServicesResult::DependencyUpdate])) }
104
109
  def dependency_services_smallest_update
105
110
  return @smallest_update if @smallest_update
106
111
 
@@ -129,10 +134,12 @@ module Dependabot
129
134
  }
130
135
  ]
131
136
  }
132
- report = JSON.parse(run_dependency_services("report", stdin_data: JSON.generate(input)))["dependencies"]
137
+ report = DependencyServicesResult.report_from_json(
138
+ run_dependency_services("report", stdin_data: JSON.generate(input))
139
+ )
133
140
  @smallest_update = T.let(
134
- report.find { |d| d["name"] == dependency.name }["smallestUpdate"],
135
- T.nilable(T::Array[T::Hash[String, T.untyped]])
141
+ DependencyServicesResult.find_report(report.dependencies, dependency.name).smallest_update,
142
+ T.nilable(T::Array[DependencyServicesResult::DependencyUpdate])
136
143
  )
137
144
  end
138
145
  # rubocop:enable Metrics/AbcSize
@@ -184,11 +191,13 @@ module Dependabot
184
191
  report_section = if vulnerable?
185
192
  dependency_services_smallest_update
186
193
  else
187
- version_report.latest_version_resolvable_with_full_unlock_hash
194
+ version_report.full_unlock_updates
188
195
  end
196
+ return [] unless report_section
197
+
189
198
  # We only expose non-transitive dependencies here...
190
199
  direct_deps = report_section.reject do |d|
191
- d["kind"] == "transitive"
200
+ d.kind == "transitive"
192
201
  end
193
202
  direct_deps.map do |d|
194
203
  parse_updated_dependency(d, resolved_requirements_update_strategy)
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-pub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.398.0
4
+ version: 0.399.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.398.0
18
+ version: 0.399.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.398.0
25
+ version: 0.399.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -242,13 +242,16 @@ extensions: []
242
242
  extra_rdoc_files: []
243
243
  files:
244
244
  - lib/dependabot/pub.rb
245
+ - lib/dependabot/pub/dependency_services_result.rb
245
246
  - lib/dependabot/pub/file_fetcher.rb
246
247
  - lib/dependabot/pub/file_parser.rb
247
248
  - lib/dependabot/pub/file_updater.rb
248
249
  - lib/dependabot/pub/helpers.rb
250
+ - lib/dependabot/pub/json_value_parser.rb
249
251
  - lib/dependabot/pub/language.rb
250
252
  - lib/dependabot/pub/metadata_finder.rb
251
253
  - lib/dependabot/pub/package/package_details_fetcher.rb
254
+ - lib/dependabot/pub/package/registry_package.rb
252
255
  - lib/dependabot/pub/package_manager.rb
253
256
  - lib/dependabot/pub/requirement.rb
254
257
  - lib/dependabot/pub/requirement_source.rb
@@ -260,7 +263,7 @@ licenses:
260
263
  - MIT
261
264
  metadata:
262
265
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
263
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.398.0
266
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.399.0
264
267
  rdoc_options: []
265
268
  require_paths:
266
269
  - lib