dependabot-hex 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +19 -0
  3. data/helpers/deps/jason/.fetch +0 -0
  4. data/helpers/deps/jason/.hex +2 -0
  5. data/helpers/deps/jason/CHANGELOG.md +60 -0
  6. data/helpers/deps/jason/LICENSE +13 -0
  7. data/helpers/deps/jason/README.md +179 -0
  8. data/helpers/deps/jason/hex_metadata.config +20 -0
  9. data/helpers/deps/jason/lib/codegen.ex +158 -0
  10. data/helpers/deps/jason/lib/decoder.ex +657 -0
  11. data/helpers/deps/jason/lib/encode.ex +630 -0
  12. data/helpers/deps/jason/lib/encoder.ex +216 -0
  13. data/helpers/deps/jason/lib/formatter.ex +253 -0
  14. data/helpers/deps/jason/lib/fragment.ex +11 -0
  15. data/helpers/deps/jason/lib/helpers.ex +90 -0
  16. data/helpers/deps/jason/lib/jason.ex +228 -0
  17. data/helpers/deps/jason/mix.exs +92 -0
  18. data/helpers/lib/check_update.exs +92 -0
  19. data/helpers/lib/do_update.exs +39 -0
  20. data/helpers/lib/parse_deps.exs +103 -0
  21. data/helpers/lib/run.exs +76 -0
  22. data/helpers/mix.exs +21 -0
  23. data/helpers/mix.lock +3 -0
  24. data/lib/dependabot/hex.rb +11 -0
  25. data/lib/dependabot/hex/file_fetcher.rb +79 -0
  26. data/lib/dependabot/hex/file_parser.rb +125 -0
  27. data/lib/dependabot/hex/file_updater.rb +71 -0
  28. data/lib/dependabot/hex/file_updater/lockfile_updater.rb +142 -0
  29. data/lib/dependabot/hex/file_updater/mixfile_git_pin_updater.rb +51 -0
  30. data/lib/dependabot/hex/file_updater/mixfile_requirement_updater.rb +72 -0
  31. data/lib/dependabot/hex/file_updater/mixfile_sanitizer.rb +26 -0
  32. data/lib/dependabot/hex/file_updater/mixfile_updater.rb +94 -0
  33. data/lib/dependabot/hex/metadata_finder.rb +70 -0
  34. data/lib/dependabot/hex/native_helpers.rb +20 -0
  35. data/lib/dependabot/hex/requirement.rb +53 -0
  36. data/lib/dependabot/hex/update_checker.rb +275 -0
  37. data/lib/dependabot/hex/update_checker/file_preparer.rb +191 -0
  38. data/lib/dependabot/hex/update_checker/requirements_updater.rb +173 -0
  39. data/lib/dependabot/hex/update_checker/version_resolver.rb +170 -0
  40. data/lib/dependabot/hex/version.rb +67 -0
  41. metadata +208 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/metadata_finders"
