dependabot-bundler 0.94.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/Makefile +9 -0
  3. data/helpers/build +26 -0
  4. data/lib/dependabot/bundler.rb +27 -0
  5. data/lib/dependabot/bundler/file_fetcher.rb +216 -0
  6. data/lib/dependabot/bundler/file_fetcher/child_gemfile_finder.rb +68 -0
  7. data/lib/dependabot/bundler/file_fetcher/gemspec_finder.rb +96 -0
  8. data/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb +112 -0
  9. data/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb +65 -0
  10. data/lib/dependabot/bundler/file_parser.rb +297 -0
  11. data/lib/dependabot/bundler/file_parser/file_preparer.rb +84 -0
  12. data/lib/dependabot/bundler/file_parser/gemfile_checker.rb +46 -0
  13. data/lib/dependabot/bundler/file_updater.rb +125 -0
  14. data/lib/dependabot/bundler/file_updater/gemfile_updater.rb +114 -0
  15. data/lib/dependabot/bundler/file_updater/gemspec_dependency_name_finder.rb +50 -0
  16. data/lib/dependabot/bundler/file_updater/gemspec_sanitizer.rb +296 -0
  17. data/lib/dependabot/bundler/file_updater/gemspec_updater.rb +62 -0
  18. data/lib/dependabot/bundler/file_updater/git_pin_replacer.rb +78 -0
  19. data/lib/dependabot/bundler/file_updater/git_source_remover.rb +100 -0
  20. data/lib/dependabot/bundler/file_updater/lockfile_updater.rb +387 -0
  21. data/lib/dependabot/bundler/file_updater/requirement_replacer.rb +221 -0
  22. data/lib/dependabot/bundler/metadata_finder.rb +204 -0
  23. data/lib/dependabot/bundler/requirement.rb +29 -0
  24. data/lib/dependabot/bundler/update_checker.rb +334 -0
  25. data/lib/dependabot/bundler/update_checker/file_preparer.rb +279 -0
  26. data/lib/dependabot/bundler/update_checker/force_updater.rb +259 -0
  27. data/lib/dependabot/bundler/update_checker/latest_version_finder.rb +165 -0
  28. data/lib/dependabot/bundler/update_checker/requirements_updater.rb +281 -0
  29. data/lib/dependabot/bundler/update_checker/ruby_requirement_setter.rb +113 -0
  30. data/lib/dependabot/bundler/update_checker/shared_bundler_helpers.rb +244 -0
  31. data/lib/dependabot/bundler/update_checker/version_resolver.rb +272 -0
  32. data/lib/dependabot/bundler/version.rb +13 -0
  33. 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)