dependabot-composer 0.89.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/composer/file_updater"
4
+
5
+ module Dependabot
6
+ module Composer
7
+ class FileUpdater
8
+ class ManifestUpdater
9
+ def initialize(dependencies:, manifest:)
10
+ @dependencies = dependencies
11
+ @manifest = manifest
12
+ end
13
+
14
+ def updated_manifest_content
15
+ dependencies.reduce(manifest.content.dup) do |content, dep|
16
+ updated_content = content
17
+ updated_requirements(dep).each do |new_req|
18
+ old_req = old_requirement(dep, new_req).fetch(:requirement)
19
+ updated_req = new_req.fetch(:requirement)
20
+
21
+ regex =
22
+ /
23
+ "#{Regexp.escape(dep.name)}"\s*:\s*
24
+ "#{Regexp.escape(old_req)}"
25
+ /x
26
+
27
+ updated_content = content.gsub(regex) do |declaration|
28
+ declaration.gsub(%("#{old_req}"), %("#{updated_req}"))
29
+ end
30
+
31
+ raise "Expected content to change!" if content == updated_content
32
+ end
33
+
34
+ updated_content
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :dependencies, :manifest
41
+
42
+ def new_requirements(dependency)
43
+ dependency.requirements.select { |r| r[:file] == manifest.name }
44
+ end
45
+
46
+ def old_requirement(dependency, new_requirement)
47
+ dependency.previous_requirements.
48
+ select { |r| r[:file] == manifest.name }.
49
+ find { |r| r[:groups] == new_requirement[:groups] }
50
+ end
51
+
52
+ def updated_requirements(dependency)
53
+ new_requirements(dependency).
54
+ reject { |r| dependency.previous_requirements.include?(r) }
55
+ end
56
+
57
+ def requirement_changed?(file, dependency)
58
+ changed_requirements =
59
+ dependency.requirements - dependency.previous_requirements
60
+
61
+ changed_requirements.any? { |f| f[:file] == file.name }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/metadata_finders"
5
+ require "dependabot/metadata_finders/base"
6
+ require "dependabot/shared_helpers"
7
+ require "dependabot/composer/version"
8
+
9
+ module Dependabot
10
+ module Composer
11
+ class MetadataFinder < Dependabot::MetadataFinders::Base
12
+ private
13
+
14
+ def look_up_source
15
+ source_from_dependency || look_up_source_from_packagist
16
+ end
17
+
18
+ def source_from_dependency
19
+ source_url =
20
+ dependency.requirements.
21
+ map { |r| r.fetch(:source) }.compact.
22
+ first&.fetch(:url, nil)
23
+
24
+ Source.from_url(source_url)
25
+ end
26
+
27
+ def look_up_source_from_packagist
28
+ return nil if packagist_listing&.fetch("packages", nil) == []
29
+ unless packagist_listing&.dig("packages", dependency.name.downcase)
30
+ return nil
31
+ end
32
+
33
+ version_listings =
34
+ packagist_listing["packages"][dependency.name.downcase].
35
+ select { |version, _| Composer::Version.correct?(version) }.
36
+ sort_by { |version, _| Composer::Version.new(version) }.
37
+ map { |_, listing| listing }.
38
+ reverse
39
+
40
+ potential_source_urls =
41
+ version_listings.
42
+ flat_map { |info| [info["homepage"], info.dig("source", "url")] }.
43
+ compact
44
+
45
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
46
+
47
+ Source.from_url(source_url)
48
+ end
49
+
50
+ def packagist_listing
51
+ return @packagist_listing unless @packagist_listing.nil?
52
+
53
+ response = Excon.get(
54
+ "https://packagist.org/p/#{dependency.name.downcase}.json",
55
+ idempotent: true,
56
+ **SharedHelpers.excon_defaults
57
+ )
58
+
59
+ return nil unless response.status == 200
60
+
61
+ @packagist_listing = JSON.parse(response.body)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ Dependabot::MetadataFinders.
68
+ register("composer", Dependabot::Composer::MetadataFinder)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dependabot
4
+ module Composer
5
+ module NativeHelpers
6
+ def self.composer_helper_path
7
+ File.join(composer_helpers_dir, "bin/run.php")
8
+ end
9
+
10
+ def self.composer_helpers_dir
11
+ File.join(native_helpers_root, "composer/helpers")
12
+ end
13
+
14
+ def self.native_helpers_root
15
+ default_path = File.join(__dir__, "../../../..")
16
+ ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+
5
+ module Dependabot
6
+ module Composer
7
+ class Requirement < Gem::Requirement
8
+ AND_SEPARATOR =
9
+ /(?<=[a-zA-Z0-9*])(?<!\sas)[\s,]+(?![\s,]*[|-]|as)/.freeze
10
+ OR_SEPARATOR = /(?<=[a-zA-Z0-9*])[\s,]*\|\|?\s*/.freeze
11
+
12
+ def self.parse(obj)
13
+ new_obj = obj.gsub(/@\w+/, "").gsub(/[a-z0-9\-_\.]*\sas\s+/i, "")
14
+ super(new_obj)
15
+ end
16
+
17
+ # Returns an array of requirements. At least one requirement from the
18
+ # returned array must be satisfied for a version to be valid.
19
+ def self.requirements_array(requirement_string)
20
+ requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
21
+ new(req_string)
22
+ end
23
+ end
24
+
25
+ def initialize(*requirements)
26
+ requirements =
27
+ requirements.flatten.
28
+ flat_map { |req_string| req_string.split(AND_SEPARATOR) }.
29
+ flat_map { |req| convert_php_constraint_to_ruby_constraint(req) }
30
+
31
+ super(requirements)
32
+ end
33
+
34
+ private
35
+
36
+ # rubocop:disable Metrics/PerceivedComplexity
37
+ def convert_php_constraint_to_ruby_constraint(req_string)
38
+ req_string = req_string.gsub(/v(?=\d)/, "")
39
+
40
+ # Return an unlikely version if a dev requirement is specified. This
41
+ # ensures that the dev-requirement doesn't match anything.
42
+ return "0-dev-branch-match" if req_string.strip.start_with?("dev-")
43
+
44
+ if req_string.start_with?("*") then ">= 0"
45
+ elsif req_string.include?("*") then convert_wildcard_req(req_string)
46
+ elsif req_string.match?(/^~[^>]/) then convert_tilde_req(req_string)
47
+ elsif req_string.start_with?("^") then convert_caret_req(req_string)
48
+ elsif req_string.match?(/\s-\s/) then convert_hyphen_req(req_string)
49
+ else req_string
50
+ end
51
+ end
52
+ # rubocop:enable Metrics/PerceivedComplexity
53
+
54
+ def convert_wildcard_req(req_string)
55
+ version = req_string.gsub(/^~/, "").gsub(/(?:\.|^)\*/, "")
56
+ "~> #{version}.0"
57
+ end
58
+
59
+ def convert_tilde_req(req_string)
60
+ version = req_string.gsub(/^~/, "")
61
+ "~> #{version}"
62
+ end
63
+
64
+ def convert_caret_req(req_string)
65
+ version = req_string.gsub(/^\^/, "")
66
+ parts = version.split(".")
67
+ first_non_zero = parts.find { |d| d != "0" }
68
+ first_non_zero_index =
69
+ first_non_zero ? parts.index(first_non_zero) : parts.count - 1
70
+ upper_bound = parts.map.with_index do |part, i|
71
+ if i < first_non_zero_index then part
72
+ elsif i == first_non_zero_index then (part.to_i + 1).to_s
73
+ else 0
74
+ end
75
+ end.join(".")
76
+
77
+ [">= #{version}", "< #{upper_bound}"]
78
+ end
79
+
80
+ def convert_hyphen_req(req_string)
81
+ req_string = req_string
82
+ lower_bound, upper_bound = req_string.split(/\s+-\s+/)
83
+ if upper_bound.split(".").count < 3
84
+ upper_bound_parts = upper_bound.split(".")
85
+ upper_bound_parts[-1] = (upper_bound_parts[-1].to_i + 1).to_s
86
+ upper_bound = upper_bound_parts.join(".")
87
+
88
+ [">= #{lower_bound}", "< #{upper_bound}"]
89
+ else
90
+ [">= #{lower_bound}", "<= #{upper_bound}"]
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ Dependabot::Utils.
98
+ register_requirement_class("composer", Dependabot::Composer::Requirement)
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "json"
5
+ require "dependabot/update_checkers"
6
+ require "dependabot/update_checkers/base"
7
+ require "dependabot/shared_helpers"
8
+ require "dependabot/errors"
9
+
10
+ module Dependabot
11
+ module Composer
12
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
13
+ require_relative "update_checker/requirements_updater"
14
+ require_relative "update_checker/version_resolver"
15
+
16
+ def latest_version
17
+ return nil if path_dependency?
18
+
19
+ # Fall back to latest_resolvable_version if no listings found
20
+ latest_version_from_registry || latest_resolvable_version
21
+ end
22
+
23
+ def latest_resolvable_version
24
+ return nil if path_dependency?
25
+
26
+ @latest_resolvable_version ||=
27
+ VersionResolver.new(
28
+ credentials: credentials,
29
+ dependency: dependency,
30
+ dependency_files: dependency_files,
31
+ latest_allowable_version: latest_version_from_registry,
32
+ requirements_to_unlock: :own
33
+ ).latest_resolvable_version
34
+ end
35
+
36
+ def latest_resolvable_version_with_no_unlock
37
+ return nil if path_dependency?
38
+
39
+ @latest_resolvable_version_with_no_unlock ||=
40
+ VersionResolver.new(
41
+ credentials: credentials,
42
+ dependency: dependency,
43
+ dependency_files: dependency_files,
44
+ latest_allowable_version: latest_version_from_registry,
45
+ requirements_to_unlock: :none
46
+ ).latest_resolvable_version
47
+ end
48
+
49
+ def updated_requirements
50
+ RequirementsUpdater.new(
51
+ requirements: dependency.requirements,
52
+ latest_version: latest_version&.to_s,
53
+ latest_resolvable_version: latest_resolvable_version&.to_s,
54
+ update_strategy: requirements_update_strategy
55
+ ).updated_requirements
56
+ end
57
+
58
+ def requirements_update_strategy
59
+ # If passed in as an option (in the base class) honour that option
60
+ if @requirements_update_strategy
61
+ return @requirements_update_strategy.to_sym
62
+ end
63
+
64
+ # Otherwise, widen ranges for libraries and bump versions for apps
65
+ library? ? :widen_ranges : :bump_versions_if_necessary
66
+ end
67
+
68
+ private
69
+
70
+ def latest_version_resolvable_with_full_unlock?
71
+ # Full unlock checks aren't implemented for Composer (yet)
72
+ false
73
+ end
74
+
75
+ def latest_version_from_registry
76
+ versions =
77
+ registry_versions.
78
+ select { |version| version_class.correct?(version.gsub(/^v/, "")) }.
79
+ map { |version| version_class.new(version.gsub(/^v/, "")) }
80
+
81
+ versions.reject!(&:prerelease?) unless wants_prerelease?
82
+ versions.reject! { |v| ignore_reqs.any? { |r| r.satisfied_by?(v) } }
83
+ versions.max
84
+ end
85
+
86
+ def wants_prerelease?
87
+ current_version = dependency.version
88
+ if current_version && version_class.new(current_version).prerelease?
89
+ return true
90
+ end
91
+
92
+ dependency.requirements.any? do |req|
93
+ req[:requirement].match?(/\d-[A-Za-z]/)
94
+ end
95
+ end
96
+
97
+ def updated_dependencies_after_full_unlock
98
+ raise NotImplementedError
99
+ end
100
+
101
+ def path_dependency?
102
+ dependency.requirements.any? { |r| r.dig(:source, :type) == "path" }
103
+ end
104
+
105
+ def composer_file
106
+ composer_file =
107
+ dependency_files.find { |f| f.name == "composer.json" }
108
+ raise "No composer.json!" unless composer_file
109
+
110
+ composer_file
111
+ end
112
+
113
+ def lockfile
114
+ dependency_files.find { |f| f.name == "composer.lock" }
115
+ end
116
+
117
+ def registry_versions
118
+ return @registry_versions unless @registry_versions.nil?
119
+
120
+ repositories =
121
+ JSON.parse(composer_file.content).
122
+ fetch("repositories", []).
123
+ select { |r| r.is_a?(Hash) }
124
+
125
+ urls = repositories.
126
+ select { |h| h["type"] == "composer" }.
127
+ map { |h| h["url"] }.compact.
128
+ map { |url| url.gsub(%r{\/$}, "") + "/packages.json" }
129
+
130
+ unless repositories.any? { |rep| rep["packagist.org"] == false }
131
+ urls << "https://packagist.org/p/#{dependency.name.downcase}.json"
132
+ end
133
+
134
+ @registry_versions = []
135
+ urls.each do |url|
136
+ @registry_versions += fetch_registry_versions_from_url(url)
137
+ end
138
+ @registry_versions.uniq
139
+ end
140
+
141
+ def fetch_registry_versions_from_url(url)
142
+ cred = registry_credentials.find { |c| url.include?(c["registry"]) }
143
+
144
+ response = Excon.get(
145
+ url,
146
+ idempotent: true,
147
+ user: cred&.fetch("username", nil),
148
+ password: cred&.fetch("password", nil),
149
+ **SharedHelpers.excon_defaults
150
+ )
151
+
152
+ return [] unless response.status == 200
153
+
154
+ listing = JSON.parse(response.body)
155
+ return [] if listing.nil?
156
+ return [] if listing.fetch("packages", []) == []
157
+ return [] unless listing.dig("packages", dependency.name.downcase)
158
+
159
+ listing.dig("packages", dependency.name.downcase).keys
160
+ rescue Excon::Error::Socket, Excon::Error::Timeout
161
+ []
162
+ end
163
+
164
+ def library?
165
+ JSON.parse(composer_file.content)["type"] == "library"
166
+ end
167
+
168
+ def registry_credentials
169
+ credentials.select { |cred| cred["type"] == "composer_repository" }
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ Dependabot::UpdateCheckers.
176
+ register("composer", Dependabot::Composer::UpdateChecker)
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ ################################################################################
4
+ # For more details on Composer version constraints, see: #
5
+ # https://getcomposer.org/doc/articles/versions.md#writing-version-constraints #
6
+ ################################################################################
7
+
8
+ require "dependabot/composer/update_checker"
9
+ require "dependabot/composer/version"
10
+ require "dependabot/composer/requirement"
11
+
12
+ module Dependabot
13
+ module Composer
14
+ class UpdateChecker
15
+ class RequirementsUpdater
16
+ ALIAS_REGEX = /[a-z0-9\-_\.]*\sas\s+/.freeze
17
+ VERSION_REGEX =
18
+ /(?:#{ALIAS_REGEX})?[0-9]+(?:\.[a-zA-Z0-9*\-]+)*/.freeze
19
+ AND_SEPARATOR =
20
+ /(?<=[a-zA-Z0-9*])(?<!\sas)[\s,]+(?![\s,]*[|-]|as)/.freeze
21
+ OR_SEPARATOR = /(?<=[a-zA-Z0-9*])[\s,]*\|\|?\s*/.freeze
22
+ SEPARATOR = /(?:#{AND_SEPARATOR})|(?:#{OR_SEPARATOR})/.freeze
23
+ ALLOWED_UPDATE_STRATEGIES =
24
+ %i(widen_ranges bump_versions bump_versions_if_necessary).freeze
25
+
26
+ def initialize(requirements:, update_strategy:,
27
+ latest_version:, latest_resolvable_version:)
28
+ @requirements = requirements
29
+ @update_strategy = update_strategy
30
+
31
+ check_update_strategy
32
+
33
+ @latest_version = version_class.new(latest_version) if latest_version
34
+ return unless latest_resolvable_version
35
+
36
+ @latest_resolvable_version =
37
+ version_class.new(latest_resolvable_version)
38
+ end
39
+
40
+ def updated_requirements
41
+ return requirements unless latest_resolvable_version
42
+
43
+ requirements.map { |req| updated_requirement(req) }
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :requirements, :update_strategy,
49
+ :latest_version, :latest_resolvable_version
50
+
51
+ def check_update_strategy
52
+ return if ALLOWED_UPDATE_STRATEGIES.include?(update_strategy)
53
+
54
+ raise "Unknown update strategy: #{update_strategy}"
55
+ end
56
+
57
+ # rubocop:disable Metrics/PerceivedComplexity
58
+ # rubocop:disable Metrics/CyclomaticComplexity
59
+ def updated_requirement(req)
60
+ req_string = req[:requirement].strip
61
+ or_string_reqs = req_string.split(OR_SEPARATOR)
62
+ or_separator = req_string.match(OR_SEPARATOR)&.to_s || " || "
63
+ numeric_or_string_reqs = or_string_reqs.
64
+ reject { |r| r.start_with?("dev-") }
65
+ branch_or_string_reqs = or_string_reqs.
66
+ select { |r| r.start_with?("dev-") }
67
+
68
+ return req unless req_string.match?(/\d/)
69
+ return req if numeric_or_string_reqs.none?
70
+ return updated_alias(req) if req_string.match?(ALIAS_REGEX)
71
+ return req if req_satisfied_by_latest_resolvable?(req_string) &&
72
+ update_strategy != :bump_versions
73
+
74
+ new_req =
75
+ case update_strategy
76
+ when :widen_ranges
77
+ widen_requirement(req, or_separator)
78
+ when :bump_versions, :bump_versions_if_necessary
79
+ update_requirement_version(req, or_separator)
80
+ end
81
+
82
+ new_req_string =
83
+ [new_req[:requirement], *branch_or_string_reqs].join(or_separator)
84
+ new_req.merge(requirement: new_req_string)
85
+ end
86
+ # rubocop:enable Metrics/PerceivedComplexity
87
+ # rubocop:enable Metrics/CyclomaticComplexity
88
+
89
+ def updated_alias(req)
90
+ req_string = req[:requirement]
91
+ real_version = req_string.split(/\sas\s/).first.strip
92
+
93
+ # If the version we're aliasing isn't a version then we don't know
94
+ # how to update it, so we just return the existing requirement.
95
+ return req unless version_class.correct?(real_version)
96
+
97
+ new_version_string = latest_resolvable_version.to_s
98
+ new_req = req_string.sub(real_version, new_version_string)
99
+ req.merge(requirement: new_req)
100
+ end
101
+
102
+ def widen_requirement(req, or_separator)
103
+ current_requirement = req[:requirement]
104
+ reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
105
+
106
+ updated_requirement =
107
+ if reqs.any? { |r| r.start_with?("^") }
108
+ update_caret_requirement(current_requirement, or_separator)
109
+ elsif reqs.any? { |r| r.start_with?("~") }
110
+ update_tilda_requirement(current_requirement, or_separator)
111
+ elsif reqs.any? { |r| r.include?("*") }
112
+ update_wildcard_requirement(current_requirement, or_separator)
113
+ elsif reqs.any? { |r| r.match?(/<|(\s+-\s+)/) }
114
+ update_range_requirement(current_requirement, or_separator)
115
+ else
116
+ update_version_string(current_requirement)
117
+ end
118
+
119
+ req.merge(requirement: updated_requirement)
120
+ end
121
+
122
+ def update_requirement_version(req, or_separator)
123
+ current_requirement = req[:requirement]
124
+ reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
125
+
126
+ updated_requirement =
127
+ if reqs.count > 1
128
+ "^#{latest_resolvable_version}"
129
+ elsif reqs.any? { |r| r.match?(/<|(\s+-\s+)/) }
130
+ update_range_requirement(current_requirement, or_separator)
131
+ elsif reqs.any? { |r| r.match?(/>[^=]/) }
132
+ current_requirement
133
+ else
134
+ update_version_string(current_requirement)
135
+ end
136
+
137
+ req.merge(requirement: updated_requirement)
138
+ end
139
+
140
+ def req_satisfied_by_latest_resolvable?(requirement_string)
141
+ ruby_requirements(requirement_string).
142
+ any? { |r| r.satisfied_by?(latest_resolvable_version) }
143
+ end
144
+
145
+ def update_version_string(req_string)
146
+ req_string.
147
+ sub(VERSION_REGEX) do |old_version|
148
+ unless req_string.match?(/[~*\^]/)
149
+ next latest_resolvable_version.to_s
150
+ end
151
+
152
+ old_parts = old_version.split(".")
153
+ new_parts = latest_resolvable_version.to_s.split(".").
154
+ first(old_parts.count)
155
+ new_parts.map.with_index do |part, i|
156
+ old_parts[i] == "*" ? "*" : part
157
+ end.join(".")
158
+ end
159
+ end
160
+
161
+ def ruby_requirements(requirement_string)
162
+ Composer::Requirement.requirements_array(requirement_string)
163
+ end
164
+
165
+ def update_caret_requirement(req_string, or_separator)
166
+ caret_requirements =
167
+ req_string.split(SEPARATOR).select { |r| r.start_with?("^") }
168
+ version_parts = latest_resolvable_version.segments
169
+
170
+ min_existing_precision =
171
+ caret_requirements.map { |r| r.split(".").count }.min
172
+ first_non_zero_index =
173
+ version_parts.count.times.find { |i| version_parts[i] != 0 }
174
+
175
+ precision = [min_existing_precision, first_non_zero_index + 1].max
176
+ version = version_parts.first(precision).map.with_index do |part, i|
177
+ i <= first_non_zero_index ? part : 0
178
+ end.join(".")
179
+
180
+ req_string + "#{or_separator}^#{version}"
181
+ end
182
+
183
+ def update_tilda_requirement(req_string, or_separator)
184
+ tilda_requirements =
185
+ req_string.split(SEPARATOR).select { |r| r.start_with?("~") }
186
+ precision = tilda_requirements.map { |r| r.split(".").count }.min
187
+
188
+ version_parts = latest_resolvable_version.segments.first(precision)
189
+ version_parts[-1] = 0
190
+ version = version_parts.join(".")
191
+
192
+ req_string + "#{or_separator}~#{version}"
193
+ end
194
+
195
+ def update_wildcard_requirement(req_string, or_separator)
196
+ wildcard_requirements =
197
+ req_string.split(SEPARATOR).select { |r| r.include?("*") }
198
+ precision = wildcard_requirements.map do |r|
199
+ r.split(".").reject { |s| s == "*" }.count
200
+ end.min
201
+ wildcard_count = wildcard_requirements.map do |r|
202
+ r.split(".").select { |s| s == "*" }.count
203
+ end.min
204
+
205
+ version_parts = latest_resolvable_version.segments.first(precision)
206
+ version = version_parts.join(".")
207
+
208
+ req_string + "#{or_separator}#{version}#{'.*' * wildcard_count}"
209
+ end
210
+
211
+ def update_range_requirement(req_string, or_separator)
212
+ range_requirements =
213
+ req_string.split(SEPARATOR).select { |r| r.match?(/<|(\s+-\s+)/) }
214
+
215
+ if range_requirements.count == 1
216
+ range_requirement = range_requirements.first
217
+ versions = range_requirement.scan(VERSION_REGEX)
218
+ upper_bound = versions.map { |v| version_class.new(v) }.max
219
+ new_upper_bound = update_greatest_version(
220
+ upper_bound,
221
+ latest_resolvable_version
222
+ )
223
+
224
+ req_string.sub(upper_bound.to_s, new_upper_bound.to_s)
225
+ else
226
+ req_string + "#{or_separator}^#{latest_resolvable_version}"
227
+ end
228
+ end
229
+
230
+ def update_greatest_version(old_version, version_to_be_permitted)
231
+ version = version_class.new(old_version)
232
+ version = version.release if version.prerelease?
233
+
234
+ index_to_update =
235
+ version.segments.map.with_index { |seg, i| seg.zero? ? 0 : i }.max
236
+
237
+ version.segments.map.with_index do |_, index|
238
+ if index < index_to_update
239
+ version_to_be_permitted.segments[index]
240
+ elsif index == index_to_update
241
+ version_to_be_permitted.segments[index] + 1
242
+ else 0
243
+ end
244
+ end.join(".")
245
+ end
246
+
247
+ def version_class
248
+ Composer::Version
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end