5
+ require "dependabot/metadata_finders/base"
6
+ require "dependabot/shared_helpers"
7
+
8
+ module Dependabot
9
+ module Hex
10
+ class MetadataFinder < Dependabot::MetadataFinders::Base
11
+ SOURCE_KEYS = %w(
12
+ GitHub Github github
13
+ GitLab Gitlab gitlab
14
+ BitBucket Bitbucket bitbucket
15
+ Source source
16
+ ).freeze
17
+
18
+ private
19
+
20
+ def look_up_source
21
+ case new_source_type
22
+ when "default" then find_source_from_hex_listing
23
+ when "git" then find_source_from_git_url
24
+ else raise "Unexpected source type: #{new_source_type}"
25
+ end
26
+ end
27
+
28
+ def new_source_type
29
+ sources =
30
+ dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
31
+
32
+ return "default" if sources.empty?
33
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
34
+
35
+ sources.first[:type] || sources.first.fetch("type")
36
+ end
37
+
38
+ def find_source_from_hex_listing
39
+ potential_source_urls =
40
+ SOURCE_KEYS.
41
+ map { |key| hex_listing.dig("meta", "links", key) }.
42
+ compact
43
+
44
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
45
+ Source.from_url(source_url)
46
+ end
47
+
48
+ def find_source_from_git_url
49
+ info = dependency.requirements.map { |r| r[:source] }.compact.first
50
+
51
+ url = info[:url] || info.fetch("url")
52
+ Source.from_url(url)
53
+ end
54
+
55
+ def hex_listing
56
+ return @hex_listing unless @hex_listing.nil?
57
+
58
+ response = Excon.get(
59
+ "https://hex.pm/api/packages/#{dependency.name}",
60
+ idempotent: true,
61
+ **SharedHelpers.excon_defaults
62
+ )
63
+
64
+ @hex_listing = JSON.parse(response.body)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ Dependabot::MetadataFinders.register("hex", Dependabot::Hex::MetadataFinder)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dependabot
4
+ module Hex
5
+ module NativeHelpers
6
+ def self.hex_helpers_dir
7
+ File.join(native_helpers_root, "hex/helpers")
8
+ end
9
+
10
+ def self.native_helpers_root
11
+ default_path = File.join(__dir__, "../../../..")
12
+ ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
13
+ end
14
+
15
+ def self.clean_path(path)
16
+ Pathname.new(path).cleanpath.to_path
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+ require "dependabot/hex/version"
5
+
6
+ module Dependabot
7
+ module Hex
8
+ class Requirement < Gem::Requirement
9
+ AND_SEPARATOR = /\s+and\s+/.freeze
10
+ OR_SEPARATOR = /\s+or\s+/.freeze
11
+
12
+ # Add the double-equality matcher to the list of allowed operations
13
+ OPS = OPS.merge("==" => ->(v, r) { v == r })
14
+
15
+ # Override the version pattern to allow local versions
16
+ quoted = OPS.keys.map { |k| Regexp.quote k }.join "|"
17
+ PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{Hex::Version::VERSION_PATTERN})\\s*"
18
+ PATTERN = /\A#{PATTERN_RAW}\z/.freeze
19
+
20
+ # Returns an array of requirements. At least one requirement from the
21
+ # returned array must be satisfied for a version to be valid.
22
+ def self.requirements_array(requirement_string)
23
+ requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
24
+ requirements = req_string.strip.split(AND_SEPARATOR)
25
+ new(requirements)
26
+ end
27
+ end
28
+
29
+ # Override the parser to create Hex::Versions
30
+ def self.parse(obj)
31
+ return ["=", Hex::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
32
+
33
+ unless (matches = PATTERN.match(obj.to_s))
34
+ msg = "Illformed requirement [#{obj.inspect}]"
35
+ raise BadRequirementError, msg
36
+ end
37
+
38
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
39
+
40
+ [matches[1] || "=", Hex::Version.new(matches[2])]
41
+ end
42
+
43
+ def satisfied_by?(version)
44
+ version = Hex::Version.new(version.to_s)
45
+
46
+ requirements.all? { |op, rv| (OPS[op] || OPS["="]).call(version, rv) }
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ Dependabot::Utils.
53
+ register_requirement_class("hex", Dependabot::Hex::Requirement)
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/git_commit_checker"
5
+ require "dependabot/update_checkers"
6
+ require "dependabot/update_checkers/base"
7
+ require "dependabot/shared_helpers"
8
+
9
+ require "json"
10
+
11
+ module Dependabot
12
+ module Hex
13
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
14
+ require_relative "update_checker/file_preparer"
15
+ require_relative "update_checker/requirements_updater"
16
+ require_relative "update_checker/version_resolver"
17
+
18
+ def latest_version
19
+ @latest_version ||=
20
+ if git_dependency?
21
+ latest_version_for_git_dependency
22
+ else
23
+ latest_release_from_hex_registry || latest_resolvable_version
24
+ end
25
+ end
26
+
27
+ def latest_resolvable_version
28
+ @latest_resolvable_version ||=
29
+ if git_dependency?
30
+ latest_resolvable_version_for_git_dependency
31
+ else
32
+ fetch_latest_resolvable_version(unlock_requirement: true)
33
+ end
34
+ end
35
+
36
+ def latest_resolvable_version_with_no_unlock
37
+ @latest_resolvable_version_with_no_unlock ||=
38
+ if git_dependency?
39
+ latest_resolvable_commit_with_unchanged_git_source
40
+ else
41
+ fetch_latest_resolvable_version(unlock_requirement: false)
42
+ end
43
+ end
44
+
45
+ def updated_requirements
46
+ RequirementsUpdater.new(
47
+ requirements: dependency.requirements,
48
+ updated_source: updated_source,
49
+ latest_resolvable_version: latest_resolvable_version&.to_s
50
+ ).updated_requirements
51
+ end
52
+
53
+ private
54
+
55
+ def latest_version_resolvable_with_full_unlock?
56
+ # Full unlock checks aren't implemented for Elixir (yet)
57
+ false
58
+ end
59
+
60
+ def updated_dependencies_after_full_unlock
61
+ raise NotImplementedError
62
+ end
63
+
64
+ def latest_version_for_git_dependency
65
+ latest_git_version_sha
66
+ end
67
+
68
+ def latest_resolvable_version_for_git_dependency
69
+ # If the gem isn't pinned, the latest version is just the latest
70
+ # commit for the specified branch.
71
+ unless git_commit_checker.pinned?
72
+ return latest_resolvable_commit_with_unchanged_git_source
73
+ end
74
+
75
+ # If the dependency is pinned to a tag that looks like a version then
76
+ # we want to update that tag. The latest version will then be the SHA
77
+ # of the latest tag that looks like a version.
78
+ if git_commit_checker.pinned_ref_looks_like_version? &&
79
+ latest_git_tag_is_resolvable?
80
+ new_tag = git_commit_checker.local_tag_for_latest_version
81
+ return new_tag.fetch(:commit_sha)
82
+ end
83
+
84
+ # If the dependency is pinned then there's nothing we can do.
85
+ dependency.version
86
+ end
87
+
88
+ def latest_resolvable_commit_with_unchanged_git_source
89
+ fetch_latest_resolvable_version(unlock_requirement: false)
90
+ rescue SharedHelpers::HelperSubprocessFailed,
91
+ Dependabot::DependencyFileNotResolvable => error
92
+ # Resolution may fail, as Elixir updates straight to the tip of the
93
+ # branch. Just return `nil` if it does (so no update).
94
+ return if error.message.include?("resolution failed")
95
+
96
+ raise error
97
+ end
98
+
99
+ def git_dependency?
100
+ git_commit_checker.git_dependency?
101
+ end
102
+
103
+ def latest_git_version_sha
104
+ # If the gem isn't pinned, the latest version is just the latest
105
+ # commit for the specified branch.
106
+ unless git_commit_checker.pinned?
107
+ return git_commit_checker.head_commit_for_current_branch
108
+ end
109
+
110
+ # If the dependency is pinned to a tag that looks like a version then
111
+ # we want to update that tag. The latest version will then be the SHA
112
+ # of the latest tag that looks like a version.
113
+ if git_commit_checker.pinned_ref_looks_like_version?
114
+ latest_tag = git_commit_checker.local_tag_for_latest_version
115
+ return latest_tag&.fetch(:commit_sha) || dependency.version
116
+ end
117
+
118
+ # If the dependency is pinned to a tag that doesn't look like a
119
+ # version then there's nothing we can do.
120
+ dependency.version
121
+ end
122
+
123
+ def latest_git_tag_is_resolvable?
124
+ return @git_tag_resolvable if @latest_git_tag_is_resolvable_checked
125
+
126
+ @latest_git_tag_is_resolvable_checked = true
127
+
128
+ return false if git_commit_checker.local_tag_for_latest_version.nil?
129
+
130
+ replacement_tag = git_commit_checker.local_tag_for_latest_version
131
+
132
+ prepared_files = FilePreparer.new(
133
+ dependency: dependency,
134
+ dependency_files: dependency_files,
135
+ replacement_git_pin: replacement_tag.fetch(:tag)
136
+ ).prepared_dependency_files
137
+
138
+ resolver_result = VersionResolver.new(
139
+ dependency: dependency,
140
+ prepared_dependency_files: prepared_files,
141
+ original_dependency_files: dependency_files,
142
+ credentials: credentials
143
+ ).latest_resolvable_version
144
+
145
+ @git_tag_resolvable = !resolver_result.nil?
146
+ rescue SharedHelpers::HelperSubprocessFailed,
147
+ Dependabot::DependencyFileNotResolvable => error
148
+ raise error unless error.message.include?("resolution failed")
149
+
150
+ @git_tag_resolvable = false
151
+ end
152
+
153
+ def updated_source
154
+ # Never need to update source, unless a git_dependency
155
+ return dependency_source_details unless git_dependency?
156
+
157
+ # Update the git tag if updating a pinned version
158
+ if git_commit_checker.pinned_ref_looks_like_version? &&
159
+ latest_git_tag_is_resolvable?
160
+ new_tag = git_commit_checker.local_tag_for_latest_version
161
+ return dependency_source_details.merge(ref: new_tag.fetch(:tag))
162
+ end
163
+
164
+ # Otherwise return the original source
165
+ dependency_source_details
166
+ end
167
+
168
+ def dependency_source_details
169
+ sources =
170
+ dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
171
+
172
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
173
+
174
+ sources.first
175
+ end
176
+
177
+ def fetch_latest_resolvable_version(unlock_requirement:)
178
+ @latest_resolvable_version_hash ||= {}
179
+ @latest_resolvable_version_hash[unlock_requirement] ||=
180
+ version_resolver(unlock_requirement: unlock_requirement).
181
+ latest_resolvable_version
182
+ end
183
+
184
+ def version_resolver(unlock_requirement:)
185
+ @version_resolver ||= {}
186
+ @version_resolver[unlock_requirement] ||=
187
+ begin
188
+ prepared_dependency_files = prepared_dependency_files(
189
+ unlock_requirement: unlock_requirement,
190
+ latest_allowable_version: latest_release_from_hex_registry
191
+ )
192
+
193
+ VersionResolver.new(
194
+ dependency: dependency,
195
+ prepared_dependency_files: prepared_dependency_files,
196
+ original_dependency_files: dependency_files,
197
+ credentials: credentials
198
+ )
199
+ end
200
+ end
201
+
202
+ def prepared_dependency_files(unlock_requirement:,
203
+ latest_allowable_version: nil)
204
+ FilePreparer.new(
205
+ dependency: dependency,
206
+ dependency_files: dependency_files,
207
+ unlock_requirement: unlock_requirement,
208
+ latest_allowable_version: latest_allowable_version
209
+ ).prepared_dependency_files
210
+ end
211
+
212
+ def latest_release_from_hex_registry
213
+ @latest_release_from_hex_registry ||=
214
+ begin
215
+ versions = hex_registry_response&.fetch("releases", []) || []
216
+ versions =
217
+ versions.
218
+ select { |release| version_class.correct?(release["version"]) }.
219
+ map { |release| version_class.new(release["version"]) }
220
+
221
+ versions.reject!(&:prerelease?) unless wants_prerelease?
222
+ versions.reject! do |v|
223
+ ignore_reqs.any? { |r| r.satisfied_by?(v) }
224
+ end
225
+ versions.max
226
+ end
227
+ end
228
+
229
+ def hex_registry_response
230
+ return @hex_registry_response if @hex_registry_requested
231
+
232
+ @hex_registry_requested = true
233
+
234
+ response = Excon.get(
235
+ dependency_url,
236
+ idempotent: true,
237
+ **SharedHelpers.excon_defaults
238
+ )
239
+
240
+ return unless response.status == 200
241
+
242
+ @hex_registry_response = JSON.parse(response.body)
243
+ rescue Excon::Error::Socket, Excon::Error::Timeout
244
+ nil
245
+ end
246
+
247
+ def wants_prerelease?
248
+ current_version = dependency.version
249
+ if current_version &&
250
+ version_class.correct?(current_version) &&
251
+ version_class.new(current_version).prerelease?
252
+ return true
253
+ end
254
+
255
+ dependency.requirements.any? do |req|
256
+ req[:requirement]&.match?(/\d-[A-Za-z0-9]/)
257
+ end
258
+ end
259
+
260
+ def dependency_url
261
+ "https://hex.pm/api/packages/#{dependency.name}"
262
+ end
263
+
264
+ def git_commit_checker
265
+ @git_commit_checker ||=
266
+ GitCommitChecker.new(
267
+ dependency: dependency,
268
+ credentials: credentials
269
+ )
270
+ end
271
+ end
272
+ end
273
+ end
274
+
275
+ Dependabot::UpdateCheckers.register("hex", Dependabot::Hex::UpdateChecker)
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/dependency_file"
4
+ require "dependabot/hex/update_checker"
5
+ require "dependabot/hex/file_updater/mixfile_requirement_updater"
6
+ require "dependabot/hex/file_updater/mixfile_git_pin_updater"
7
+ require "dependabot/hex/file_updater/mixfile_sanitizer"
8
+ require "dependabot/hex/version"
9
+
10
+ module Dependabot
11
+ module Hex
12
+ class UpdateChecker
13
+ # This class takes a set of dependency files and sanitizes them for use
14
+ # in UpdateCheckers::Elixir::Hex.
15
+ class FilePreparer
16
+ def initialize(dependency_files:, dependency:,
17
+ unlock_requirement: true,
18
+ replacement_git_pin: nil,
19
+ latest_allowable_version: nil)
20
+ @dependency_files = dependency_files
21
+ @dependency = dependency
22
+ @unlock_requirement = unlock_requirement
23
+ @replacement_git_pin = replacement_git_pin
24
+ @latest_allowable_version = latest_allowable_version
25
+ end
26
+
27
+ def prepared_dependency_files
28
+ files = []
29
+ files += mixfiles.map do |file|
30
+ DependencyFile.new(
31
+ name: file.name,
32
+ content: mixfile_content_for_update_check(file),
33
+ directory: file.directory
34
+ )
35
+ end
36
+ files << lockfile if lockfile
37
+ files += support_files
38
+ files
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :dependency_files, :dependency, :replacement_git_pin,
44
+ :latest_allowable_version
45
+
46
+ def unlock_requirement?
47
+ @unlock_requirement
48
+ end
49
+
50
+ def replace_git_pin?
51
+ !replacement_git_pin.nil?
52
+ end
53
+
54
+ def mixfile_content_for_update_check(file)
55
+ content = file.content
56
+
57
+ unless dependency_appears_in_file?(file.name)
58
+ return sanitize_mixfile(content)
59
+ end
60
+
61
+ content = relax_version(content, filename: file.name)
62
+ if replace_git_pin?
63
+ content = replace_git_pin(content, filename: file.name)
64
+ end
65
+
66
+ sanitize_mixfile(content)
67
+ end
68
+
69
+ def relax_version(content, filename:)
70
+ old_requirement =
71
+ dependency.requirements.find { |r| r.fetch(:file) == filename }.
72
+ fetch(:requirement)
73
+
74
+ Hex::FileUpdater::MixfileRequirementUpdater.new(
75
+ dependency_name: dependency.name,
76
+ mixfile_content: content,
77
+ previous_requirement: old_requirement,
78
+ updated_requirement: updated_version_requirement_string(filename),
79
+ insert_if_bare: true
80
+ ).updated_content
81
+ end
82
+
83
+ def updated_version_requirement_string(filename)
84
+ lower_bound_req = updated_version_req_lower_bound(filename)
85
+
86
+ return lower_bound_req if latest_allowable_version.nil?
87
+ unless version_class.correct?(latest_allowable_version)
88
+ return lower_bound_req
89
+ end
90
+
91
+ lower_bound_req + " and <= #{latest_allowable_version}"
92
+ end
93
+
94
+ # rubocop:disable Metrics/AbcSize
95
+ # rubocop:disable Metrics/PerceivedComplexity
96
+ def updated_version_req_lower_bound(filename)
97
+ original_req = dependency.requirements.
98
+ find { |r| r.fetch(:file) == filename }&.
99
+ fetch(:requirement)
100
+
101
+ if original_req && !unlock_requirement? then original_req
102
+ elsif dependency.version&.match?(/^[0-9a-f]{40}$/) then ">= 0"
103
+ elsif dependency.version then ">= #{dependency.version}"
104
+ else
105
+ version_for_requirement =
106
+ dependency.requirements.map { |r| r[:requirement] }.compact.
107
+ reject { |req_string| req_string.start_with?("<") }.
108
+ select { |req_string| req_string.match?(version_regex) }.
109
+ map { |req_string| req_string.match(version_regex) }.
110
+ select { |version| version_class.correct?(version.to_s) }.
111
+ max_by { |version| version_class.new(version.to_s) }
112
+
113
+ return ">= 0" unless version_for_requirement
114
+
115
+ # Elixir requires that versions are specified to three places
116
+ # when used with a >= specifier
117
+ parts = version_for_requirement.to_s.split(".")
118
+ parts << "0" while parts.count < 3
119
+ ">= #{parts.join('.')}"
120
+ end
121
+ end
122
+ # rubocop:enable Metrics/AbcSize
123
+ # rubocop:enable Metrics/PerceivedComplexity
124
+
125
+ def replace_git_pin(content, filename:)
126
+ old_pin =
127
+ dependency.requirements.find { |r| r.fetch(:file) == filename }&.
128
+ dig(:source, :ref)
129
+
130
+ return content unless old_pin
131
+ return content if old_pin == replacement_git_pin
132
+
133
+ Hex::FileUpdater::MixfileGitPinUpdater.new(
134
+ dependency_name: dependency.name,
135
+ mixfile_content: content,
136
+ previous_pin: old_pin,
137
+ updated_pin: replacement_git_pin
138
+ ).updated_content
139
+ end
140
+
141
+ def sanitize_mixfile(content)
142
+ Hex::FileUpdater::MixfileSanitizer.new(
143
+ mixfile_content: content
144
+ ).sanitized_content
145
+ end
146
+
147
+ def mixfiles
148
+ mixfiles =
149
+ dependency_files.
150
+ select { |f| f.name.end_with?("mix.exs") }
151
+ raise "No mix.exs!" if mixfiles.none?
152
+
153
+ mixfiles
154
+ end
155
+
156
+ def lockfile
157
+ @lockfile ||= dependency_files.find { |f| f.name == "mix.lock" }
158
+ end
159
+
160
+ def support_files
161
+ @support_files ||= dependency_files.select(&:support_file)
162
+ end
163
+
164
+ def wants_prerelease?
165
+ current_version = dependency.version
166
+ if current_version &&
167
+ version_class.correct?(current_version) &&
168
+ version_class.new(current_version).prerelease?
169
+ return true
170
+ end
171
+
172
+ dependency.requirements.any? do |req|
173
+ req[:requirement].match?(/\d-[A-Za-z0-9]/)
174
+ end
175
+ end
176
+
177
+ def version_class
178
+ Hex::Version
179
+ end
180
+
181
+ def version_regex
182
+ version_class::VERSION_PATTERN
183
+ end
184
+
185
+ def dependency_appears_in_file?(file_name)
186
+ dependency.requirements.any? { |r| r[:file] == file_name }
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end