dependabot-bundler 0.95.6 → 0.95.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/bundler/file_fetcher/child_gemfile_finder.rb +68 -0
  3. data/lib/dependabot/bundler/file_fetcher/gemspec_finder.rb +96 -0
  4. data/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb +112 -0
  5. data/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb +65 -0
  6. data/lib/dependabot/bundler/file_fetcher.rb +216 -0
  7. data/lib/dependabot/bundler/file_parser/file_preparer.rb +84 -0
  8. data/lib/dependabot/bundler/file_parser/gemfile_checker.rb +46 -0
  9. data/lib/dependabot/bundler/file_parser.rb +297 -0
  10. data/lib/dependabot/bundler/file_updater/gemfile_updater.rb +114 -0
  11. data/lib/dependabot/bundler/file_updater/gemspec_dependency_name_finder.rb +50 -0
  12. data/lib/dependabot/bundler/file_updater/gemspec_sanitizer.rb +298 -0
  13. data/lib/dependabot/bundler/file_updater/gemspec_updater.rb +62 -0
  14. data/lib/dependabot/bundler/file_updater/git_pin_replacer.rb +78 -0
  15. data/lib/dependabot/bundler/file_updater/git_source_remover.rb +100 -0
  16. data/lib/dependabot/bundler/file_updater/lockfile_updater.rb +387 -0
  17. data/lib/dependabot/bundler/file_updater/requirement_replacer.rb +221 -0
  18. data/lib/dependabot/bundler/file_updater.rb +125 -0
  19. data/lib/dependabot/bundler/metadata_finder.rb +204 -0
  20. data/lib/dependabot/bundler/requirement.rb +29 -0
  21. data/lib/dependabot/bundler/update_checker/file_preparer.rb +279 -0
  22. data/lib/dependabot/bundler/update_checker/force_updater.rb +259 -0
  23. data/lib/dependabot/bundler/update_checker/latest_version_finder.rb +165 -0
  24. data/lib/dependabot/bundler/update_checker/requirements_updater.rb +281 -0
  25. data/lib/dependabot/bundler/update_checker/ruby_requirement_setter.rb +113 -0
  26. data/lib/dependabot/bundler/update_checker/shared_bundler_helpers.rb +244 -0
  27. data/lib/dependabot/bundler/update_checker/version_resolver.rb +272 -0
  28. data/lib/dependabot/bundler/update_checker.rb +334 -0
  29. data/lib/dependabot/bundler/version.rb +13 -0
  30. data/lib/dependabot/bundler.rb +27 -0
  31. data/lib/dependabot/monkey_patches/bundler/definition_bundler_version_patch.rb +15 -0
  32. data/lib/dependabot/monkey_patches/bundler/definition_ruby_version_patch.rb +14 -0
  33. data/lib/dependabot/monkey_patches/bundler/git_source_patch.rb +27 -0
  34. metadata +37 -5
