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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/dependency_file"
4
+ require "dependabot/bundler/file_parser"
5
+ require "dependabot/bundler/file_updater/gemspec_sanitizer"
6
+
7
+ module Dependabot
8
+ module Bundler
9
+ class FileParser
10
+ class FilePreparer
11
+ def initialize(dependency_files:)
12
+ @dependency_files = dependency_files
13
+ end
14
+
15
+ def prepared_dependency_files
16
+ files = []
17
+
18
+ gemspecs.compact.each do |file|
19
+ files << DependencyFile.new(
20
+ name: file.name,
21
+ content: sanitize_gemspec_content(file.content),
22
+ directory: file.directory,
23
+ support_file: file.support_file?
24
+ )
25
+ end
26
+
27
+ files += [
28
+ gemfile,
29
+ *evaled_gemfiles,
30
+ lockfile,
31
+ ruby_version_file,
32
+ *imported_ruby_files
33
+ ].compact
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :dependency_files
39
+
40
+ def gemfile
41
+ dependency_files.find { |f| f.name == "Gemfile" } ||
42
+ dependency_files.find { |f| f.name == "gems.rb" }
43
+ end
44
+
45
+ def evaled_gemfiles
46
+ dependency_files.
47
+ reject { |f| f.name.end_with?(".gemspec") }.
48
+ reject { |f| f.name.end_with?(".lock") }.
49
+ reject { |f| f.name.end_with?(".ruby-version") }.
50
+ reject { |f| f.name == "Gemfile" }.
51
+ reject { |f| f.name == "gems.rb" }.
52
+ reject { |f| f.name == "gems.locked" }
53
+ end
54
+
55
+ def lockfile
56
+ dependency_files.find { |f| f.name == "Gemfile.lock" } ||
57
+ dependency_files.find { |f| f.name == "gems.locked" }
58
+ end
59
+
60
+ def gemspecs
61
+ dependency_files.select { |f| f.name.end_with?(".gemspec") }
62
+ end
63
+
64
+ def ruby_version_file
65
+ dependency_files.find { |f| f.name == ".ruby-version" }
66
+ end
67
+
68
+ def imported_ruby_files
69
+ dependency_files.
70
+ select { |f| f.name.end_with?(".rb") }.
71
+ reject { |f| f.name == "gems.rb" }
72
+ end
73
+
74
+ def sanitize_gemspec_content(gemspec_content)
75
+ # No need to set the version correctly - this is just an update
76
+ # check so we're not going to persist any changes to the lockfile.
77
+ FileUpdater::GemspecSanitizer.
78
+ new(replacement_version: "0.0.1").
79
+ rewrite(gemspec_content)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parser/current"
4
+ require "dependabot/bundler/file_parser"
5
+
6
+ module Dependabot
7
+ module Bundler
8
+ class FileParser
9
+ # Checks whether a dependency is declared in a Gemfile
10
+ class GemfileChecker
11
+ def initialize(dependency:, gemfile:)
12
+ @dependency = dependency
13
+ @gemfile = gemfile
14
+ end
15
+
16
+ def includes_dependency?
17
+ return false unless Parser::CurrentRuby.parse(gemfile.content)
18
+
19
+ Parser::CurrentRuby.parse(gemfile.content).children.any? do |node|
20
+ deep_check_for_gem(node)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :dependency, :gemfile
27
+
28
+ def deep_check_for_gem(node)
29
+ return true if declares_targeted_gem?(node)
30
+ return false unless node.is_a?(Parser::AST::Node)
31
+
32
+ node.children.any? do |child_node|
33
+ deep_check_for_gem(child_node)
34
+ end
35
+ end
36
+
37
+ def declares_targeted_gem?(node)
38
+ return false unless node.is_a?(Parser::AST::Node)
39
+ return false unless node.children[1] == :gem
40
+
41
+ node.children[2].children.first == dependency.name
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_updaters"
4
+ require "dependabot/file_updaters/base"
5
+
6
+ module Dependabot
7
+ module Bundler
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ require_relative "file_updater/gemfile_updater"
10
+ require_relative "file_updater/gemspec_updater"
11
+ require_relative "file_updater/lockfile_updater"
12
+
13
+ def self.updated_files_regex
14
+ [
15
+ /^Gemfile$/,
16
+ /^Gemfile\.lock$/,
17
+ /^gems\.rb$/,
18
+ /^gems\.locked$/,
19
+ /^*\.gemspec$/
20
+ ]
21
+ end
22
+
23
+ def updated_dependency_files
24
+ updated_files = []
25
+
26
+ if gemfile && file_changed?(gemfile)
27
+ updated_files <<
28
+ updated_file(
29
+ file: gemfile,
30
+ content: updated_gemfile_content(gemfile)
31
+ )
32
+ end
33
+
34
+ if lockfile && dependencies.any?(&:appears_in_lockfile?)
35
+ updated_files <<
36
+ updated_file(file: lockfile, content: updated_lockfile_content)
37
+ end
38
+
39
+ top_level_gemspecs.each do |file|
40
+ next unless file_changed?(file)
41
+
42
+ updated_files <<
43
+ updated_file(file: file, content: updated_gemspec_content(file))
44
+ end
45
+
46
+ evaled_gemfiles.each do |file|
47
+ next unless file_changed?(file)
48
+
49
+ updated_files <<
50
+ updated_file(file: file, content: updated_gemfile_content(file))
51
+ end
52
+
53
+ updated_files
54
+ end
55
+
56
+ private
57
+
58
+ def check_required_files
59
+ file_names = dependency_files.map(&:name)
60
+
61
+ if lockfile && !gemfile
62
+ raise "A Gemfile must be provided if a lockfile is!"
63
+ end
64
+
65
+ return if file_names.any? { |name| name.match?(%r{^[^/]*\.gemspec$}) }
66
+ return if gemfile
67
+
68
+ raise "A gemspec or Gemfile must be provided!"
69
+ end
70
+
71
+ def gemfile
72
+ @gemfile ||= get_original_file("Gemfile") ||
73
+ get_original_file("gems.rb")
74
+ end
75
+
76
+ def lockfile
77
+ @lockfile ||= get_original_file("Gemfile.lock") ||
78
+ get_original_file("gems.locked")
79
+ end
80
+
81
+ def evaled_gemfiles
82
+ @evaled_gemfiles ||=
83
+ dependency_files.
84
+ reject { |f| f.name.end_with?(".gemspec") }.
85
+ reject { |f| f.name.end_with?(".lock") }.
86
+ reject { |f| f.name.end_with?(".ruby-version") }.
87
+ reject { |f| f.name == "Gemfile" }.
88
+ reject { |f| f.name == "gems.rb" }.
89
+ reject { |f| f.name == "gems.locked" }
90
+ end
91
+
92
+ def updated_gemfile_content(file)
93
+ GemfileUpdater.new(
94
+ dependencies: dependencies,
95
+ gemfile: file
96
+ ).updated_gemfile_content
97
+ end
98
+
99
+ def updated_gemspec_content(gemspec)
100
+ GemspecUpdater.new(
101
+ dependencies: dependencies,
102
+ gemspec: gemspec
103
+ ).updated_gemspec_content
104
+ end
105
+
106
+ def updated_lockfile_content
107
+ @updated_lockfile_content ||=
108
+ LockfileUpdater.new(
109
+ dependencies: dependencies,
110
+ dependency_files: dependency_files,
111
+ credentials: credentials
112
+ ).updated_lockfile_content
113
+ end
114
+
115
+ def top_level_gemspecs
116
+ dependency_files.
117
+ select { |file| file.name.end_with?(".gemspec") }.
118
+ reject(&:support_file?)
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ Dependabot::FileUpdaters.
125
+ register("bundler", Dependabot::Bundler::FileUpdater)
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/bundler/file_updater"
4
+
5
+ module Dependabot
6
+ module Bundler
7
+ class FileUpdater
8
+ class GemfileUpdater
9
+ require_relative "git_pin_replacer"
10
+ require_relative "git_source_remover"
11
+ require_relative "requirement_replacer"
12
+
13
+ def initialize(dependencies:, gemfile:)
14
+ @dependencies = dependencies
15
+ @gemfile = gemfile
16
+ end
17
+
18
+ def updated_gemfile_content
19
+ content = gemfile.content
20
+
21
+ dependencies.each do |dependency|
22
+ content = replace_gemfile_version_requirement(
23
+ dependency,
24
+ gemfile,
25
+ content
26
+ )
27
+
28
+ if remove_git_source?(dependency)
29
+ content = remove_gemfile_git_source(dependency, content)
30
+ end
31
+
32
+ if update_git_pin?(dependency)
33
+ content = update_gemfile_git_pin(dependency, gemfile, content)
34
+ end
35
+ end
36
+
37
+ content
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :dependencies, :gemfile
43
+
44
+ def replace_gemfile_version_requirement(dependency, file, content)
45
+ return content unless requirement_changed?(file, dependency)
46
+
47
+ updated_requirement =
48
+ dependency.requirements.
49
+ find { |r| r[:file] == file.name }.
50
+ fetch(:requirement)
51
+
52
+ previous_requirement =
53
+ dependency.previous_requirements.
54
+ find { |r| r[:file] == file.name }.
55
+ fetch(:requirement)
56
+
57
+ RequirementReplacer.new(
58
+ dependency: dependency,
59
+ file_type: :gemfile,
60
+ updated_requirement: updated_requirement,
61
+ previous_requirement: previous_requirement
62
+ ).rewrite(content)
63
+ end
64
+
65
+ def requirement_changed?(file, dependency)
66
+ changed_requirements =
67
+ dependency.requirements - dependency.previous_requirements
68
+
69
+ changed_requirements.any? { |f| f[:file] == file.name }
70
+ end
71
+
72
+ def remove_git_source?(dependency)
73
+ old_gemfile_req =
74
+ dependency.previous_requirements.
75
+ find { |f| %w(Gemfile gems.rb).include?(f[:file]) }
76
+
77
+ return false unless old_gemfile_req&.dig(:source, :type) == "git"
78
+
79
+ new_gemfile_req =
80
+ dependency.requirements.
81
+ find { |f| %w(Gemfile gems.rb).include?(f[:file]) }
82
+
83
+ new_gemfile_req[:source].nil?
84
+ end
85
+
86
+ def update_git_pin?(dependency)
87
+ new_gemfile_req =
88
+ dependency.requirements.
89
+ find { |f| %w(Gemfile gems.rb).include?(f[:file]) }
90
+ return false unless new_gemfile_req&.dig(:source, :type) == "git"
91
+
92
+ # If the new requirement is a git dependency with a ref then there's
93
+ # no harm in doing an update
94
+ new_gemfile_req.dig(:source, :ref)
95
+ end
96
+
97
+ def remove_gemfile_git_source(dependency, content)
98
+ GitSourceRemover.new(dependency: dependency).rewrite(content)
99
+ end
100
+
101
+ def update_gemfile_git_pin(dependency, file, content)
102
+ new_pin =
103
+ dependency.requirements.
104
+ find { |f| f[:file] == file.name }.
105
+ fetch(:source).fetch(:ref)
106
+
107
+ GitPinReplacer.
108
+ new(dependency: dependency, new_pin: new_pin).
109
+ rewrite(content)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,50 @@
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 GemspecDependencyNameFinder
10
+ attr_reader :gemspec_content
11
+
12
+ def initialize(gemspec_content:)
13
+ @gemspec_content = gemspec_content
14
+ end
15
+
16
+ # rubocop:disable Security/Eval
17
+ def dependency_name
18
+ ast = Parser::CurrentRuby.parse(gemspec_content)
19
+ dependency_name_node = find_dependency_name_node(ast)
20
+ return unless dependency_name_node
21
+
22
+ begin
23
+ eval(dependency_name_node.children[2].loc.expression.source)
24
+ rescue StandardError
25
+ nil # If we can't evaluate the expression just return nil
26
+ end
27
+ end
28
+ # rubocop:enable Security/Eval
29
+
30
+ private
31
+
32
+ def find_dependency_name_node(node)
33
+ return unless node.is_a?(Parser::AST::Node)
34
+ return node if declares_dependency_name?(node)
35
+
36
+ node.children.find do |cn|
37
+ dependency_name_node = find_dependency_name_node(cn)
38
+ break dependency_name_node if dependency_name_node
39
+ end
40
+ end
41
+
42
+ def declares_dependency_name?(node)
43
+ return false unless node.is_a?(Parser::AST::Node)
44
+
45
+ node.children[1] == :name=
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,296 @@
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 GemspecSanitizer
10
+ UNNECESSARY_ASSIGNMENTS = %i(
11
+ bindir=
12
+ cert_chain=
13
+ email=
14
+ executables=
15
+ extra_rdoc_files=
16
+ homepage=
17
+ license=
18
+ licenses=
19
+ metadata=
20
+ post_install_message=
21
+ rdoc_options=
22
+ ).freeze
23
+
24
+ attr_reader :replacement_version
25
+
26
+ def initialize(replacement_version:)
27
+ @replacement_version = replacement_version
28
+ end
29
+
30
+ def rewrite(content)
31
+ buffer = Parser::Source::Buffer.new("(gemspec_content)")
32
+ buffer.source = content
33
+ ast = Parser::CurrentRuby.new.parse(buffer)
34
+
35
+ Rewriter.
36
+ new(replacement_version: replacement_version).
37
+ rewrite(buffer, ast)
38
+ end
39
+
40
+ class Rewriter < Parser::TreeRewriter
41
+ def initialize(replacement_version:)
42
+ @replacement_version = replacement_version
43
+ end
44
+
45
+ def on_send(node)
46
+ # Wrap any `require` or `require_relative` calls in a rescue
47
+ # block, as we might not have the required files
48
+ wrap_require(node) if requires_file?(node)
49
+
50
+ # Remove any assignments to a VERSION constant (or similar), as
51
+ # that constant probably comes from a required file
52
+ replace_version_assignments(node)
53
+
54
+ # Replace the `s.files= ...` assignment with a blank array, as
55
+ # occassionally a File.open(..).readlines pattern is used
56
+ replace_file_assignments(node)
57
+
58
+ # Replace the `s.require_path= ...` assignment, as
59
+ # occassionally a Dir['lib'] pattern is used
60
+ replace_require_paths_assignments(node)
61
+
62
+ # Replace any `File.read(...)` calls with a dummy string
63
+ replace_file_reads(node)
64
+
65
+ # Remove the arguments from any `Find.find(...)` calls
66
+ remove_find_dot_find_args(node)
67
+
68
+ remove_unnecessary_assignments(node)
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :replacement_version
74
+
75
+ def requires_file?(node)
76
+ %i(require require_relative).include?(node.children[1])
77
+ end
78
+
79
+ def wrap_require(node)
80
+ replace(
81
+ node.loc.expression,
82
+ "begin\n"\
83
+ "#{node.loc.expression.source_line}\n"\
84
+ "rescue LoadError\n"\
85
+ "end"
86
+ )
87
+ end
88
+
89
+ def replace_version_assignments(node)
90
+ return unless node.is_a?(Parser::AST::Node)
91
+
92
+ if node_assigns_to_version_constant?(node)
93
+ return replace_constant(node)
94
+ end
95
+
96
+ node.children.each { |child| replace_version_assignments(child) }
97
+ end
98
+
99
+ def replace_file_assignments(node)
100
+ return unless node.is_a?(Parser::AST::Node)
101
+
102
+ if node_assigns_files_to_var?(node)
103
+ return replace_file_assignment(node)
104
+ end
105
+
106
+ node.children.each { |child| replace_file_assignments(child) }
107
+ end
108
+
109
+ def replace_require_paths_assignments(node)
110
+ return unless node.is_a?(Parser::AST::Node)
111
+
112
+ if node_assigns_require_paths?(node)
113
+ return replace_require_paths_assignment(node)
114
+ end
115
+
116
+ node.children.each do |child|
117
+ replace_require_paths_assignments(child)
118
+ end
119
+ end
120
+
121
+ def node_assigns_to_version_constant?(node)
122
+ return false unless node.is_a?(Parser::AST::Node)
123
+ return false unless node.children.first.is_a?(Parser::AST::Node)
124
+ return false unless node.children.first&.type == :lvar
125
+
126
+ return true if node.children[1] == :version=
127
+ return true if node_is_version_constant?(node.children.last)
128
+ return true if node_calls_version_constant?(node.children.last)
129
+
130
+ node_interpolates_version_constant?(node.children.last)
131
+ end
132
+
133
+ def node_assigns_files_to_var?(node)
134
+ return false unless node.is_a?(Parser::AST::Node)
135
+ return false unless node.children.first.is_a?(Parser::AST::Node)
136
+ return false unless node.children.first&.type == :lvar
137
+ return false unless node.children[1] == :files=
138
+
139
+ node.children[2]&.type == :send
140
+ end
141
+
142
+ def node_assigns_require_paths?(node)
143
+ return false unless node.is_a?(Parser::AST::Node)
144
+ return false unless node.children.first.is_a?(Parser::AST::Node)
145
+ return false unless node.children.first&.type == :lvar
146
+
147
+ node.children[1] == :require_paths=
148
+ end
149
+
150
+ def replace_file_reads(node)
151
+ return unless node.is_a?(Parser::AST::Node)
152
+ return if node.children[1] == :version=
153
+ return replace_file_read(node) if node_reads_a_file?(node)
154
+ return replace_file_readlines(node) if node_uses_readlines?(node)
155
+
156
+ node.children.each { |child| replace_file_reads(child) }
157
+ end
158
+
159
+ def node_reads_a_file?(node)
160
+ return false unless node.is_a?(Parser::AST::Node)
161
+ return false unless node.children.first.is_a?(Parser::AST::Node)
162
+ return false unless node.children.first&.type == :const
163
+ return false unless node.children.first.children.last == :File
164
+
165
+ node.children[1] == :read
166
+ end
167
+
168
+ def node_uses_readlines?(node)
169
+ return false unless node.is_a?(Parser::AST::Node)
170
+ return false unless node.children.first.is_a?(Parser::AST::Node)
171
+ return false unless node.children.first&.type == :const
172
+ return false unless node.children.first.children.last == :File
173
+
174
+ node.children[1] == :readlines
175
+ end
176
+
177
+ def remove_find_dot_find_args(node)
178
+ return unless node.is_a?(Parser::AST::Node)
179
+ return if node.children[1] == :version=
180
+ return remove_find_args(node) if node_calls_find_dot_find?(node)
181
+
182
+ node.children.each { |child| remove_find_dot_find_args(child) }
183
+ end
184
+
185
+ def node_calls_find_dot_find?(node)
186
+ return false unless node.is_a?(Parser::AST::Node)
187
+ return false unless node.children.first.is_a?(Parser::AST::Node)
188
+ return false unless node.children.first&.type == :const
189
+ return false unless node.children.first.children.last == :Find
190
+
191
+ node.children[1] == :find
192
+ end
193
+
194
+ def remove_unnecessary_assignments(node)
195
+ return unless node.is_a?(Parser::AST::Node)
196
+
197
+ if unnecessary_assignment?(node) &&
198
+ node.children.last&.location&.respond_to?(:heredoc_end)
199
+ range_to_remove = node.loc.expression.join(
200
+ node.children.last.location.heredoc_end
201
+ )
202
+ return replace(range_to_remove, '"sanitized"')
203
+ elsif unnecessary_assignment?(node)
204
+ return replace(node.loc.expression, '"sanitized"')
205
+ end
206
+
207
+ node.children.each do |child|
208
+ remove_unnecessary_assignments(child)
209
+ end
210
+ end
211
+
212
+ def unnecessary_assignment?(node)
213
+ return false unless node.is_a?(Parser::AST::Node)
214
+ return false unless node.children.first.is_a?(Parser::AST::Node)
215
+ return false unless node.children.first&.type == :lvar
216
+
217
+ UNNECESSARY_ASSIGNMENTS.include?(node.children[1])
218
+ end
219
+
220
+ def node_is_version_constant?(node)
221
+ return false unless node.is_a?(Parser::AST::Node)
222
+ return false unless node.type == :const
223
+
224
+ node.children.last.to_s.match?(/version/i)
225
+ end
226
+
227
+ def node_calls_version_constant?(node)
228
+ return false unless node.is_a?(Parser::AST::Node)
229
+ return false unless node.type == :send
230
+
231
+ node.children.any? { |n| node_is_version_constant?(n) }
232
+ end
233
+
234
+ def node_interpolates_version_constant?(node)
235
+ return false unless node.is_a?(Parser::AST::Node)
236
+ return false unless node.type == :dstr
237
+
238
+ node.children.
239
+ select { |n| n.type == :begin }.
240
+ flat_map(&:children).
241
+ any? { |n| node_is_version_constant?(n) }
242
+ end
243
+
244
+ def replace_constant(node)
245
+ case node.children.last&.type
246
+ when :str, :int then nil # no-op
247
+ when :const, :send, :lvar
248
+ replace(
249
+ node.children.last.loc.expression,
250
+ %("#{replacement_version}")
251
+ )
252
+ when :dstr
253
+ node.children.last.children.
254
+ select { |n| n.type == :begin }.
255
+ flat_map(&:children).
256
+ select { |n| node_is_version_constant?(n) }.
257
+ each do |n|
258
+ replace(
259
+ n.loc.expression,
260
+ %("#{replacement_version}")
261
+ )
262
+ end
263
+ else
264
+ raise "Unexpected node type #{node.children.last&.type}"
265
+ end
266
+ end
267
+
268
+ def replace_file_assignment(node)
269
+ replace(node.children.last.loc.expression, "[]")
270
+ end
271
+
272
+ def replace_require_paths_assignment(node)
273
+ replace(node.children.last.loc.expression, "['lib']")
274
+ end
275
+
276
+ def replace_file_read(node)
277
+ replace(node.loc.expression, '"text"')
278
+ end
279
+
280
+ def replace_file_readlines(node)
281
+ replace(node.loc.expression, '["text"]')
282
+ end
283
+
284
+ def remove_find_args(node)
285
+ last_arg = node.children.last
286
+
287
+ range_to_remove =
288
+ last_arg.loc.expression.join(node.children[2].loc.begin.begin)
289
+
290
+ remove(range_to_remove)
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end