dependabot-composer 0.397.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 +4 -4
- data/helpers/v2/composer.lock +22 -22
- data/lib/dependabot/composer/document_value_parser.rb +41 -0
- data/lib/dependabot/composer/file_fetcher/path_dependency_builder.rb +13 -17
- data/lib/dependabot/composer/file_fetcher.rb +21 -41
- data/lib/dependabot/composer/file_parser.rb +32 -35
- data/lib/dependabot/composer/file_updater/lockfile_updater.rb +14 -3
- data/lib/dependabot/composer/helpers.rb +17 -16
- data/lib/dependabot/composer/lockfile_document.rb +180 -0
- data/lib/dependabot/composer/manifest_document.rb +145 -0
- data/lib/dependabot/composer/package/package_details_fetcher.rb +18 -74
- data/lib/dependabot/composer/package/registry_package.rb +146 -0
- data/lib/dependabot/composer/update_checker/version_resolver.rb +13 -4
- metadata +8 -4
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "dependabot/composer/version"
|
|
5
|
+
require "dependabot/composer/manifest_document"
|
|
6
|
+
require "dependabot/composer/lockfile_document"
|
|
5
7
|
require "sorbet-runtime"
|
|
6
8
|
|
|
7
9
|
module Dependabot
|
|
@@ -31,20 +33,20 @@ module Dependabot
|
|
|
31
33
|
|
|
32
34
|
sig do
|
|
33
35
|
params(
|
|
34
|
-
composer_json:
|
|
35
|
-
parsed_lockfile: T.nilable(
|
|
36
|
+
composer_json: ManifestDocument,
|
|
37
|
+
parsed_lockfile: T.nilable(LockfileDocument)
|
|
36
38
|
)
|
|
37
39
|
.returns(String)
|
|
38
40
|
end
|
|
39
41
|
def self.composer_version(composer_json, parsed_lockfile = nil)
|
|
40
42
|
# If the parsed lockfile has a plugin API version, always use V2.
|
|
41
43
|
# V1 helpers have been removed, so we run with Composer V2 regardless.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
plugin_api_version = parsed_lockfile&.plugin_api_version
|
|
45
|
+
if plugin_api_version
|
|
46
|
+
version = Composer::Version.new(plugin_api_version)
|
|
44
47
|
major_version = version.canonical_segments.first
|
|
45
48
|
|
|
46
49
|
if major_version && major_version <= 1
|
|
47
|
-
plugin_api_version = parsed_lockfile[PackageManager::PLUGIN_API_VERSION_KEY]
|
|
48
50
|
Dependabot.logger.warn(
|
|
49
51
|
"Composer V1 lockfile detected (plugin-api-version: #{plugin_api_version}). " \
|
|
50
52
|
"Dependabot no longer supports Composer V1. Running with Composer V2."
|
|
@@ -56,7 +58,8 @@ module Dependabot
|
|
|
56
58
|
|
|
57
59
|
# Check if the composer name does not follow the Composer V2 naming conventions.
|
|
58
60
|
# This happens if "name" is present in composer.json but doesn't match the required pattern.
|
|
59
|
-
|
|
61
|
+
composer_name = composer_json.name
|
|
62
|
+
composer_name_invalid = composer_name && composer_name !~ COMPOSER_V2_NAME_REGEX
|
|
60
63
|
|
|
61
64
|
# If the name is invalid returns the fallback version.
|
|
62
65
|
return V2 if composer_name_invalid
|
|
@@ -141,34 +144,32 @@ module Dependabot
|
|
|
141
144
|
end
|
|
142
145
|
|
|
143
146
|
# Capture the platform PHP version from composer.json
|
|
144
|
-
sig { params(parsed_composer_json:
|
|
147
|
+
sig { params(parsed_composer_json: ManifestDocument).returns(T.nilable(String)) }
|
|
145
148
|
def self.capture_platform_php(parsed_composer_json)
|
|
146
149
|
capture_platform(parsed_composer_json, Language::NAME)
|
|
147
150
|
end
|
|
148
151
|
|
|
149
152
|
# Capture the platform extension from composer.json
|
|
150
|
-
sig { params(parsed_composer_json:
|
|
153
|
+
sig { params(parsed_composer_json: ManifestDocument, name: String).returns(T.nilable(String)) }
|
|
151
154
|
def self.capture_platform(parsed_composer_json, name)
|
|
152
|
-
parsed_composer_json.
|
|
155
|
+
parsed_composer_json.platform(name)
|
|
153
156
|
end
|
|
154
157
|
|
|
155
158
|
# Capture PHP version constraint from composer.json
|
|
156
|
-
sig { params(parsed_composer_json:
|
|
159
|
+
sig { params(parsed_composer_json: ManifestDocument).returns(T.nilable(String)) }
|
|
157
160
|
def self.php_constraint(parsed_composer_json)
|
|
158
161
|
dependency_constraint(parsed_composer_json, Language::NAME)
|
|
159
162
|
end
|
|
160
163
|
|
|
161
164
|
# Capture extension version constraint from composer.json
|
|
162
|
-
sig { params(parsed_composer_json:
|
|
165
|
+
sig { params(parsed_composer_json: ManifestDocument, name: String).returns(T.nilable(String)) }
|
|
163
166
|
def self.dependency_constraint(parsed_composer_json, name)
|
|
164
|
-
parsed_composer_json.
|
|
167
|
+
parsed_composer_json.dependency_constraint(name)
|
|
165
168
|
end
|
|
166
169
|
|
|
167
|
-
sig { params(composer_json:
|
|
170
|
+
sig { params(composer_json: ManifestDocument).returns(T::Boolean) }
|
|
168
171
|
def self.invalid_v2_requirement?(composer_json)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
composer_json[PackageManager::REQUIRE_KEY].keys.any? do |key|
|
|
172
|
+
composer_json.required_dependency_names.any? do |key|
|
|
172
173
|
key !~ PLATFORM_PACKAGE_REGEX && key !~ COMPOSER_V2_NAME_REGEX
|
|
173
174
|
end
|
|
174
175
|
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
require "dependabot/dependency_file"
|
|
8
|
+
require "dependabot/dependency_requirement"
|
|
9
|
+
require "dependabot/composer/document_value_parser"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Composer
|
|
13
|
+
class LockfileDocument
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
class PackageRecord
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
sig { params(data: DocumentValueParser::ObjectHash, context: String).void }
|
|
20
|
+
def initialize(data:, context:)
|
|
21
|
+
@data = data
|
|
22
|
+
@context = context
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { returns(T.nilable(String)) }
|
|
26
|
+
def name
|
|
27
|
+
value = @data["name"]
|
|
28
|
+
value if value.is_a?(String)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { returns(T.nilable(String)) }
|
|
32
|
+
def version
|
|
33
|
+
@data["version"]&.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { returns(T.nilable(String)) }
|
|
37
|
+
def required_version
|
|
38
|
+
@data.fetch("version")&.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { returns(T::Boolean) }
|
|
42
|
+
def path_source?
|
|
43
|
+
@data["source"].nil? && path_distribution?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { returns(T::Boolean) }
|
|
47
|
+
def path_distribution?
|
|
48
|
+
section("dist")&.[]("type") == "path"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
52
|
+
def matches_distribution_path?(path)
|
|
53
|
+
section("dist")&.[]("url") == path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { params(prefix: String).returns(T::Boolean) }
|
|
57
|
+
def dist_url_starts_with?(prefix)
|
|
58
|
+
section("dist")&.[]("url").to_s.start_with?(prefix)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { returns(String) }
|
|
62
|
+
def dist_url
|
|
63
|
+
DocumentValueParser.string(section("dist")&.[]("url"), "#{@context} dist.url")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { returns(String) }
|
|
67
|
+
def to_manifest_json
|
|
68
|
+
@data.to_json
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { returns(T.nilable(String)) }
|
|
72
|
+
def source_reference
|
|
73
|
+
DocumentValueParser.optional_string(section("source")&.[]("reference"), "#{@context} source.reference")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { returns(T.nilable(Dependabot::DependencyRequirement::ObjectHash)) }
|
|
77
|
+
def git_source
|
|
78
|
+
source = section("source")
|
|
79
|
+
return unless source && source["type"] == "git"
|
|
80
|
+
|
|
81
|
+
# The URL is passed through as source metadata, not interpreted by the file parser.
|
|
82
|
+
{ type: "git", url: source["url"] }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
sig { params(key: String).returns(T.nilable(DocumentValueParser::ObjectHash)) }
|
|
88
|
+
def section(key)
|
|
89
|
+
value = @data[key]
|
|
90
|
+
return if value.nil?
|
|
91
|
+
|
|
92
|
+
DocumentValueParser.object_hash(value, "#{@context} #{key}")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { params(file: Dependabot::DependencyFile).returns(LockfileDocument) }
|
|
97
|
+
def self.from_file(file)
|
|
98
|
+
new(data: T.cast(JSON.parse(T.must(file.content)), Object), context: file.path)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(data: Object, context: String).void }
|
|
102
|
+
def initialize(data:, context:)
|
|
103
|
+
@data = T.let(DocumentValueParser.object_hash(data, context), DocumentValueParser::ObjectHash)
|
|
104
|
+
@context = context
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { returns(T.nilable(T.any(String, Integer))) }
|
|
108
|
+
def plugin_api_version
|
|
109
|
+
value = @data["plugin-api-version"]
|
|
110
|
+
return unless value
|
|
111
|
+
return value if value.is_a?(String) || value.is_a?(Integer)
|
|
112
|
+
|
|
113
|
+
raise TypeError, "#{@context} plugin-api-version must be a string or integer"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sig { params(group: String).returns(T::Array[PackageRecord]) }
|
|
117
|
+
def packages(group)
|
|
118
|
+
entries = @data[group]
|
|
119
|
+
return [] unless entries.is_a?(Array)
|
|
120
|
+
|
|
121
|
+
entries.each_with_index.filter_map { |entry, index| package_record(T.cast(entry, Object), group, index) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
sig { params(group: String, name: String).returns(T.nilable(PackageRecord)) }
|
|
125
|
+
def find_package(group, name)
|
|
126
|
+
entries = @data.fetch(group, [])
|
|
127
|
+
return if entries.nil?
|
|
128
|
+
raise TypeError, "#{@context} #{group} must be an array" unless entries.is_a?(Array)
|
|
129
|
+
|
|
130
|
+
entries.each_with_index do |entry, index|
|
|
131
|
+
package = package_record(T.cast(entry, Object), group, index)
|
|
132
|
+
return package if package&.name == name
|
|
133
|
+
end
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
sig { params(group: String).returns(T::Array[PackageRecord]) }
|
|
138
|
+
def path_packages(group)
|
|
139
|
+
distribution_entries(group).each_with_index.filter_map do |entry, index|
|
|
140
|
+
package = strict_package_record(entry, group, index)
|
|
141
|
+
package if package.path_distribution?
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
sig { params(group: String, path: String).returns(T.nilable(PackageRecord)) }
|
|
146
|
+
def find_path_package(group, path)
|
|
147
|
+
distribution_entries(group).each_with_index do |entry, index|
|
|
148
|
+
package = strict_package_record(entry, group, index)
|
|
149
|
+
return package if package.matches_distribution_path?(path)
|
|
150
|
+
end
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
sig { params(group: String).returns(T::Array[Object]) }
|
|
157
|
+
def distribution_entries(group)
|
|
158
|
+
entries = @data[group]
|
|
159
|
+
return [] unless entries
|
|
160
|
+
return [] if entries.is_a?(Hash) && entries.empty?
|
|
161
|
+
raise TypeError, "#{@context} #{group} must be an array" unless entries.is_a?(Array)
|
|
162
|
+
|
|
163
|
+
entries.map { |entry| T.cast(entry, Object) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
sig { params(value: Object, group: String, index: Integer).returns(T.nilable(PackageRecord)) }
|
|
167
|
+
def package_record(value, group, index)
|
|
168
|
+
return if value.is_a?(String)
|
|
169
|
+
|
|
170
|
+
strict_package_record(value, group, index)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
sig { params(value: Object, group: String, index: Integer).returns(PackageRecord) }
|
|
174
|
+
def strict_package_record(value, group, index)
|
|
175
|
+
context = "#{@context} #{group}[#{index}]"
|
|
176
|
+
PackageRecord.new(data: DocumentValueParser.object_hash(value, context), context: context)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
require "dependabot/dependency_file"
|
|
8
|
+
require "dependabot/composer/document_value_parser"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Composer
|
|
12
|
+
class ManifestDocument
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
class DependencyEntry
|
|
16
|
+
extend T::Sig
|
|
17
|
+
|
|
18
|
+
sig { params(name: String, value: Object, context: String).void }
|
|
19
|
+
def initialize(name:, value:, context:)
|
|
20
|
+
@name = name
|
|
21
|
+
@value = value
|
|
22
|
+
@context = context
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { returns(String) }
|
|
26
|
+
attr_reader :name
|
|
27
|
+
|
|
28
|
+
sig { returns(T.nilable(String)) }
|
|
29
|
+
def string_requirement
|
|
30
|
+
value = @value
|
|
31
|
+
value if value.is_a?(String)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { returns(String) }
|
|
35
|
+
def requirement
|
|
36
|
+
DocumentValueParser.string(@value, @context)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class Repository
|
|
41
|
+
extend T::Sig
|
|
42
|
+
|
|
43
|
+
sig { params(type: String, url: Object, context: String).void }
|
|
44
|
+
def initialize(type:, url:, context:)
|
|
45
|
+
@type = type
|
|
46
|
+
@url = url
|
|
47
|
+
@context = context
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { returns(String) }
|
|
51
|
+
attr_reader :type
|
|
52
|
+
|
|
53
|
+
sig { returns(String) }
|
|
54
|
+
def url
|
|
55
|
+
DocumentValueParser.string(@url, "#{@context} url")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { params(file: Dependabot::DependencyFile).returns(ManifestDocument) }
|
|
60
|
+
def self.from_file(file)
|
|
61
|
+
new(data: T.cast(JSON.parse(T.must(file.content)), Object), context: file.path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { params(data: Object, context: String).void }
|
|
65
|
+
def initialize(data:, context:)
|
|
66
|
+
@data = T.let(DocumentValueParser.object_hash(data, context), DocumentValueParser::ObjectHash)
|
|
67
|
+
@context = context
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { returns(T.nilable(String)) }
|
|
71
|
+
def name
|
|
72
|
+
value = @data["name"]
|
|
73
|
+
return unless value
|
|
74
|
+
|
|
75
|
+
DocumentValueParser.string(value, "#{@context} name")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { params(group: String).returns(T::Array[DependencyEntry]) }
|
|
79
|
+
def requirements(group)
|
|
80
|
+
value = @data[group]
|
|
81
|
+
return [] unless value.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
DocumentValueParser.object_hash(value, "#{@context} #{group}").map do |name, requirement|
|
|
84
|
+
DependencyEntry.new(name: name, value: requirement, context: "#{@context} #{group} requirement")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig { returns(T::Array[String]) }
|
|
89
|
+
def required_dependency_names
|
|
90
|
+
return [] unless @data.key?("require")
|
|
91
|
+
|
|
92
|
+
DocumentValueParser.object_hash(@data.fetch("require"), "#{@context} require").keys
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { params(name: String).returns(T.nilable(String)) }
|
|
96
|
+
def platform(name)
|
|
97
|
+
config = section(@data, "config", "config")
|
|
98
|
+
platform = config && section(config, "platform", "config.platform")
|
|
99
|
+
DocumentValueParser.optional_string(platform&.[](name), "#{@context} config.platform.#{name}")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
sig { params(name: String).returns(T.nilable(String)) }
|
|
103
|
+
def dependency_constraint(name)
|
|
104
|
+
requirements = section(@data, "require", "require")
|
|
105
|
+
DocumentValueParser.optional_string(requirements&.[](name), "#{@context} require.#{name}")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { returns(T::Array[Repository]) }
|
|
109
|
+
def repositories
|
|
110
|
+
value = @data["repositories"]
|
|
111
|
+
entries =
|
|
112
|
+
case value
|
|
113
|
+
when Hash then value.values
|
|
114
|
+
when Array then value
|
|
115
|
+
else return []
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
entries.each_with_index.filter_map do |raw_entry, index|
|
|
119
|
+
entry = T.cast(raw_entry, Object)
|
|
120
|
+
next unless entry.is_a?(Hash)
|
|
121
|
+
|
|
122
|
+
context = "#{@context} repositories[#{index}]"
|
|
123
|
+
details = DocumentValueParser.object_hash(entry, context)
|
|
124
|
+
type = details["type"]
|
|
125
|
+
next unless type.is_a?(String) && (type == "path" || type == "artifact")
|
|
126
|
+
|
|
127
|
+
Repository.new(type: type, url: details["url"], context: context)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
sig do
|
|
134
|
+
params(data: DocumentValueParser::ObjectHash, key: String, field: String)
|
|
135
|
+
.returns(T.nilable(DocumentValueParser::ObjectHash))
|
|
136
|
+
end
|
|
137
|
+
def section(data, key, field)
|
|
138
|
+
value = data[key]
|
|
139
|
+
return if value.nil?
|
|
140
|
+
|
|
141
|
+
DocumentValueParser.object_hash(value, "#{@context} #{field}")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
|
-
require "time"
|
|
6
5
|
require "excon"
|
|
7
6
|
require "nokogiri"
|
|
8
7
|
require "sorbet-runtime"
|
|
9
8
|
require "dependabot/registry_client"
|
|
10
9
|
require "dependabot/package/package_release"
|
|
11
10
|
require "dependabot/package/package_details"
|
|
11
|
+
require "dependabot/composer/package/registry_package"
|
|
12
12
|
|
|
13
13
|
# Stores metadata for a package, including all its available versions
|
|
14
14
|
module Dependabot
|
|
@@ -46,7 +46,7 @@ module Dependabot
|
|
|
46
46
|
@security_advisories = security_advisories
|
|
47
47
|
|
|
48
48
|
@registry_urls = T.let(nil, T.nilable(T::Array[String]))
|
|
49
|
-
@registry_version_details = T.let(nil, T.nilable(T::Array[
|
|
49
|
+
@registry_version_details = T.let(nil, T.nilable(T::Array[RegistryPackage::Release]))
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
sig { returns(Dependabot::Dependency) }
|
|
@@ -66,38 +66,18 @@ module Dependabot
|
|
|
66
66
|
|
|
67
67
|
sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
|
|
68
68
|
def fetch_releases
|
|
69
|
-
|
|
70
|
-
.select do |version_details|
|
|
71
|
-
version = version_details.fetch("version")
|
|
72
|
-
version && version_class.correct?(version.gsub(/^v/, ""))
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
releases = available_version_details.map do |version_details|
|
|
76
|
-
format_version_release(version_details)
|
|
77
|
-
end
|
|
78
|
-
releases
|
|
69
|
+
registry_version_details.filter_map { |release| format_version_release(release) }
|
|
79
70
|
end
|
|
80
71
|
|
|
81
72
|
sig { returns(Dependabot::Package::PackageDetails) }
|
|
82
73
|
def fetch
|
|
83
|
-
available_version_details = registry_version_details
|
|
84
|
-
.select do |version_details|
|
|
85
|
-
version = version_details.fetch("version")
|
|
86
|
-
version && version_class.correct?(version.gsub(/^v/, ""))
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
releases = available_version_details.map do |version_details|
|
|
90
|
-
format_version_release(version_details)
|
|
91
|
-
end
|
|
92
74
|
Dependabot::Package::PackageDetails.new(
|
|
93
75
|
dependency: dependency,
|
|
94
|
-
releases:
|
|
76
|
+
releases: fetch_releases.reverse.uniq(&:version)
|
|
95
77
|
)
|
|
96
78
|
end
|
|
97
79
|
|
|
98
|
-
sig
|
|
99
|
-
returns(T::Array[T::Hash[String, T.untyped]])
|
|
100
|
-
end
|
|
80
|
+
sig { returns(T::Array[RegistryPackage::Release]) }
|
|
101
81
|
def registry_version_details
|
|
102
82
|
return @registry_version_details unless @registry_version_details.nil?
|
|
103
83
|
|
|
@@ -114,16 +94,15 @@ module Dependabot
|
|
|
114
94
|
urls << "https://repo.packagist.org/p2/#{dependency.name.downcase}.json"
|
|
115
95
|
end
|
|
116
96
|
|
|
117
|
-
|
|
97
|
+
releases = T.let([], T::Array[RegistryPackage::Release])
|
|
118
98
|
urls.each do |url|
|
|
119
|
-
|
|
99
|
+
releases.concat(fetch_registry_versions_from_url(url))
|
|
120
100
|
end
|
|
121
101
|
|
|
122
|
-
@registry_version_details.uniq
|
|
123
|
-
@registry_version_details
|
|
102
|
+
@registry_version_details = releases.uniq(&:version_string)
|
|
124
103
|
end
|
|
125
104
|
|
|
126
|
-
sig { params(url: String).returns(T::Array[
|
|
105
|
+
sig { params(url: String).returns(T::Array[RegistryPackage::Release]) }
|
|
127
106
|
def fetch_registry_versions_from_url(url)
|
|
128
107
|
url_host = URI(url).host
|
|
129
108
|
cred = registry_credentials.find do |c|
|
|
@@ -146,60 +125,25 @@ module Dependabot
|
|
|
146
125
|
[]
|
|
147
126
|
end
|
|
148
127
|
|
|
149
|
-
sig { params(response:
|
|
128
|
+
sig { params(response: Excon::Response, url: String).returns(T::Array[RegistryPackage::Release]) }
|
|
150
129
|
def parse_registry_response(response, url)
|
|
151
130
|
return [] unless response.status == 200
|
|
152
131
|
|
|
153
|
-
|
|
154
|
-
return [] if listing.nil?
|
|
155
|
-
return [] unless listing.is_a?(Hash)
|
|
156
|
-
return [] if listing.fetch("packages", []) == []
|
|
157
|
-
return [] unless listing.dig("packages", dependency.name.downcase)
|
|
158
|
-
|
|
159
|
-
extract_versions(listing)
|
|
160
|
-
rescue JSON::ParserError
|
|
161
|
-
msg = "'#{url}' does not contain valid JSON"
|
|
162
|
-
raise DependencyFileNotResolvable, msg
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
sig { params(listing: T::Hash[String, T.untyped]).returns(T::Array[T::Hash[String, T.untyped]]) }
|
|
166
|
-
def extract_versions(listing)
|
|
167
|
-
# Packagist's Metadata API format:
|
|
168
|
-
# v1: "packages": {<package name>: {<version_number>: {hash of metadata for a particular release version}}}
|
|
169
|
-
# v2: "packages": {<package name>: [{hash of metadata for a particular release version}]}
|
|
170
|
-
version_listings = listing.dig("packages", dependency.name.downcase)
|
|
171
|
-
|
|
172
|
-
if version_listings.is_a?(Hash) # some private registries are still using the v1 format
|
|
173
|
-
# Regardless of API version, composer always reads the version from the metadata hash. So for the v1 API,
|
|
174
|
-
# ignore the keys as repositories other than packagist.org could be using different keys. Instead, coerce
|
|
175
|
-
# to an array of metadata hashes to match v2 format.
|
|
176
|
-
version_listings = version_listings.values
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
if version_listings.is_a?(Array)
|
|
180
|
-
version_listings
|
|
181
|
-
else
|
|
182
|
-
[]
|
|
183
|
-
end
|
|
132
|
+
RegistryPackage.from_json(response.body, package_name: dependency.name, source: url).releases
|
|
184
133
|
end
|
|
185
134
|
|
|
186
135
|
sig do
|
|
187
|
-
params(
|
|
188
|
-
release_data: T::Hash[String, T.untyped]
|
|
189
|
-
)
|
|
190
|
-
.returns(Dependabot::Package::PackageRelease)
|
|
136
|
+
params(release_data: RegistryPackage::Release).returns(T.nilable(Dependabot::Package::PackageRelease))
|
|
191
137
|
end
|
|
192
138
|
def format_version_release(release_data)
|
|
193
|
-
version = release_data
|
|
194
|
-
|
|
195
|
-
url = release_data["dist"] ? release_data["dist"]["url"] : nil
|
|
196
|
-
package_type = PACKAGE_TYPE
|
|
139
|
+
version = release_data.version_string
|
|
140
|
+
return unless version && version_class.correct?(version.gsub(/^v/, ""))
|
|
197
141
|
|
|
198
142
|
package_release(
|
|
199
|
-
version: version,
|
|
200
|
-
released_at: released_at,
|
|
201
|
-
url: url,
|
|
202
|
-
package_type:
|
|
143
|
+
version: version.gsub(/^v/, ""),
|
|
144
|
+
released_at: release_data.released_at,
|
|
145
|
+
url: release_data.url,
|
|
146
|
+
package_type: PACKAGE_TYPE
|
|
203
147
|
)
|
|
204
148
|
end
|
|
205
149
|
|