dependabot-composer 0.89.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.
@@ -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