-
Notifications
You must be signed in to change notification settings - Fork 919
/
file_preparer.rb
293 lines (247 loc) 路 10.1 KB
/
file_preparer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# frozen_string_literal: true
require "dependabot/dependency_file"
require "dependabot/bundler/update_checker"
require "dependabot/bundler/file_updater/gemspec_sanitizer"
require "dependabot/bundler/file_updater/git_pin_replacer"
require "dependabot/bundler/file_updater/git_source_remover"
require "dependabot/bundler/file_updater/requirement_replacer"
require "dependabot/bundler/file_updater/gemspec_dependency_name_finder"
require "dependabot/bundler/file_updater/lockfile_updater"
require "dependabot/bundler/file_updater/ruby_requirement_setter"
module Dependabot
module Bundler
class UpdateChecker
# This class takes a set of dependency files and sanitizes them for use
# in UpdateCheckers::Ruby::Bundler. In particular, it:
# - Removes any version requirement on the dependency being updated
# (in the Gemfile)
# - Sanitizes any provided gemspecs to remove file imports etc. (since
# Dependabot doesn't pull down the entire repo). This process is
# imperfect - an alternative would be to clone the repo
# - Sets the ruby version in the Gemfile to be the lowest possible
# version allowed by the gemspec, if the gemspec has a required ruby
# version range
class FilePreparer
VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/.freeze
# Can't be a constant because some of these don't exist in bundler
# 1.15, which Heroku uses, which causes an exception on boot.
def gemspec_sources
[
::Bundler::Source::Path,
::Bundler::Source::Gemspec
]
end
def initialize(dependency_files:, dependency:,
remove_git_source: false,
unlock_requirement: true,
replacement_git_pin: nil,
latest_allowable_version: nil,
lock_ruby_version: true)
@dependency_files = dependency_files
@dependency = dependency
@remove_git_source = remove_git_source
@unlock_requirement = unlock_requirement
@replacement_git_pin = replacement_git_pin
@latest_allowable_version = latest_allowable_version
@lock_ruby_version = lock_ruby_version
end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def prepared_dependency_files
files = []
if gemfile
files << DependencyFile.new(
name: gemfile.name,
content: gemfile_content_for_update_check(gemfile),
directory: gemfile.directory
)
end
top_level_gemspecs.each do |gemspec|
files << DependencyFile.new(
name: gemspec.name,
content: gemspec_content_for_update_check(gemspec),
directory: gemspec.directory
)
end
path_gemspecs.each do |file|
files << DependencyFile.new(
name: file.name,
content: sanitize_gemspec_content(file.content),
directory: file.directory,
support_file: file.support_file?
)
end
evaled_gemfiles.each do |file|
files << DependencyFile.new(
name: file.name,
content: gemfile_content_for_update_check(file),
directory: file.directory
)
end
# No editing required for lockfile or Ruby version file
files += [
lockfile,
ruby_version_file,
*imported_ruby_files,
*specification_files
].compact
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
private
attr_reader :dependency_files, :dependency, :replacement_git_pin,
:latest_allowable_version
def remove_git_source?
@remove_git_source
end
def unlock_requirement?
@unlock_requirement
end
def replace_git_pin?
!replacement_git_pin.nil?
end
def gemfile
dependency_files.find { |f| f.name == "Gemfile" } ||
dependency_files.find { |f| f.name == "gems.rb" }
end
def evaled_gemfiles
dependency_files.
reject { |f| f.name.end_with?(".gemspec") }.
reject { |f| f.name.end_with?(".specification") }.
reject { |f| f.name.end_with?(".lock") }.
reject { |f| f.name.end_with?(".ruby-version") }.
reject { |f| f.name == "Gemfile" }.
reject { |f| f.name == "gems.rb" }.
reject { |f| f.name == "gems.locked" }
end
def lockfile
dependency_files.find { |f| f.name == "Gemfile.lock" } ||
dependency_files.find { |f| f.name == "gems.locked" }
end
def specification_files
dependency_files.select { |f| f.name.end_with?(".specification") }
end
def top_level_gemspecs
dependency_files.
select { |f| f.name.end_with?(".gemspec") }.
reject(&:support_file?)
end
def ruby_version_file
dependency_files.find { |f| f.name == ".ruby-version" }
end
def path_gemspecs
all = dependency_files.select { |f| f.name.end_with?(".gemspec") }
all - top_level_gemspecs
end
def imported_ruby_files
dependency_files.
select { |f| f.name.end_with?(".rb") }.
reject { |f| f.name == "gems.rb" }
end
def gemfile_content_for_update_check(file)
content = file.content
content = replace_gemfile_constraint(content, file.name)
content = remove_git_source(content) if remove_git_source?
content = replace_git_pin(content) if replace_git_pin?
content = lock_ruby_version(content) if lock_ruby_version?(file)
content
end
def gemspec_content_for_update_check(gemspec)
content = gemspec.content
content = replace_gemspec_constraint(content, gemspec.name)
sanitize_gemspec_content(content)
end
def replace_gemfile_constraint(content, filename)
FileUpdater::RequirementReplacer.new(
dependency: dependency,
file_type: :gemfile,
updated_requirement: updated_version_requirement_string(filename),
insert_if_bare: true
).rewrite(content)
end
def replace_gemspec_constraint(content, filename)
FileUpdater::RequirementReplacer.new(
dependency: dependency,
file_type: :gemspec,
updated_requirement: updated_version_requirement_string(filename),
insert_if_bare: true
).rewrite(content)
end
def sanitize_gemspec_content(gemspec_content)
new_version = replacement_version_for_gemspec(gemspec_content)
FileUpdater::GemspecSanitizer.
new(replacement_version: new_version).
rewrite(gemspec_content)
end
def updated_version_requirement_string(filename)
lower_bound_req = updated_version_req_lower_bound(filename)
return lower_bound_req if latest_allowable_version.nil?
unless Gem::Version.correct?(latest_allowable_version)
return lower_bound_req
end
lower_bound_req + ", <= #{latest_allowable_version}"
end
# rubocop:disable Metrics/PerceivedComplexity
def updated_version_req_lower_bound(filename)
original_req = dependency.requirements.
find { |r| r.fetch(:file) == filename }&.
fetch(:requirement)
if original_req && !unlock_requirement? then original_req
elsif dependency.version&.match?(/^[0-9a-f]{40}$/) then ">= 0"
elsif dependency.version then ">= #{dependency.version}"
else
version_for_requirement =
dependency.requirements.map { |r| r[:requirement] }.
reject { |req_string| req_string.start_with?("<") }.
select { |req_string| req_string.match?(VERSION_REGEX) }.
map { |req_string| req_string.match(VERSION_REGEX) }.
select { |version| Gem::Version.correct?(version) }.
max_by { |version| Gem::Version.new(version) }
">= #{version_for_requirement || 0}"
end
end
# rubocop:enable Metrics/PerceivedComplexity
def remove_git_source(content)
FileUpdater::GitSourceRemover.new(
dependency: dependency
).rewrite(content)
end
def replace_git_pin(content)
FileUpdater::GitPinReplacer.new(
dependency: dependency,
new_pin: replacement_git_pin
).rewrite(content)
end
def lock_ruby_version(gemfile_content)
top_level_gemspecs.each do |gs|
gemfile_content = FileUpdater::RubyRequirementSetter.
new(gemspec: gs).rewrite(gemfile_content)
end
gemfile_content
end
def lock_ruby_version?(file)
@lock_ruby_version && file == gemfile
end
# rubocop:disable Metrics/PerceivedComplexity
def replacement_version_for_gemspec(gemspec_content)
return "0.0.1" unless lockfile
gemspec_specs =
::Bundler::LockfileParser.new(sanitized_lockfile_content).specs.
select { |s| gemspec_sources.include?(s.source.class) }
gem_name =
FileUpdater::GemspecDependencyNameFinder.
new(gemspec_content: gemspec_content).
dependency_name
return gemspec_specs.first&.version || "0.0.1" unless gem_name
spec = gemspec_specs.find { |s| s.name == gem_name }
spec&.version || gemspec_specs.first&.version || "0.0.1"
end
# rubocop:enable Metrics/PerceivedComplexity
def sanitized_lockfile_content
re = FileUpdater::LockfileUpdater::LOCKFILE_ENDING
lockfile.content.gsub(re, "")
end
end
end
end
end