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,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
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/metadata_finders"
5
+ require "dependabot/metadata_finders/base"
6
+
7
+ module Dependabot
8
+ module Bundler
9
+ class MetadataFinder < Dependabot::MetadataFinders::Base
10
+ SOURCE_KEYS = %w(
11
+ source_code_uri
12
+ homepage_uri
13
+ wiki_uri
14
+ bug_tracker_uri
15
+ documentation_uri
16
+ changelog_uri
17
+ mailing_list_uri
18
+ download_uri
19
+ ).freeze
20
+
21
+ def homepage_url
22
+ return super unless %w(default rubygems).include?(new_source_type)
23
+ return super unless rubygems_api_response["homepage_uri"]
24
+
25
+ rubygems_api_response["homepage_uri"]
26
+ end
27
+
28
+ private
29
+
30
+ def look_up_source
31
+ case new_source_type
32
+ when "git" then find_source_from_git_url
33
+ when "default", "rubygems" then find_source_from_rubygems
34
+ else raise "Unexpected source type: #{new_source_type}"
35
+ end
36
+ end
37
+
38
+ def new_source_type
39
+ sources =
40
+ dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
41
+
42
+ return "default" if sources.empty?
43
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
44
+
45
+ sources.first[:type] || sources.first.fetch("type")
46
+ end
47
+
48
+ def find_source_from_rubygems
49
+ api_source = find_source_from_rubygems_api_response
50
+ return api_source if api_source || new_source_type == "default"
51
+
52
+ find_source_from_gemspec_download
53
+ end
54
+
55
+ def find_source_from_rubygems_api_response
56
+ source_url = rubygems_api_response.
57
+ values_at(*SOURCE_KEYS).
58
+ compact.
59
+ find { |url| Source.from_url(url) }
60
+
61
+ Source.from_url(source_url)
62
+ end
63
+
64
+ def find_source_from_git_url
65
+ info = dependency.requirements.map { |r| r[:source] }.compact.first
66
+
67
+ url = info[:url] || info.fetch("url")
68
+ Source.from_url(url)
69
+ end
70
+
71
+ def find_source_from_gemspec_download
72
+ github_urls = []
73
+ return unless rubygems_marshalled_gemspec_response
74
+
75
+ rubygems_marshalled_gemspec_response.scan(Source::SOURCE_REGEX) do
76
+ github_urls << Regexp.last_match.to_s
77
+ end
78
+
79
+ source_url = github_urls.find do |url|
80
+ repo = Source.from_url(url).repo
81
+ repo.downcase.end_with?(dependency.name)
82
+ end
83
+ return unless source_url
84
+
85
+ Source.from_url(source_url)
86
+ end
87
+
88
+ # Note: This response MUST NOT be unmarshalled
89
+ # (as calling Marshal.load is unsafe)
90
+ def rubygems_marshalled_gemspec_response
91
+ if defined?(@rubygems_marshalled_gemspec_response)
92
+ return @rubygems_marshalled_gemspec_response
93
+ end
94
+
95
+ gemspec_uri =
96
+ "#{registry_url}quick/Marshal.4.8/"\
97
+ "#{dependency.name}-#{dependency.version}.gemspec.rz"
98
+
99
+ response =
100
+ Excon.get(
101
+ gemspec_uri,
102
+ headers: registry_auth_headers,
103
+ idempotent: true,
104
+ **SharedHelpers.excon_defaults
105
+ )
106
+
107
+ if response.status >= 400
108
+ return @rubygems_marshalled_gemspec_response = nil
109
+ end
110
+
111
+ @rubygems_marshalled_gemspec_response =
112
+ Zlib::Inflate.inflate(response.body)
113
+ rescue Zlib::DataError
114
+ @rubygems_marshalled_gemspec_response = nil
115
+ end
116
+
117
+ def rubygems_api_response
118
+ return @rubygems_api_response if defined?(@rubygems_api_response)
119
+
120
+ response =
121
+ Excon.get(
122
+ "#{registry_url}api/v1/gems/#{dependency.name}.json",
123
+ headers: registry_auth_headers,
124
+ idempotent: true,
125
+ **SharedHelpers.excon_defaults
126
+ )
127
+ return @rubygems_api_response = {} if response.status >= 400
128
+
129
+ response_body = response.body
130
+ response_body = augment_private_response_if_appropriate(response_body)
131
+
132
+ @rubygems_api_response = JSON.parse(response_body)
133
+ append_slash_to_source_code_uri(@rubygems_api_response)
134
+ rescue JSON::ParserError, Excon::Error::Timeout
135
+ @rubygems_api_response = {}
136
+ end
137
+
138
+ def append_slash_to_source_code_uri(listing)
139
+ # We have to do this so that `Source.from_url(...)` doesn't prune the
140
+ # last line off of the directory.
141
+ return listing unless listing&.fetch("source_code_uri", nil)
142
+ return listing if listing.fetch("source_code_uri").end_with?("/")
143
+
144
+ listing["source_code_uri"] = listing["source_code_uri"] + "/"
145
+ listing
146
+ end
147
+
148
+ def augment_private_response_if_appropriate(response_body)
149
+ return response_body if new_source_type == "default"
150
+
151
+ parsed_body = JSON.parse(response_body)
152
+ return response_body if (SOURCE_KEYS - parsed_body.keys).none?
153
+
154
+ digest = parsed_body.values_at("version", "authors", "info").hash
155
+
156
+ source_url = parsed_body.
157
+ values_at(*SOURCE_KEYS).
158
+ compact.
159
+ find { |url| Source.from_url(url) }
160
+ return response_body if source_url
161
+
162
+ rubygems_response =
163
+ Excon.get(
164
+ "https://rubygems.org/api/v1/gems/#{dependency.name}.json",
165
+ idempotent: true,
166
+ **SharedHelpers.excon_defaults
167
+ )
168
+ parsed_rubygems_body = JSON.parse(rubygems_response.body)
169
+ rubygems_digest =
170
+ parsed_rubygems_body.values_at("version", "authors", "info").hash
171
+
172
+ digest == rubygems_digest ? rubygems_response.body : response_body
173
+ rescue JSON::ParserError, Excon::Error::Socket, Excon::Error::Timeout
174
+ response_body
175
+ end
176
+
177
+ def registry_url
178
+ return "https://rubygems.org/" if new_source_type == "default"
179
+
180
+ info = dependency.requirements.map { |r| r[:source] }.compact.first
181
+ info[:url] || info.fetch("url")
182
+ end
183
+
184
+ def registry_auth_headers
185
+ return {} unless new_source_type == "rubygems"
186
+
187
+ token =
188
+ credentials.
189
+ select { |cred| cred["type"] == "rubygems_server" }.
190
+ find { |cred| registry_url.include?(cred["host"]) }&.
191
+ fetch("token")
192
+
193
+ return {} unless token
194
+
195
+ token += ":" unless token.include?(":")
196
+ encoded_token = Base64.encode64(token).delete("\n")
197
+ { "Authorization" => "Basic #{encoded_token}" }
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ Dependabot::MetadataFinders.
204
+ register("bundler", Dependabot::Bundler::MetadataFinder)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+
5
+ module Dependabot
6
+ module Bundler
7
+ class Requirement < Gem::Requirement
8
+ # For consistency with other langauges, we define a requirements array.
9
+ # Ruby doesn't have an `OR` separator for requirements, so it always
10
+ # contains a single element.
11
+ def self.requirements_array(requirement_string)
12
+ [new(requirement_string)]
13
+ end
14
+
15
+ # Patches Gem::Requirement to make it accept requirement strings like
16
+ # "~> 4.2.5, >= 4.2.5.1" without first needing to split them.
17
+ def initialize(*requirements)
18
+ requirements = requirements.flatten.flat_map do |req_string|
19
+ req_string.split(",")
20
+ end
21
+
22
+ super(requirements)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ Dependabot::Utils.
29
+ register_requirement_class("bundler", Dependabot::Bundler::Requirement)