dependabot-bundler 0.94.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/helpers/Makefile +9 -0
- data/helpers/build +26 -0
- data/lib/dependabot/bundler.rb +27 -0
- data/lib/dependabot/bundler/file_fetcher.rb +216 -0
- data/lib/dependabot/bundler/file_fetcher/child_gemfile_finder.rb +68 -0
- data/lib/dependabot/bundler/file_fetcher/gemspec_finder.rb +96 -0
- data/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb +112 -0
- data/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb +65 -0
- data/lib/dependabot/bundler/file_parser.rb +297 -0
- data/lib/dependabot/bundler/file_parser/file_preparer.rb +84 -0
- data/lib/dependabot/bundler/file_parser/gemfile_checker.rb +46 -0
- data/lib/dependabot/bundler/file_updater.rb +125 -0
- data/lib/dependabot/bundler/file_updater/gemfile_updater.rb +114 -0
- data/lib/dependabot/bundler/file_updater/gemspec_dependency_name_finder.rb +50 -0
- data/lib/dependabot/bundler/file_updater/gemspec_sanitizer.rb +296 -0
- data/lib/dependabot/bundler/file_updater/gemspec_updater.rb +62 -0
- data/lib/dependabot/bundler/file_updater/git_pin_replacer.rb +78 -0
- data/lib/dependabot/bundler/file_updater/git_source_remover.rb +100 -0
- data/lib/dependabot/bundler/file_updater/lockfile_updater.rb +387 -0
- data/lib/dependabot/bundler/file_updater/requirement_replacer.rb +221 -0
- data/lib/dependabot/bundler/metadata_finder.rb +204 -0
- data/lib/dependabot/bundler/requirement.rb +29 -0
- data/lib/dependabot/bundler/update_checker.rb +334 -0
- data/lib/dependabot/bundler/update_checker/file_preparer.rb +279 -0
- data/lib/dependabot/bundler/update_checker/force_updater.rb +259 -0
- data/lib/dependabot/bundler/update_checker/latest_version_finder.rb +165 -0
- data/lib/dependabot/bundler/update_checker/requirements_updater.rb +281 -0
- data/lib/dependabot/bundler/update_checker/ruby_requirement_setter.rb +113 -0
- data/lib/dependabot/bundler/update_checker/shared_bundler_helpers.rb +244 -0
- data/lib/dependabot/bundler/update_checker/version_resolver.rb +272 -0
- data/lib/dependabot/bundler/version.rb +13 -0
- metadata +200 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "parser/current"
|
5
|
+
require "dependabot/bundler/file_fetcher"
|
6
|
+
require "dependabot/errors"
|
7
|
+
|
8
|
+
module Dependabot
|
9
|
+
module Bundler
|
10
|
+
class FileFetcher
|
11
|
+
# Finds the paths of any gemspecs declared using `path: ` in the
|
12
|
+
# passed Gemfile.
|
13
|
+
class PathGemspecFinder
|
14
|
+
def initialize(gemfile:)
|
15
|
+
@gemfile = gemfile
|
16
|
+
end
|
17
|
+
|
18
|
+
def path_gemspec_paths
|
19
|
+
ast = Parser::CurrentRuby.parse(gemfile.content)
|
20
|
+
find_path_gemspec_paths(ast)
|
21
|
+
rescue Parser::SyntaxError
|
22
|
+
raise Dependabot::DependencyFileNotParseable, gemfile.path
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :gemfile
|
28
|
+
|
29
|
+
# rubocop:disable Security/Eval
|
30
|
+
def find_path_gemspec_paths(node)
|
31
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
32
|
+
|
33
|
+
if declares_path_dependency?(node)
|
34
|
+
path_node = path_node_for_gem_declaration(node)
|
35
|
+
|
36
|
+
begin
|
37
|
+
# We use eval here, but we know what we're doing. The
|
38
|
+
# FileFetchers helper method should only ever be run in an
|
39
|
+
# isolated environment
|
40
|
+
path = eval(path_node.loc.expression.source)
|
41
|
+
rescue StandardError
|
42
|
+
return []
|
43
|
+
end
|
44
|
+
return [clean_path(path)]
|
45
|
+
end
|
46
|
+
|
47
|
+
relevant_child_nodes(node).flat_map do |child_node|
|
48
|
+
find_path_gemspec_paths(child_node)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
# rubocop:enable Security/Eval
|
52
|
+
|
53
|
+
def current_dir
|
54
|
+
@current_dir ||= gemfile.name.rpartition("/").first
|
55
|
+
@current_dir = nil if @current_dir == ""
|
56
|
+
@current_dir
|
57
|
+
end
|
58
|
+
|
59
|
+
def declares_path_dependency?(node)
|
60
|
+
return false unless node.is_a?(Parser::AST::Node)
|
61
|
+
return false unless node.children[1] == :gem
|
62
|
+
|
63
|
+
!path_node_for_gem_declaration(node).nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
def clean_path(path)
|
67
|
+
if Pathname.new(path).absolute?
|
68
|
+
base_path = Pathname.new(File.expand_path(Dir.pwd))
|
69
|
+
path = Pathname.new(path).relative_path_from(base_path).to_s
|
70
|
+
end
|
71
|
+
path = File.join(current_dir, path) unless current_dir.nil?
|
72
|
+
Pathname.new(path).cleanpath
|
73
|
+
end
|
74
|
+
|
75
|
+
# rubocop:disable Security/Eval
|
76
|
+
def relevant_child_nodes(node)
|
77
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
78
|
+
return node.children unless node.type == :if
|
79
|
+
|
80
|
+
begin
|
81
|
+
if eval(node.children.first.loc.expression.source)
|
82
|
+
[node.children[1]]
|
83
|
+
else
|
84
|
+
[node.children[2]]
|
85
|
+
end
|
86
|
+
rescue StandardError
|
87
|
+
return node.children
|
88
|
+
end
|
89
|
+
end
|
90
|
+
# rubocop:enable Security/Eval
|
91
|
+
|
92
|
+
def path_node_for_gem_declaration(node)
|
93
|
+
return unless node.children.last.type == :hash
|
94
|
+
|
95
|
+
kwargs_node = node.children.last
|
96
|
+
|
97
|
+
path_hash_pair =
|
98
|
+
kwargs_node.children.
|
99
|
+
find { |hash_pair| key_from_hash_pair(hash_pair) == :path }
|
100
|
+
|
101
|
+
return unless path_hash_pair
|
102
|
+
|
103
|
+
path_hash_pair.children.last
|
104
|
+
end
|
105
|
+
|
106
|
+
def key_from_hash_pair(node)
|
107
|
+
node.children.first.children.first.to_sym
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "parser/current"
|
5
|
+
require "dependabot/bundler/file_fetcher"
|
6
|
+
require "dependabot/errors"
|
7
|
+
|
8
|
+
module Dependabot
|
9
|
+
module Bundler
|
10
|
+
class FileFetcher
|
11
|
+
# Finds the paths of any files included using `require_relative` in the
|
12
|
+
# passed file.
|
13
|
+
class RequireRelativeFinder
|
14
|
+
def initialize(file:)
|
15
|
+
@file = file
|
16
|
+
end
|
17
|
+
|
18
|
+
def require_relative_paths
|
19
|
+
ast = Parser::CurrentRuby.parse(file.content)
|
20
|
+
find_require_relative_paths(ast)
|
21
|
+
rescue Parser::SyntaxError
|
22
|
+
raise Dependabot::DependencyFileNotParseable, file.path
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :file
|
28
|
+
|
29
|
+
# rubocop:disable Security/Eval
|
30
|
+
def find_require_relative_paths(node)
|
31
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
32
|
+
|
33
|
+
if declares_require_relative?(node)
|
34
|
+
# We use eval here, but we know what we're doing. The FileFetchers
|
35
|
+
# helper method should only ever be run in an isolated environment
|
36
|
+
source = node.children[2].loc.expression.source
|
37
|
+
begin
|
38
|
+
path = eval(source)
|
39
|
+
rescue StandardError
|
40
|
+
return []
|
41
|
+
end
|
42
|
+
|
43
|
+
path = File.join(current_dir, path) unless current_dir.nil?
|
44
|
+
return [Pathname.new(path + ".rb").cleanpath.to_path]
|
45
|
+
end
|
46
|
+
|
47
|
+
node.children.flat_map do |child_node|
|
48
|
+
find_require_relative_paths(child_node)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
# rubocop:enable Security/Eval
|
52
|
+
|
53
|
+
def current_dir
|
54
|
+
@current_dir ||= file.name.split("/")[0..-2].last
|
55
|
+
end
|
56
|
+
|
57
|
+
def declares_require_relative?(node)
|
58
|
+
return false unless node.is_a?(Parser::AST::Node)
|
59
|
+
|
60
|
+
node.children[1] == :require_relative
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/dependency"
|
4
|
+
require "dependabot/file_parsers"
|
5
|
+
require "dependabot/file_parsers/base"
|
6
|
+
require "dependabot/bundler/file_updater/lockfile_updater"
|
7
|
+
require "dependabot/bundler/version"
|
8
|
+
require "dependabot/shared_helpers"
|
9
|
+
require "dependabot/errors"
|
10
|
+
|
11
|
+
module Dependabot
|
12
|
+
module Bundler
|
13
|
+
class FileParser < Dependabot::FileParsers::Base
|
14
|
+
require "dependabot/file_parsers/base/dependency_set"
|
15
|
+
require "dependabot/bundler/file_parser/file_preparer"
|
16
|
+
require "dependabot/bundler/file_parser/gemfile_checker"
|
17
|
+
|
18
|
+
def parse
|
19
|
+
dependency_set = DependencySet.new
|
20
|
+
dependency_set += gemfile_dependencies
|
21
|
+
dependency_set += gemspec_dependencies
|
22
|
+
dependency_set += lockfile_dependencies
|
23
|
+
dependency_set.dependencies
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Can't be a constant because some of these don't exist in bundler
|
29
|
+
# 1.15, which Heroku uses, which causes an exception on boot.
|
30
|
+
def sources
|
31
|
+
[
|
32
|
+
NilClass,
|
33
|
+
::Bundler::Source::Rubygems,
|
34
|
+
::Bundler::Source::Git,
|
35
|
+
::Bundler::Source::Path,
|
36
|
+
::Bundler::Source::Gemspec,
|
37
|
+
::Bundler::Source::Metadata
|
38
|
+
]
|
39
|
+
end
|
40
|
+
|
41
|
+
def gemfile_dependencies
|
42
|
+
dependencies = DependencySet.new
|
43
|
+
|
44
|
+
return dependencies unless gemfile
|
45
|
+
|
46
|
+
[gemfile, *evaled_gemfiles].each do |file|
|
47
|
+
parsed_gemfile.each do |dep|
|
48
|
+
next unless dependency_in_gemfile?(gemfile: file, dependency: dep)
|
49
|
+
|
50
|
+
dependencies <<
|
51
|
+
Dependency.new(
|
52
|
+
name: dep.name,
|
53
|
+
version: dependency_version(dep.name)&.to_s,
|
54
|
+
requirements: [{
|
55
|
+
requirement: dep.requirement.to_s,
|
56
|
+
groups: dep.groups,
|
57
|
+
source: source_for(dep),
|
58
|
+
file: file.name
|
59
|
+
}],
|
60
|
+
package_manager: "bundler"
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
dependencies
|
66
|
+
end
|
67
|
+
|
68
|
+
def gemspec_dependencies
|
69
|
+
dependencies = DependencySet.new
|
70
|
+
|
71
|
+
gemspecs.each do |gemspec|
|
72
|
+
parsed_gemspec(gemspec).dependencies.each do |dependency|
|
73
|
+
dependencies <<
|
74
|
+
Dependency.new(
|
75
|
+
name: dependency.name,
|
76
|
+
version: dependency_version(dependency.name)&.to_s,
|
77
|
+
requirements: [{
|
78
|
+
requirement: dependency.requirement.to_s,
|
79
|
+
groups: dependency.runtime? ? ["runtime"] : ["development"],
|
80
|
+
source: nil,
|
81
|
+
file: gemspec.name
|
82
|
+
}],
|
83
|
+
package_manager: "bundler"
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
dependencies
|
89
|
+
end
|
90
|
+
|
91
|
+
def lockfile_dependencies
|
92
|
+
dependencies = DependencySet.new
|
93
|
+
|
94
|
+
return dependencies unless lockfile
|
95
|
+
|
96
|
+
# Create a DependencySet where each element has no requirement. Any
|
97
|
+
# requirements will be added when combining the DependencySet with
|
98
|
+
# other DependencySets.
|
99
|
+
parsed_lockfile.specs.each do |dependency|
|
100
|
+
next if dependency.source.is_a?(::Bundler::Source::Path)
|
101
|
+
|
102
|
+
dependencies <<
|
103
|
+
Dependency.new(
|
104
|
+
name: dependency.name,
|
105
|
+
version: dependency_version(dependency.name)&.to_s,
|
106
|
+
requirements: [],
|
107
|
+
package_manager: "bundler"
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
dependencies
|
112
|
+
end
|
113
|
+
|
114
|
+
def parsed_gemfile
|
115
|
+
base_directory = dependency_files.first.directory
|
116
|
+
@parsed_gemfile ||=
|
117
|
+
SharedHelpers.in_a_temporary_directory(base_directory) do
|
118
|
+
write_temporary_dependency_files
|
119
|
+
|
120
|
+
SharedHelpers.in_a_forked_process do
|
121
|
+
::Bundler.instance_variable_set(:@root, Pathname.new(Dir.pwd))
|
122
|
+
|
123
|
+
::Bundler::Definition.build(gemfile.name, nil, {}).
|
124
|
+
dependencies.
|
125
|
+
select(&:current_platform?).
|
126
|
+
# We can't dump gemspec sources, and we wouldn't bump them
|
127
|
+
# anyway, so we filter them out.
|
128
|
+
reject { |dep| dep.source.is_a?(::Bundler::Source::Gemspec) }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
rescue SharedHelpers::ChildProcessFailed => error
|
132
|
+
msg = error.error_class + " with message: " +
|
133
|
+
error.error_message.force_encoding("UTF-8").encode
|
134
|
+
raise Dependabot::DependencyFileNotEvaluatable, msg
|
135
|
+
end
|
136
|
+
|
137
|
+
def parsed_gemspec(file)
|
138
|
+
@parsed_gemspecs ||= {}
|
139
|
+
@parsed_gemspecs[file.name] ||=
|
140
|
+
SharedHelpers.in_a_temporary_directory do
|
141
|
+
[file, *imported_ruby_files].each do |f|
|
142
|
+
path = f.name
|
143
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
144
|
+
File.write(path, f.content)
|
145
|
+
end
|
146
|
+
|
147
|
+
SharedHelpers.in_a_forked_process do
|
148
|
+
::Bundler.instance_variable_set(:@root, Pathname.new(Dir.pwd))
|
149
|
+
::Bundler.load_gemspec_uncached(file.name)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
rescue SharedHelpers::ChildProcessFailed => error
|
153
|
+
msg = error.error_class + " with message: " + error.error_message
|
154
|
+
raise Dependabot::DependencyFileNotEvaluatable, msg
|
155
|
+
end
|
156
|
+
|
157
|
+
def prepared_dependency_files
|
158
|
+
@prepared_dependency_files ||=
|
159
|
+
FilePreparer.new(dependency_files: dependency_files).
|
160
|
+
prepared_dependency_files
|
161
|
+
end
|
162
|
+
|
163
|
+
def write_temporary_dependency_files
|
164
|
+
prepared_dependency_files.each do |file|
|
165
|
+
path = file.name
|
166
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
167
|
+
File.write(path, file.content)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def check_required_files
|
172
|
+
file_names = dependency_files.map(&:name)
|
173
|
+
|
174
|
+
return if file_names.any? do |name|
|
175
|
+
name.end_with?(".gemspec") && !name.include?("/")
|
176
|
+
end
|
177
|
+
|
178
|
+
return if gemfile
|
179
|
+
|
180
|
+
raise "A gemspec or Gemfile must be provided!"
|
181
|
+
end
|
182
|
+
|
183
|
+
def source_for(dependency)
|
184
|
+
source = dependency.source
|
185
|
+
if lockfile && default_rubygems?(source)
|
186
|
+
# If there's a lockfile and the Gemfile doesn't have anything
|
187
|
+
# interesting to say about the source, check that.
|
188
|
+
source = source_from_lockfile(dependency.name)
|
189
|
+
end
|
190
|
+
raise "Bad source: #{source}" unless sources.include?(source.class)
|
191
|
+
|
192
|
+
return nil if default_rubygems?(source)
|
193
|
+
|
194
|
+
details = { type: source.class.name.split("::").last.downcase }
|
195
|
+
if source.is_a?(::Bundler::Source::Git)
|
196
|
+
details.merge!(git_source_details(source))
|
197
|
+
end
|
198
|
+
if source.is_a?(::Bundler::Source::Rubygems)
|
199
|
+
details[:url] = source.remotes.first.to_s
|
200
|
+
end
|
201
|
+
details
|
202
|
+
end
|
203
|
+
|
204
|
+
def git_source_details(source)
|
205
|
+
{
|
206
|
+
url: source.uri,
|
207
|
+
branch: source.branch || "master",
|
208
|
+
ref: source.ref
|
209
|
+
}
|
210
|
+
end
|
211
|
+
|
212
|
+
def default_rubygems?(source)
|
213
|
+
return true if source.nil?
|
214
|
+
return false unless source.is_a?(::Bundler::Source::Rubygems)
|
215
|
+
|
216
|
+
source.remotes.any? { |r| r.to_s.include?("rubygems.org") }
|
217
|
+
end
|
218
|
+
|
219
|
+
def dependency_version(dependency_name)
|
220
|
+
return unless lockfile
|
221
|
+
|
222
|
+
spec = parsed_lockfile.specs.find { |s| s.name == dependency_name }
|
223
|
+
|
224
|
+
# Not all files in the Gemfile will appear in the Gemfile.lock. For
|
225
|
+
# instance, if a gem specifies `platform: [:windows]`, and the
|
226
|
+
# Gemfile.lock is generated on a Linux machine, the gem will be not
|
227
|
+
# appear in the lockfile.
|
228
|
+
return unless spec
|
229
|
+
|
230
|
+
# If the source is Git we're better off knowing the SHA-1 than the
|
231
|
+
# version.
|
232
|
+
if spec.source.instance_of?(::Bundler::Source::Git)
|
233
|
+
return spec.source.revision
|
234
|
+
end
|
235
|
+
|
236
|
+
spec.version
|
237
|
+
end
|
238
|
+
|
239
|
+
def source_from_lockfile(dependency_name)
|
240
|
+
parsed_lockfile.specs.find { |s| s.name == dependency_name }&.source
|
241
|
+
end
|
242
|
+
|
243
|
+
def dependency_in_gemfile?(gemfile:, dependency:)
|
244
|
+
GemfileChecker.new(
|
245
|
+
dependency: dependency,
|
246
|
+
gemfile: gemfile
|
247
|
+
).includes_dependency?
|
248
|
+
end
|
249
|
+
|
250
|
+
def gemfile
|
251
|
+
@gemfile ||= get_original_file("Gemfile") ||
|
252
|
+
get_original_file("gems.rb")
|
253
|
+
end
|
254
|
+
|
255
|
+
def evaled_gemfiles
|
256
|
+
dependency_files.
|
257
|
+
reject { |f| f.name.end_with?(".gemspec") }.
|
258
|
+
reject { |f| f.name.end_with?(".lock") }.
|
259
|
+
reject { |f| f.name.end_with?(".ruby-version") }.
|
260
|
+
reject { |f| f.name == "Gemfile" }.
|
261
|
+
reject { |f| f.name == "gems.rb" }.
|
262
|
+
reject { |f| f.name == "gems.locked" }
|
263
|
+
end
|
264
|
+
|
265
|
+
def lockfile
|
266
|
+
@lockfile ||= get_original_file("Gemfile.lock") ||
|
267
|
+
get_original_file("gems.locked")
|
268
|
+
end
|
269
|
+
|
270
|
+
def parsed_lockfile
|
271
|
+
@parsed_lockfile ||=
|
272
|
+
::Bundler::LockfileParser.new(sanitized_lockfile_content)
|
273
|
+
end
|
274
|
+
|
275
|
+
def sanitized_lockfile_content
|
276
|
+
regex = FileUpdater::LockfileUpdater::LOCKFILE_ENDING
|
277
|
+
lockfile.content.gsub(regex, "")
|
278
|
+
end
|
279
|
+
|
280
|
+
def gemspecs
|
281
|
+
# Path gemspecs are excluded (they're supporting files)
|
282
|
+
@gemspecs ||= prepared_dependency_files.
|
283
|
+
select { |file| file.name.end_with?(".gemspec") }.
|
284
|
+
reject(&:support_file?)
|
285
|
+
end
|
286
|
+
|
287
|
+
def imported_ruby_files
|
288
|
+
dependency_files.
|
289
|
+
select { |f| f.name.end_with?(".rb") }.
|
290
|
+
reject { |f| f.name == "gems.rb" }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
Dependabot::FileParsers.
|
297
|
+
register("bundler", Dependabot::Bundler::FileParser)
|