@@ -0,0 +1,387 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+
5
+ require "dependabot/monkey_patches/bundler/definition_ruby_version_patch"
6
+ require "dependabot/monkey_patches/bundler/definition_bundler_version_patch"
7
+ require "dependabot/monkey_patches/bundler/git_source_patch"
8
+
9
+ require "dependabot/shared_helpers"
10
+ require "dependabot/errors"
11
+ require "dependabot/bundler/file_updater"
12
+ require "dependabot/git_commit_checker"
13
+
14
+ # rubocop:disable Metrics/ClassLength
15
+ module Dependabot
16
+ module Bundler
17
+ class FileUpdater
18
+ class LockfileUpdater
19
+ require_relative "gemfile_updater"
20
+ require_relative "gemspec_updater"
21
+ require_relative "gemspec_sanitizer"
22
+ require_relative "gemspec_dependency_name_finder"
23
+
24
+ LOCKFILE_ENDING =
25
+ /(?<ending>\s*(?:RUBY VERSION|BUNDLED WITH).*)/m.freeze
26
+ GIT_DEPENDENCIES_SECTION = /GIT\n.*?\n\n(?!GIT)/m.freeze
27
+ GIT_DEPENDENCY_DETAILS = /GIT\n.*?\n\n/m.freeze
28
+ GEM_NOT_FOUND_ERROR_REGEX =
29
+ /locked to (?<name>[^\s]+) \(|not find (?<name>[^\s]+)-\d/.freeze
30
+ RETRYABLE_ERRORS = [::Bundler::HTTPError].freeze
31
+
32
+ # Can't be a constant because some of these don't exist in bundler
33
+ # 1.15, which Heroku uses, which causes an exception on boot.
34
+ def gemspec_sources
35
+ [
36
+ ::Bundler::Source::Path,
37
+ ::Bundler::Source::Gemspec
38
+ ]
39
+ end
40
+
41
+ def initialize(dependencies:, dependency_files:, credentials:)
42
+ @dependencies = dependencies
43
+ @dependency_files = dependency_files
44
+ @credentials = credentials
45
+ end
46
+
47
+ def updated_lockfile_content
48
+ @updated_lockfile_content ||=
49
+ begin
50
+ updated_content = build_updated_lockfile
51
+
52
+ if lockfile.content == updated_content
53
+ raise "Expected content to change!"
54
+ end
55
+
56
+ updated_content
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :dependencies, :dependency_files, :credentials
63
+
64
+ def build_updated_lockfile
65
+ base_dir = dependency_files.first.directory
66
+ lockfile_body =
67
+ SharedHelpers.in_a_temporary_directory(base_dir) do |tmp_dir|
68
+ write_temporary_dependency_files
69
+
70
+ SharedHelpers.in_a_forked_process do
71
+ # Set the path for path gemspec correctly
72
+ ::Bundler.instance_variable_set(:@root, tmp_dir)
73
+
74
+ # Remove installed gems from the default Rubygems index
75
+ ::Gem::Specification.all = []
76
+
77
+ # Set auth details
78
+ relevant_credentials.each do |cred|
79
+ token = cred["token"] ||
80
+ "#{cred['username']}:#{cred['password']}"
81
+
82
+ ::Bundler.settings.set_command_option(
83
+ cred.fetch("host"),
84
+ token.gsub("@", "%40F").gsub("?", "%3F")
85
+ )
86
+ end
87
+
88
+ generate_lockfile
89
+ end
90
+ end
91
+ post_process_lockfile(lockfile_body)
92
+ end
93
+
94
+ def write_temporary_dependency_files
95
+ File.write(gemfile.name, updated_gemfile_content(gemfile))
96
+ File.write(lockfile.name, sanitized_lockfile_body)
97
+
98
+ top_level_gemspecs.each do |gemspec|
99
+ path = gemspec.name
100
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
101
+ updated_content = updated_gemspec_content(gemspec)
102
+ File.write(path, sanitized_gemspec_content(updated_content))
103
+ end
104
+
105
+ write_ruby_version_file
106
+ write_path_gemspecs
107
+ write_imported_ruby_files
108
+
109
+ evaled_gemfiles.each do |file|
110
+ path = file.name
111
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
112
+ File.write(path, updated_gemfile_content(file))
113
+ end
114
+ end
115
+
116
+ def generate_lockfile
117
+ dependencies_to_unlock = dependencies.map(&:name)
118
+
119
+ begin
120
+ definition = build_definition(dependencies_to_unlock)
121
+
122
+ old_reqs = lock_deps_being_updated_to_exact_versions(definition)
123
+
124
+ definition.resolve_remotely!
125
+
126
+ old_reqs.each do |dep_name, old_req|
127
+ d_dep = definition.dependencies.find { |d| d.name == dep_name }
128
+ if old_req == :none then definition.dependencies.delete(d_dep)
129
+ else d_dep.instance_variable_set(:@requirement, old_req)
130
+ end
131
+ end
132
+
133
+ definition.to_lock
134
+ rescue ::Bundler::GemNotFound => error
135
+ unlock_yanked_gem(dependencies_to_unlock, error) && retry
136
+ rescue ::Bundler::VersionConflict => error
137
+ unlock_blocking_subdeps(dependencies_to_unlock, error) && retry
138
+ rescue *RETRYABLE_ERRORS
139
+ raise if @retrying
140
+
141
+ @retrying = true
142
+ sleep(rand(1.0..5.0))
143
+ retry
144
+ end
145
+ end
146
+
147
+ def unlock_yanked_gem(dependencies_to_unlock, error)
148
+ raise unless error.message.match?(GEM_NOT_FOUND_ERROR_REGEX)
149
+
150
+ gem_name = error.message.match(GEM_NOT_FOUND_ERROR_REGEX).
151
+ named_captures["name"]
152
+ raise if dependencies_to_unlock.include?(gem_name)
153
+
154
+ dependencies_to_unlock << gem_name
155
+ end
156
+
157
+ def unlock_blocking_subdeps(dependencies_to_unlock, error)
158
+ all_deps = ::Bundler::LockfileParser.new(sanitized_lockfile_body).
159
+ specs.map(&:name).map(&:to_s)
160
+ top_level = build_definition([]).dependencies.
161
+ map(&:name).map(&:to_s)
162
+ allowed_new_unlocks = all_deps - top_level - dependencies_to_unlock
163
+
164
+ # Unlock any sub-dependencies that Bundler reports caused the
165
+ # conflict
166
+ potentials_deps =
167
+ error.cause.conflicts.values.
168
+ flat_map(&:requirement_trees).
169
+ map do |tree|
170
+ tree.find { |req| allowed_new_unlocks.include?(req.name) }
171
+ end.compact.map(&:name)
172
+
173
+ # If there's nothing more we can unlock, give up
174
+ raise if potentials_deps.none?
175
+
176
+ dependencies_to_unlock.append(*potentials_deps)
177
+ end
178
+
179
+ def build_definition(dependencies_to_unlock)
180
+ defn = ::Bundler::Definition.build(
181
+ gemfile.name,
182
+ lockfile.name,
183
+ gems: dependencies_to_unlock
184
+ )
185
+
186
+ # Bundler unlocks the sub-dependencies of gems it is passed even
187
+ # if those sub-deps are top-level dependencies. We only want true
188
+ # subdeps unlocked, like they were in the UpdateChecker, so we
189
+ # mutate the unlocked gems array.
190
+ unlocked = defn.instance_variable_get(:@unlock).fetch(:gems)
191
+ must_not_unlock = defn.dependencies.map(&:name).map(&:to_s) -
192
+ dependencies_to_unlock
193
+ unlocked.reject! { |n| must_not_unlock.include?(n) }
194
+
195
+ defn
196
+ end
197
+
198
+ def lock_deps_being_updated_to_exact_versions(definition)
199
+ dependencies.each_with_object({}) do |dep, old_reqs|
200
+ defn_dep = definition.dependencies.find { |d| d.name == dep.name }
201
+
202
+ if defn_dep.nil?
203
+ definition.dependencies <<
204
+ ::Bundler::Dependency.new(dep.name, dep.version)
205
+ old_reqs[dep.name] = :none
206
+ elsif git_dependency?(dep) &&
207
+ defn_dep.source.is_a?(::Bundler::Source::Git)
208
+ defn_dep.source.unlock!
209
+ elsif Gem::Version.correct?(dep.version)
210
+ new_req = Gem::Requirement.create("= #{dep.version}")
211
+ old_reqs[dep.name] = defn_dep.requirement
212
+ defn_dep.instance_variable_set(:@requirement, new_req)
213
+ end
214
+ end
215
+ end
216
+
217
+ def write_ruby_version_file
218
+ return unless ruby_version_file
219
+
220
+ path = ruby_version_file.name
221
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
222
+ File.write(path, ruby_version_file.content)
223
+ end
224
+
225
+ def write_path_gemspecs
226
+ path_gemspecs.each do |file|
227
+ path = file.name
228
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
229
+ File.write(path, sanitized_gemspec_content(file.content))
230
+ end
231
+ end
232
+
233
+ def write_imported_ruby_files
234
+ imported_ruby_files.each do |file|
235
+ path = file.name
236
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
237
+ File.write(path, file.content)
238
+ end
239
+ end
240
+
241
+ def path_gemspecs
242
+ all = dependency_files.select { |f| f.name.end_with?(".gemspec") }
243
+ all - top_level_gemspecs
244
+ end
245
+
246
+ def imported_ruby_files
247
+ dependency_files.
248
+ select { |f| f.name.end_with?(".rb") }.
249
+ reject { |f| f.name == "gems.rb" }
250
+ end
251
+
252
+ def top_level_gemspecs
253
+ dependency_files.
254
+ select { |file| file.name.end_with?(".gemspec") }.
255
+ reject(&:support_file?)
256
+ end
257
+
258
+ def ruby_version_file
259
+ dependency_files.find { |f| f.name == ".ruby-version" }
260
+ end
261
+
262
+ def post_process_lockfile(lockfile_body)
263
+ lockfile_body = reorder_git_dependencies(lockfile_body)
264
+ replace_lockfile_ending(lockfile_body)
265
+ end
266
+
267
+ def reorder_git_dependencies(lockfile_body)
268
+ new_section = lockfile_body.match(GIT_DEPENDENCIES_SECTION)&.to_s
269
+ old_section = lockfile.content.match(GIT_DEPENDENCIES_SECTION)&.to_s
270
+
271
+ return lockfile_body unless new_section && old_section
272
+
273
+ new_deps = new_section.scan(GIT_DEPENDENCY_DETAILS)
274
+ old_deps = old_section.scan(GIT_DEPENDENCY_DETAILS)
275
+
276
+ return lockfile_body unless new_deps.count == old_deps.count
277
+
278
+ reordered_new_section = new_deps.sort_by do |new_dep_details|
279
+ remote = new_dep_details.match(/remote: (?<remote>.*\n)/)[:remote]
280
+ i = old_deps.index { |details| details.include?(remote) }
281
+
282
+ # If this dependency isn't in the old lockfile then we can't rely
283
+ # on that (presumably outdated) lockfile to do reordering.
284
+ # Instead, we just return the default-ordered content just
285
+ # generated.
286
+ return lockfile_body unless i
287
+
288
+ i
289
+ end.join
290
+
291
+ lockfile_body.gsub(new_section, reordered_new_section)
292
+ end
293
+
294
+ def replace_lockfile_ending(lockfile_body)
295
+ # Re-add the old `BUNDLED WITH` version (and remove the RUBY VERSION
296
+ # if it wasn't previously present in the lockfile)
297
+ lockfile_body.gsub(
298
+ LOCKFILE_ENDING,
299
+ lockfile.content.match(LOCKFILE_ENDING)&.[](:ending) || "\n"
300
+ )
301
+ end
302
+
303
+ def sanitized_gemspec_content(gemspec_content)
304
+ new_version = replacement_version_for_gemspec(gemspec_content)
305
+
306
+ GemspecSanitizer.
307
+ new(replacement_version: new_version).
308
+ rewrite(gemspec_content)
309
+ end
310
+
311
+ def replacement_version_for_gemspec(gemspec_content)
312
+ return "0.0.1" unless lockfile
313
+
314
+ gemspec_specs =
315
+ ::Bundler::LockfileParser.new(sanitized_lockfile_body).specs.
316
+ select { |s| gemspec_sources.include?(s.source.class) }
317
+
318
+ gem_name =
319
+ GemspecDependencyNameFinder.new(gemspec_content: gemspec_content).
320
+ dependency_name
321
+
322
+ return gemspec_specs.first&.version || "0.0.1" unless gem_name
323
+
324
+ spec = gemspec_specs.find { |s| s.name == gem_name }
325
+ spec&.version || gemspec_specs.first&.version || "0.0.1"
326
+ end
327
+
328
+ def relevant_credentials
329
+ credentials.select do |cred|
330
+ next true if cred["type"] == "git_source"
331
+ next true if cred["type"] == "rubygems_server"
332
+
333
+ false
334
+ end
335
+ end
336
+
337
+ def updated_gemfile_content(file)
338
+ GemfileUpdater.new(
339
+ dependencies: dependencies,
340
+ gemfile: file
341
+ ).updated_gemfile_content
342
+ end
343
+
344
+ def updated_gemspec_content(gemspec)
345
+ GemspecUpdater.new(
346
+ dependencies: dependencies,
347
+ gemspec: gemspec
348
+ ).updated_gemspec_content
349
+ end
350
+
351
+ def gemfile
352
+ @gemfile ||= dependency_files.find { |f| f.name == "Gemfile" } ||
353
+ dependency_files.find { |f| f.name == "gems.rb" }
354
+ end
355
+
356
+ def lockfile
357
+ @lockfile ||=
358
+ dependency_files.find { |f| f.name == "Gemfile.lock" } ||
359
+ dependency_files.find { |f| f.name == "gems.locked" }
360
+ end
361
+
362
+ def sanitized_lockfile_body
363
+ lockfile.content.gsub(LOCKFILE_ENDING, "")
364
+ end
365
+
366
+ def evaled_gemfiles
367
+ @evaled_gemfiles ||=
368
+ dependency_files.
369
+ reject { |f| f.name.end_with?(".gemspec") }.
370
+ reject { |f| f.name.end_with?(".lock") }.
371
+ reject { |f| f.name.end_with?(".ruby-version") }.
372
+ reject { |f| f.name == "Gemfile" }.
373
+ reject { |f| f.name == "gems.rb" }.
374
+ reject { |f| f.name == "gems.locked" }
375
+ end
376
+
377
+ def git_dependency?(dep)
378
+ GitCommitChecker.new(
379
+ dependency: dep,
380
+ credentials: credentials
381
+ ).git_dependency?
382
+ end
383
+ end
384
+ end
385
+ end
386
+ end
387
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parser/current"
4
+ require "dependabot/bundler/file_updater"
5
+
6
+ module Dependabot
7
+ module Bundler
8
+ class FileUpdater
9
+ class RequirementReplacer
10
+ attr_reader :dependency, :file_type, :updated_requirement,
11
+ :previous_requirement
12
+
13
+ def initialize(dependency:, file_type:, updated_requirement:,
14
+ previous_requirement: nil, insert_if_bare: false)
15
+ @dependency = dependency
16
+ @file_type = file_type
17
+ @updated_requirement = updated_requirement
18
+ @previous_requirement = previous_requirement
19
+ @insert_if_bare = insert_if_bare
20
+ end
21
+
22
+ def rewrite(content)
23
+ buffer = Parser::Source::Buffer.new("(gemfile_content)")
24
+ buffer.source = content
25
+ ast = Parser::CurrentRuby.new.parse(buffer)
26
+
27
+ updated_content = Rewriter.new(
28
+ dependency: dependency,
29
+ file_type: file_type,
30
+ updated_requirement: updated_requirement,
31
+ insert_if_bare: insert_if_bare?
32
+ ).rewrite(buffer, ast)
33
+
34
+ update_comment_spacing_if_required(content, updated_content)
35
+ end
36
+
37
+ private
38
+
39
+ def insert_if_bare?
40
+ @insert_if_bare
41
+ end
42
+
43
+ def update_comment_spacing_if_required(content, updated_content)
44
+ return updated_content unless previous_requirement
45
+
46
+ return updated_content if updated_content == content
47
+ return updated_content if length_change.zero?
48
+
49
+ updated_lines = updated_content.lines
50
+ updated_line_index =
51
+ updated_lines.length.
52
+ times.find { |i| content.lines[i] != updated_content.lines[i] }
53
+ updated_line = updated_lines[updated_line_index]
54
+
55
+ updated_line =
56
+ if length_change.positive?
57
+ updated_line.sub(/(?<=\s)\s{#{length_change}}#/, "#")
58
+ elsif length_change.negative?
59
+ updated_line.sub(/(?<=\s{2})#/, " " * length_change.abs + "#")
60
+ end
61
+
62
+ updated_lines[updated_line_index] = updated_line
63
+ updated_lines.join
64
+ end
65
+
66
+ def length_change
67
+ unless previous_requirement.start_with?("=")
68
+ return updated_requirement.length - previous_requirement.length
69
+ end
70
+
71
+ updated_requirement.length -
72
+ previous_requirement.gsub(/^=/, "").strip.length
73
+ end
74
+
75
+ class Rewriter < Parser::TreeRewriter
76
+ # TODO: Ideally we wouldn't have to ignore all of these, but
77
+ # implementing each one will be tricky.
78
+ SKIPPED_TYPES = %i(send lvar dstr begin if splat const).freeze
79
+
80
+ def initialize(dependency:, file_type:, updated_requirement:,
81
+ insert_if_bare:)
82
+ @dependency = dependency
83
+ @file_type = file_type
84
+ @updated_requirement = updated_requirement
85
+ @insert_if_bare = insert_if_bare
86
+
87
+ return if %i(gemfile gemspec).include?(file_type)
88
+
89
+ raise "File type must be :gemfile or :gemspec. Got #{file_type}."
90
+ end
91
+
92
+ def on_send(node)
93
+ return unless declares_targeted_gem?(node)
94
+
95
+ req_nodes = node.children[3..-1]
96
+ req_nodes = req_nodes.reject { |child| child.type == :hash }
97
+
98
+ return if req_nodes.none? && !insert_if_bare?
99
+ return if req_nodes.any? { |n| SKIPPED_TYPES.include?(n.type) }
100
+
101
+ quote_characters = extract_quote_characters_from(req_nodes)
102
+ space_after_specifier = space_after_specifier?(req_nodes)
103
+ use_equality_operator = use_equality_operator?(req_nodes)
104
+
105
+ new_req = new_requirement_string(
106
+ quote_characters: quote_characters,
107
+ space_after_specifier: space_after_specifier,
108
+ use_equality_operator: use_equality_operator
109
+ )
110
+ if req_nodes.any?
111
+ replace(range_for(req_nodes), new_req)
112
+ else
113
+ insert_after(range_for(node.children[2..2]), ", #{new_req}")
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ attr_reader :dependency, :file_type, :updated_requirement
120
+
121
+ def insert_if_bare?
122
+ @insert_if_bare
123
+ end
124
+
125
+ def declaration_methods
126
+ return %i(gem) if file_type == :gemfile
127
+
128
+ %i(add_dependency add_runtime_dependency
129
+ add_development_dependency)
130
+ end
131
+
132
+ def declares_targeted_gem?(node)
133
+ return false unless declaration_methods.include?(node.children[1])
134
+
135
+ node.children[2].children.first == dependency.name
136
+ end
137
+
138
+ def extract_quote_characters_from(requirement_nodes)
139
+ return ['"', '"'] if requirement_nodes.none?
140
+
141
+ case requirement_nodes.first.type
142
+ when :str, :dstr
143
+ [
144
+ requirement_nodes.first.loc.begin.source,
145
+ requirement_nodes.first.loc.end.source
146
+ ]
147
+ else
148
+ [
149
+ requirement_nodes.first.children.first.loc.begin.source,
150
+ requirement_nodes.first.children.first.loc.end.source
151
+ ]
152
+ end
153
+ end
154
+
155
+ def space_after_specifier?(requirement_nodes)
156
+ return true if requirement_nodes.none?
157
+
158
+ req_string =
159
+ case requirement_nodes.first.type
160
+ when :str, :dstr
161
+ requirement_nodes.first.loc.expression.source
162
+ else
163
+ requirement_nodes.first.children.first.loc.expression.source
164
+ end
165
+
166
+ ops = Gem::Requirement::OPS.keys
167
+ return true if ops.none? { |op| req_string.include?(op) }
168
+
169
+ req_string.include?(" ")
170
+ end
171
+
172
+ def use_equality_operator?(requirement_nodes)
173
+ return true if requirement_nodes.none?
174
+
175
+ req_string =
176
+ case requirement_nodes.first.type
177
+ when :str, :dstr
178
+ requirement_nodes.first.loc.expression.source
179
+ else
180
+ requirement_nodes.first.children.first.loc.expression.source
181
+ end
182
+
183
+ req_string.match?(/(?<![<>])=/)
184
+ end
185
+
186
+ def new_requirement_string(quote_characters:,
187
+ space_after_specifier:,
188
+ use_equality_operator:)
189
+ open_quote, close_quote = quote_characters
190
+ new_requirement_string =
191
+ updated_requirement.split(",").
192
+ map do |r|
193
+ req_string = serialized_req(r, use_equality_operator)
194
+ %(#{open_quote}#{req_string}#{close_quote})
195
+ end.join(", ")
196
+
197
+ new_requirement_string.delete!(" ") unless space_after_specifier
198
+ new_requirement_string
199
+ end
200
+
201
+ def serialized_req(req, use_equality_operator)
202
+ tmp_req = req
203
+
204
+ # Gem::Requirement serializes exact matches as a string starting
205
+ # with `=`. We may need to remove that equality operator if it
206
+ # wasn't used originally.
207
+ unless use_equality_operator
208
+ tmp_req = tmp_req.gsub(/(?<![<>])=/, "")
209
+ end
210
+
211
+ tmp_req.strip
212
+ end
213
+
214
+ def range_for(nodes)
215
+ nodes.first.loc.begin.begin.join(nodes.last.loc.expression)
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end