dependabot-elm 0.82.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.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/update_checkers"
5
+ require "dependabot/update_checkers/base"
6
+ require "dependabot/shared_helpers"
7
+ require "dependabot/errors"
8
+
9
+ module Dependabot
10
+ module Elm
11
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
12
+ require_relative "update_checker/requirements_updater"
13
+ require_relative "update_checker/elm_18_version_resolver"
14
+ require_relative "update_checker/elm_19_version_resolver"
15
+
16
+ def latest_version
17
+ @latest_version ||= candidate_versions.max
18
+ end
19
+
20
+ # Overwrite the base class to allow multi-dependency update PRs for
21
+ # dependencies for which we don't have a version.
22
+ def can_update?(requirements_to_unlock:)
23
+ if dependency.appears_in_lockfile?
24
+ version_can_update?(requirements_to_unlock: requirements_to_unlock)
25
+ elsif requirements_to_unlock == :none
26
+ false
27
+ elsif requirements_to_unlock == :own
28
+ requirements_can_update?
29
+ elsif requirements_to_unlock == :all
30
+ updated_dependencies_after_full_unlock.any?
31
+ end
32
+ end
33
+
34
+ def latest_resolvable_version
35
+ @latest_resolvable_version ||=
36
+ version_resolver.
37
+ latest_resolvable_version(unlock_requirement: :own)
38
+ end
39
+
40
+ def latest_resolvable_version_with_no_unlock
41
+ # Irrelevant, since Elm has a single dependency file (well, there's
42
+ # also `exact-dependencies.json`, but it's not recommended that that
43
+ # is committed).
44
+ nil
45
+ end
46
+
47
+ def updated_requirements
48
+ RequirementsUpdater.new(
49
+ requirements: dependency.requirements,
50
+ latest_resolvable_version: latest_resolvable_version
51
+ ).updated_requirements
52
+ end
53
+
54
+ private
55
+
56
+ def version_resolver
57
+ @version_resolver ||=
58
+ if dependency.requirements.any? { |r| r.fetch(:file) == "elm.json" }
59
+ Elm19VersionResolver.new(
60
+ dependency: dependency,
61
+ dependency_files: dependency_files
62
+ )
63
+ else
64
+ Elm18VersionResolver.new(
65
+ dependency: dependency,
66
+ dependency_files: dependency_files,
67
+ candidate_versions: candidate_versions
68
+ )
69
+ end
70
+ end
71
+
72
+ def updated_dependencies_after_full_unlock
73
+ version_resolver.updated_dependencies_after_full_unlock
74
+ end
75
+
76
+ def latest_version_resolvable_with_full_unlock?
77
+ latest_version == version_resolver.
78
+ latest_resolvable_version(unlock_requirement: :all)
79
+ end
80
+
81
+ def candidate_versions
82
+ all_versions.
83
+ reject { |v| ignore_reqs.any? { |r| r.satisfied_by?(v) } }
84
+ end
85
+
86
+ def all_versions
87
+ return @all_versions if @version_lookup_attempted
88
+
89
+ @version_lookup_attempted = true
90
+
91
+ response = Excon.get(
92
+ "https://package.elm-lang.org/packages/#{dependency.name}/"\
93
+ "releases.json",
94
+ idempotent: true,
95
+ **Dependabot::SharedHelpers.excon_defaults
96
+ )
97
+
98
+ return @all_versions = [] unless response.status == 200
99
+
100
+ @all_versions =
101
+ JSON.parse(response.body).
102
+ keys.
103
+ map { |v| version_class.new(v) }.
104
+ sort
105
+ end
106
+
107
+ # Overwrite the base class's requirements_up_to_date? method to instead
108
+ # check whether the latest version is allowed
109
+ def requirements_up_to_date?
110
+ return false unless latest_version
111
+
112
+ dependency.requirements.
113
+ map { |r| r.fetch(:requirement) }.
114
+ map { |r| requirement_class.new(r) }.
115
+ all? { |r| r.satisfied_by?(latest_version) }
116
+ end
117
+
118
+ def ignore_reqs
119
+ # Note: we use Gem::Requirement here because ignore conditions will
120
+ # be passed as Ruby ranges
121
+ ignored_versions.map { |req| Gem::Requirement.new(req.split(",")) }
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ Dependabot::UpdateCheckers.
128
+ register("elm-package", Dependabot::Elm::UpdateChecker)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/elm/version"
4
+ require "dependabot/elm/update_checker"
5
+
6
+ module Dependabot
7
+ module Elm
8
+ class UpdateChecker
9
+ class CliParser
10
+ INSTALL_DEPENDENCY_REGEX =
11
+ %r{([^\s]+\/[^\s]+)\s+(\d+\.\d+\.\d+)}.freeze
12
+ UPGRADE_DEPENDENCY_REGEX =
13
+ %r{([^\s]+\/[^\s]+) \(\d+\.\d+\.\d+ => (\d+\.\d+\.\d+)\)}.freeze
14
+
15
+ def self.decode_install_preview(text)
16
+ installs = {}
17
+
18
+ # Parse new installs
19
+ text.scan(INSTALL_DEPENDENCY_REGEX).
20
+ each { |n, v| installs[n] = Elm::Version.new(v) }
21
+
22
+ # Parse upgrades
23
+ text.scan(UPGRADE_DEPENDENCY_REGEX).
24
+ each { |n, v| installs[n] = Elm::Version.new(v) }
25
+
26
+ installs
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/shared_helpers"
4
+ require "dependabot/errors"
5
+ require "dependabot/elm/file_parser"
6
+ require "dependabot/elm/update_checker"
7
+ require "dependabot/elm/update_checker/cli_parser"
8
+ require "dependabot/elm/update_checker/requirements_updater"
9
+ require "dependabot/elm/requirement"
10
+
11
+ module Dependabot
12
+ module Elm
13
+ class UpdateChecker
14
+ class Elm18VersionResolver
15
+ class UnrecoverableState < StandardError; end
16
+
17
+ def initialize(dependency:, dependency_files:, candidate_versions:)
18
+ @dependency = dependency
19
+ @dependency_files = dependency_files
20
+ @candidate_versions = candidate_versions
21
+ end
22
+
23
+ def latest_resolvable_version(unlock_requirement:)
24
+ unless %i(none own all).include?(unlock_requirement)
25
+ raise "Invalid unlock setting: #{unlock_requirement}"
26
+ end
27
+
28
+ # Elm has no lockfile, so we will never create an update PR if
29
+ # unlock requirements are `none`. Just return the current version.
30
+ return current_version if unlock_requirement == :none
31
+
32
+ # Otherwise, we gotta check a few conditions to see if bumping
33
+ # wouldn't also bump other deps in elm-package.json
34
+ candidate_versions.sort.reverse_each do |version|
35
+ return version if can_update?(version, unlock_requirement)
36
+ end
37
+
38
+ # Fall back to returning the dependency's current version, which is
39
+ # presumed to be resolvable
40
+ current_version
41
+ end
42
+
43
+ def updated_dependencies_after_full_unlock
44
+ version = latest_resolvable_version(unlock_requirement: :all)
45
+ deps_after_install = fetch_install_metadata(target_version: version)
46
+
47
+ original_dependency_details.map do |original_dep|
48
+ new_version = deps_after_install.fetch(original_dep.name)
49
+
50
+ old_reqs = original_dep.requirements.map do |req|
51
+ requirement_class.new(req[:requirement])
52
+ end
53
+
54
+ next if old_reqs.all? { |req| req.satisfied_by?(new_version) }
55
+
56
+ new_requirements =
57
+ RequirementsUpdater.new(
58
+ requirements: original_dep.requirements,
59
+ latest_resolvable_version: new_version.to_s
60
+ ).updated_requirements
61
+
62
+ Dependency.new(
63
+ name: original_dep.name,
64
+ version: new_version.to_s,
65
+ requirements: new_requirements,
66
+ previous_version: original_dep.version,
67
+ previous_requirements: original_dep.requirements,
68
+ package_manager: original_dep.package_manager
69
+ )
70
+ end.compact
71
+ end
72
+
73
+ private
74
+
75
+ attr_reader :dependency, :dependency_files, :candidate_versions
76
+
77
+ def can_update?(version, unlock_requirement)
78
+ deps_after_install = fetch_install_metadata(target_version: version)
79
+
80
+ result = check_install_result(deps_after_install, version)
81
+
82
+ # If the install was clean then we can definitely update
83
+ return true if result == :clean_bump
84
+
85
+ # Otherwise, we can still update if the result was a forced full
86
+ # unlock and we're allowed to unlock other requirements
87
+ return false unless unlock_requirement == :all
88
+
89
+ result == :forced_full_unlock_bump
90
+ end
91
+
92
+ def check_install_result(deps_after_install, target_version)
93
+ # This can go one of 5 ways:
94
+ # 1) We bump our dep and no other dep is bumped
95
+ # 2) We bump our dep and another dep is bumped too
96
+ # Scenario: NoRedInk/datetimepicker bump to 3.0.2 also
97
+ # bumps elm-css to 14
98
+ # 3) We bump our dep but actually elm-package doesn't bump it
99
+ # Scenario: elm-css bump to 14 but datetimepicker is at 3.0.1
100
+ # 4) We bump our dep but elm-package just says
101
+ # "Packages configured successfully!"
102
+ # Narrator: they weren't
103
+ # Scenario: impossible dependency (i.e. elm-css 999.999.999)
104
+ # a <= v < b where a is greater than latest version
105
+ # 5) We bump our dep but elm-package blows up (not handled here)
106
+ # Scenario: rtfeldman/elm-css 14 && rtfeldman/hashed-class 1.0.0
107
+ # I'm not sure what's different from this scenario
108
+ # to 3), why it blows up instead of just rolling
109
+ # elm-css back to version 9 which is what
110
+ # hashed-class requires
111
+
112
+ # 4) We bump our dep but elm-package just says
113
+ # "Packages configured successfully!"
114
+ return :empty_elm_stuff_bug if deps_after_install.empty?
115
+
116
+ version_after_install = deps_after_install.fetch(dependency.name)
117
+
118
+ # 3) We bump our dep but actually elm-package doesn't bump it
119
+ return :downgrade_bug if version_after_install < target_version
120
+
121
+ other_top_level_deps_bumped =
122
+ original_dependency_details.
123
+ reject { |dep| dep.name == dependency.name }.
124
+ select do |dep|
125
+ reqs = dep.requirements.map { |r| r.fetch(:requirement) }
126
+ reqs = reqs.map { |r| requirement_class.new(r) }
127
+ reqs.any? { |r| !r.satisfied_by?(deps_after_install[dep.name]) }
128
+ end
129
+
130
+ # 2) We bump our dep and another dep is bumped
131
+ return :forced_full_unlock_bump if other_top_level_deps_bumped.any?
132
+
133
+ # 1) We bump our dep and no other dep is bumped
134
+ :clean_bump
135
+ end
136
+
137
+ def fetch_install_metadata(target_version:)
138
+ @install_cache ||= {}
139
+ @install_cache[target_version.to_s] ||=
140
+ SharedHelpers.in_a_temporary_directory do
141
+ write_temporary_dependency_files(target_version: target_version)
142
+
143
+ # Elm package install outputs a preview of the actions to be
144
+ # performed. We can use this preview to calculate whether it
145
+ # would do anything funny
146
+ command = "yes n | elm-package install"
147
+ response = run_shell_command(command)
148
+
149
+ deps_after_install = CliParser.decode_install_preview(response)
150
+
151
+ deps_after_install
152
+ rescue SharedHelpers::HelperSubprocessFailed => error
153
+ # 5) We bump our dep but elm-package blows up
154
+ handle_elm_package_errors(error)
155
+ end
156
+ end
157
+
158
+ def run_shell_command(command)
159
+ raw_response = nil
160
+ IO.popen(command, err: %i(child out)) do |process|
161
+ raw_response = process.read
162
+ end
163
+
164
+ # Raise an error with the output from the shell session if Elm
165
+ # returns a non-zero status
166
+ return raw_response if $CHILD_STATUS.success?
167
+
168
+ raise SharedHelpers::HelperSubprocessFailed.new(
169
+ raw_response,
170
+ command
171
+ )
172
+ end
173
+
174
+ def handle_elm_package_errors(error)
175
+ if error.message.include?("I cannot find a set of packages that " \
176
+ "works with your constraints")
177
+ raise Dependabot::DependencyFileNotResolvable, error.message
178
+ end
179
+
180
+ # I don't know any other errors
181
+ raise error
182
+ end
183
+
184
+ def write_temporary_dependency_files(target_version:)
185
+ dependency_files.each do |file|
186
+ path = file.name
187
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
188
+
189
+ File.write(
190
+ path,
191
+ updated_elm_package_content(file.content, target_version)
192
+ )
193
+ end
194
+ end
195
+
196
+ def updated_elm_package_content(content, version)
197
+ json = JSON.parse(content)
198
+
199
+ new_requirement = RequirementsUpdater.new(
200
+ requirements: dependency.requirements,
201
+ latest_resolvable_version: version.to_s
202
+ ).updated_requirements.first[:requirement]
203
+
204
+ json["dependencies"][dependency.name] = new_requirement
205
+ JSON.dump(json)
206
+ end
207
+
208
+ def original_dependency_details
209
+ @original_dependency_details ||=
210
+ Elm::FileParser.new(
211
+ dependency_files: dependency_files,
212
+ source: nil
213
+ ).parse
214
+ end
215
+
216
+ def current_version
217
+ return unless dependency.version
218
+
219
+ version_class.new(dependency.version)
220
+ end
221
+
222
+ def version_class
223
+ Elm::Version
224
+ end
225
+
226
+ def requirement_class
227
+ Elm::Requirement
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/shared_helpers"
4
+ require "dependabot/errors"
5
+ require "dependabot/elm/file_parser"
6
+ require "dependabot/elm/update_checker"
7
+ require "dependabot/elm/update_checker/cli_parser"
8
+ require "dependabot/elm/update_checker/requirements_updater"
9
+ require "dependabot/elm/requirement"
10
+
11
+ module Dependabot
12
+ module Elm
13
+ class UpdateChecker
14
+ class Elm19VersionResolver
15
+ class UnrecoverableState < StandardError; end
16
+
17
+ def initialize(dependency:, dependency_files:)
18
+ @dependency = dependency
19
+ @dependency_files = dependency_files
20
+ end
21
+
22
+ def latest_resolvable_version(unlock_requirement:)
23
+ unless %i(none own all).include?(unlock_requirement)
24
+ raise "Invalid unlock setting: #{unlock_requirement}"
25
+ end
26
+
27
+ # Elm has no lockfile, so we will never create an update PR if
28
+ # unlock requirements are `none`. Just return the current version.
29
+ return current_version if unlock_requirement == :none
30
+
31
+ # Otherwise, we gotta check a few conditions to see if bumping
32
+ # wouldn't also bump other deps in elm-package.json
33
+ fetch_latest_resolvable_version(unlock_requirement)
34
+ end
35
+
36
+ def updated_dependencies_after_full_unlock
37
+ changed_deps = install_metadata
38
+
39
+ original_dependency_details.map do |original_dep|
40
+ new_version = changed_deps.fetch(original_dep.name, nil)
41
+ next unless new_version
42
+
43
+ old_reqs = original_dep.requirements.map do |req|
44
+ requirement_class.new(req[:requirement])
45
+ end
46
+
47
+ next if old_reqs.all? { |req| req.satisfied_by?(new_version) }
48
+
49
+ new_requirements =
50
+ RequirementsUpdater.new(
51
+ requirements: original_dep.requirements,
52
+ latest_resolvable_version: new_version.to_s
53
+ ).updated_requirements
54
+
55
+ Dependency.new(
56
+ name: original_dep.name,
57
+ version: new_version.to_s,
58
+ requirements: new_requirements,
59
+ previous_version: original_dep.version,
60
+ previous_requirements: original_dep.requirements,
61
+ package_manager: original_dep.package_manager
62
+ )
63
+ end.compact
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :dependency, :dependency_files
69
+
70
+ def fetch_latest_resolvable_version(unlock_requirement)
71
+ changed_deps = install_metadata
72
+
73
+ result = check_install_result(changed_deps)
74
+ version_after_install = changed_deps.fetch(dependency.name)
75
+
76
+ # If the install was clean then we can definitely update
77
+ return version_after_install if result == :clean_bump
78
+
79
+ # Otherwise, we can still update if the result was a forced full
80
+ # unlock and we're allowed to unlock other requirements
81
+ return version_after_install if unlock_requirement == :all
82
+
83
+ current_version
84
+ end
85
+
86
+ def check_install_result(changed_deps)
87
+ other_deps_bumped =
88
+ changed_deps.
89
+ keys.
90
+ reject { |name| name == dependency.name }
91
+
92
+ return :forced_full_unlock_bump if other_deps_bumped.any?
93
+
94
+ :clean_bump
95
+ end
96
+
97
+ def install_metadata
98
+ @install_metadata ||=
99
+ SharedHelpers.in_a_temporary_directory do
100
+ write_temporary_dependency_files
101
+
102
+ # Elm package install outputs a preview of the actions to be
103
+ # performed. We can use this preview to calculate whether it
104
+ # would do anything funny
105
+ command = "yes n | elm19 install #{dependency.name}"
106
+ response = run_shell_command(command)
107
+
108
+ CliParser.decode_install_preview(response)
109
+ rescue SharedHelpers::HelperSubprocessFailed => error
110
+ # 5) We bump our dep but elm blows up
111
+ handle_elm_errors(error)
112
+ end
113
+ end
114
+
115
+ def run_shell_command(command)
116
+ raw_response = nil
117
+ IO.popen(command, err: %i(child out)) do |process|
118
+ raw_response = process.read
119
+ end
120
+
121
+ # Raise an error with the output from the shell session if Elm
122
+ # returns a non-zero status
123
+ return raw_response if $CHILD_STATUS.success?
124
+
125
+ raise SharedHelpers::HelperSubprocessFailed.new(
126
+ raw_response,
127
+ command
128
+ )
129
+ end
130
+
131
+ def handle_elm_errors(error)
132
+ if error.message.include?("OLD DEPENDENCIES") ||
133
+ error.message.include?("BAD JSON")
134
+ raise Dependabot::DependencyFileNotResolvable, error.message
135
+ end
136
+
137
+ # Raise any unrecognised errors
138
+ raise error
139
+ end
140
+
141
+ def write_temporary_dependency_files
142
+ dependency_files.each do |file|
143
+ path = file.name
144
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
145
+
146
+ File.write(path, updated_elm_json_content(file.content))
147
+ end
148
+ end
149
+
150
+ def updated_elm_json_content(content)
151
+ json = JSON.parse(content)
152
+
153
+ # Delete the dependency from the elm.json, so that we can use
154
+ # `elm install <dependency_name>` to generate the install plan
155
+ %w(dependencies test-dependencies).each do |type|
156
+ if json.dig(type, dependency.name)
157
+ json[type].delete(dependency.name)
158
+ end
159
+
160
+ %w(direct indirect).each do |category|
161
+ if json.dig(type, category, dependency.name)
162
+ json[type][category].delete(dependency.name)
163
+ end
164
+ end
165
+ end
166
+
167
+ json["source-directories"] = []
168
+
169
+ JSON.dump(json)
170
+ end
171
+
172
+ def original_dependency_details
173
+ @original_dependency_details ||=
174
+ Elm::FileParser.new(
175
+ dependency_files: dependency_files,
176
+ source: nil
177
+ ).parse
178
+ end
179
+
180
+ def current_version
181
+ return unless dependency.version
182
+
183
+ version_class.new(dependency.version)
184
+ end
185
+
186
+ def version_class
187
+ Elm::Version
188
+ end
189
+
190
+ def requirement_class
191
+ Elm::Requirement
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end