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,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)
|