dependabot-maven 0.85.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/file_parsers"
7
+ require "dependabot/file_parsers/base"
8
+ require "dependabot/errors"
9
+
10
+ # The best Maven documentation is at:
11
+ # - http://maven.apache.org/pom.html
12
+ module Dependabot
13
+ module Maven
14
+ class FileParser < Dependabot::FileParsers::Base
15
+ require "dependabot/file_parsers/base/dependency_set"
16
+ require_relative "file_parser/property_value_finder"
17
+
18
+ # The following "dependencies" are candidates for updating:
19
+ # - The project's parent
20
+ # - Any dependencies (incl. those in dependencyManagement or plugins)
21
+ # - Any plugins (incl. those in pluginManagement)
22
+ # - Any extensions
23
+ DEPENDENCY_SELECTOR = "project > parent, "\
24
+ "dependencies > dependency, "\
25
+ "extensions > extension"
26
+ PLUGIN_SELECTOR = "plugins > plugin"
27
+
28
+ # Regex to get the property name from a declaration that uses a property
29
+ PROPERTY_REGEX = /\$\{(?<property>.*?)\}/.freeze
30
+
31
+ def parse
32
+ dependency_set = DependencySet.new
33
+ pomfiles.each { |pom| dependency_set += pomfile_dependencies(pom) }
34
+ dependency_set.dependencies
35
+ end
36
+
37
+ private
38
+
39
+ def pomfile_dependencies(pom)
40
+ dependency_set = DependencySet.new
41
+
42
+ errors = []
43
+ doc = Nokogiri::XML(pom.content)
44
+ doc.remove_namespaces!
45
+
46
+ doc.css(DEPENDENCY_SELECTOR).each do |dependency_node|
47
+ dep = dependency_from_dependency_node(pom, dependency_node)
48
+ dependency_set << dep if dep
49
+ rescue DependencyFileNotEvaluatable => error
50
+ errors << error
51
+ end
52
+
53
+ doc.css(PLUGIN_SELECTOR).each do |dependency_node|
54
+ dep = dependency_from_plugin_node(pom, dependency_node)
55
+ dependency_set << dep if dep
56
+ rescue DependencyFileNotEvaluatable => error
57
+ errors << error
58
+ end
59
+
60
+ raise errors.first if errors.any? && dependency_set.dependencies.none?
61
+
62
+ dependency_set
63
+ end
64
+
65
+ def dependency_from_dependency_node(pom, dependency_node)
66
+ return unless (name = dependency_name(dependency_node, pom))
67
+ return if internal_dependency_names.include?(name)
68
+
69
+ build_dependency(pom, dependency_node, name)
70
+ end
71
+
72
+ def dependency_from_plugin_node(pom, dependency_node)
73
+ return unless (name = plugin_name(dependency_node, pom))
74
+ return if internal_dependency_names.include?(name)
75
+
76
+ build_dependency(pom, dependency_node, name)
77
+ end
78
+
79
+ def build_dependency(pom, dependency_node, name)
80
+ property_details =
81
+ {
82
+ property_name: version_property_name(dependency_node),
83
+ property_source: property_source(dependency_node, pom)
84
+ }.compact
85
+
86
+ Dependency.new(
87
+ name: name,
88
+ version: dependency_version(pom, dependency_node),
89
+ package_manager: "maven",
90
+ requirements: [{
91
+ requirement: dependency_requirement(pom, dependency_node),
92
+ file: pom.name,
93
+ groups: [],
94
+ source: nil,
95
+ metadata: {
96
+ packaging_type: packaging_type(pom, dependency_node)
97
+ }.merge(property_details)
98
+ }]
99
+ )
100
+ end
101
+
102
+ def dependency_name(dependency_node, pom)
103
+ return unless dependency_node.at_xpath("./groupId")
104
+ return unless dependency_node.at_xpath("./artifactId")
105
+
106
+ [
107
+ evaluated_value(
108
+ dependency_node.at_xpath("./groupId").content.strip,
109
+ pom
110
+ ),
111
+ evaluated_value(
112
+ dependency_node.at_xpath("./artifactId").content.strip,
113
+ pom
114
+ )
115
+ ].join(":")
116
+ end
117
+
118
+ def plugin_name(dependency_node, pom)
119
+ return unless plugin_group_id(pom, dependency_node)
120
+ return unless dependency_node.at_xpath("./artifactId")
121
+
122
+ [
123
+ plugin_group_id(pom, dependency_node),
124
+ evaluated_value(
125
+ dependency_node.at_xpath("./artifactId").content.strip,
126
+ pom
127
+ )
128
+ ].join(":")
129
+ end
130
+
131
+ def plugin_group_id(pom, node)
132
+ return "org.apache.maven.plugins" unless node.at_xpath("./groupId")
133
+
134
+ evaluated_value(
135
+ node.at_xpath("./groupId").content.strip,
136
+ pom
137
+ )
138
+ end
139
+
140
+ def dependency_version(pom, dependency_node)
141
+ requirement = dependency_requirement(pom, dependency_node)
142
+ return nil unless requirement
143
+
144
+ # If a range is specified then we can't tell the exact version
145
+ return nil if requirement.include?(",")
146
+
147
+ # Remove brackets if present (and not denoting a range)
148
+ requirement.gsub(/[\(\)\[\]]/, "").strip
149
+ end
150
+
151
+ def dependency_requirement(pom, dependency_node)
152
+ return unless dependency_node.at_xpath("./version")
153
+
154
+ version_content = dependency_node.at_xpath("./version").content.strip
155
+ version_content = evaluated_value(version_content, pom)
156
+
157
+ version_content.empty? ? nil : version_content
158
+ end
159
+
160
+ def packaging_type(pom, dependency_node)
161
+ return "pom" if dependency_node.node_name == "parent"
162
+ return "jar" unless dependency_node.at_xpath("./type")
163
+
164
+ packaging_type_content = dependency_node.at_xpath("./type").
165
+ content.strip
166
+
167
+ evaluated_value(packaging_type_content, pom)
168
+ end
169
+
170
+ def version_property_name(dependency_node)
171
+ return unless dependency_node.at_xpath("./version")
172
+
173
+ version_content = dependency_node.at_xpath("./version").content.strip
174
+
175
+ return unless version_content.match?(PROPERTY_REGEX)
176
+
177
+ version_content.
178
+ match(PROPERTY_REGEX).
179
+ named_captures.fetch("property")
180
+ end
181
+
182
+ def evaluated_value(value, pom)
183
+ return value unless value.match?(PROPERTY_REGEX)
184
+
185
+ property_name = value.match(PROPERTY_REGEX).
186
+ named_captures.fetch("property")
187
+ property_value = value_for_property(property_name, pom)
188
+
189
+ value.gsub(PROPERTY_REGEX, property_value)
190
+ end
191
+
192
+ def property_source(dependency_node, pom)
193
+ property_name = version_property_name(dependency_node)
194
+ return unless property_name
195
+
196
+ declaring_pom =
197
+ property_value_finder.
198
+ property_details(property_name: property_name, callsite_pom: pom)&.
199
+ fetch(:file)
200
+
201
+ return declaring_pom if declaring_pom
202
+
203
+ msg = "Property not found: #{property_name}"
204
+ raise DependencyFileNotEvaluatable, msg
205
+ end
206
+
207
+ def value_for_property(property_name, pom)
208
+ value =
209
+ property_value_finder.
210
+ property_details(property_name: property_name, callsite_pom: pom)&.
211
+ fetch(:value)
212
+
213
+ return value if value
214
+
215
+ msg = "Property not found: #{property_name}"
216
+ raise DependencyFileNotEvaluatable, msg
217
+ end
218
+
219
+ # Cached, since this can makes calls to the registry (to get property
220
+ # values from parent POMs)
221
+ def property_value_finder
222
+ @property_value_finder ||=
223
+ PropertyValueFinder.new(dependency_files: dependency_files)
224
+ end
225
+
226
+ def pomfiles
227
+ # Note: this (correctly) excludes any parent POMs that were downloaded
228
+ @pomfiles ||=
229
+ dependency_files.select { |f| f.name.end_with?("pom.xml") }
230
+ end
231
+
232
+ def internal_dependency_names
233
+ @internal_dependency_names ||=
234
+ dependency_files.map do |pom|
235
+ doc = Nokogiri::XML(pom.content)
236
+ group_id = doc.at_css("project > groupId") ||
237
+ doc.at_css("project > parent > groupId")
238
+ artifact_id = doc.at_css("project > artifactId")
239
+
240
+ next unless group_id && artifact_id
241
+
242
+ [group_id.content.strip, artifact_id.content.strip].join(":")
243
+ end.compact
244
+ end
245
+
246
+ def check_required_files
247
+ raise "No pom.xml!" unless get_original_file("pom.xml")
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ Dependabot::FileParsers.register("maven", Dependabot::Maven::FileParser)
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/maven/file_updater"
5
+ require "dependabot/maven/file_parser"
6
+ require "dependabot/maven/file_parser/property_value_finder"
7
+
8
+ module Dependabot
9
+ module Maven
10
+ class FileUpdater
11
+ class DeclarationFinder
12
+ DECLARATION_REGEX =
13
+ %r{<parent>.*?</parent>|<dependency>.*?</dependency>|
14
+ <plugin>.*?</plugin>|<extension>.*?</extension>}mx.freeze
15
+
16
+ attr_reader :dependency, :declaring_requirement, :dependency_files
17
+
18
+ def initialize(dependency:, dependency_files:, declaring_requirement:)
19
+ @dependency = dependency
20
+ @dependency_files = dependency_files
21
+ @declaring_requirement = declaring_requirement
22
+ end
23
+
24
+ def declaration_strings
25
+ @declaration_strings ||= fetch_pom_declaration_strings
26
+ end
27
+
28
+ def declaration_nodes
29
+ declaration_strings.map do |declaration_string|
30
+ Nokogiri::XML(declaration_string)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def declaring_pom
37
+ filename = declaring_requirement.fetch(:file)
38
+ declaring_pom = dependency_files.find { |f| f.name == filename }
39
+ return declaring_pom if declaring_pom
40
+
41
+ raise "No pom found with name #{filename}!"
42
+ end
43
+
44
+ def dependency_name
45
+ dependency.name
46
+ end
47
+
48
+ def fetch_pom_declaration_strings
49
+ deep_find_declarations(declaring_pom.content).select do |nd|
50
+ node = Nokogiri::XML(nd)
51
+ node.remove_namespaces!
52
+ next false unless node_group_id(node)
53
+ next false unless node.at_xpath("./*/artifactId")
54
+
55
+ node_name = [
56
+ node_group_id(node),
57
+ evaluated_value(node.at_xpath("./*/artifactId").content.strip)
58
+ ].compact.join(":")
59
+
60
+ next false unless node_name == dependency_name
61
+ next false unless packaging_type_matches?(node)
62
+
63
+ declaring_requirement_matches?(node)
64
+ end
65
+ end
66
+
67
+ def node_group_id(node)
68
+ unless node.at_xpath("./*/groupId") || node.at_xpath("./plugin")
69
+ return
70
+ end
71
+ return "org.apache.maven.plugins" unless node.at_xpath("./*/groupId")
72
+
73
+ evaluated_value(node.at_xpath("./*/groupId").content.strip)
74
+ end
75
+
76
+ def deep_find_declarations(string)
77
+ string.scan(DECLARATION_REGEX).flat_map do |matching_node|
78
+ [matching_node, *deep_find_declarations(matching_node[1..-1])]
79
+ end
80
+ end
81
+
82
+ def declaring_requirement_matches?(node)
83
+ node_requirement = node.at_css("version")&.content&.strip
84
+
85
+ if declaring_requirement.dig(:metadata, :property_name)
86
+ return false unless node_requirement
87
+
88
+ property_name =
89
+ node_requirement.
90
+ match(Maven::FileParser::PROPERTY_REGEX)&.
91
+ named_captures&.
92
+ fetch("property")
93
+
94
+ property_name == declaring_requirement[:metadata][:property_name]
95
+ else
96
+ node_requirement == declaring_requirement.fetch(:requirement)
97
+ end
98
+ end
99
+
100
+ def packaging_type_matches?(node)
101
+ type = declaring_requirement.dig(:metadata, :packaging_type)
102
+ type == packaging_type(node)
103
+ end
104
+
105
+ def packaging_type(dependency_node)
106
+ return "pom" if dependency_node.child.node_name == "parent"
107
+ return "jar" unless dependency_node.at_xpath("./*/type")
108
+
109
+ packaging_type_content = dependency_node.at_xpath("./*/type").
110
+ content.strip
111
+
112
+ evaluated_value(packaging_type_content)
113
+ end
114
+
115
+ def evaluated_value(value)
116
+ return value unless value.match?(Maven::FileParser::PROPERTY_REGEX)
117
+
118
+ property_name =
119
+ value.match(Maven::FileParser::PROPERTY_REGEX).
120
+ named_captures.fetch("property")
121
+
122
+ property_value =
123
+ property_value_finder.
124
+ property_details(
125
+ property_name: property_name,
126
+ callsite_pom: declaring_pom
127
+ )&.fetch(:value)
128
+
129
+ return value unless property_value
130
+
131
+ value.gsub(Maven::FileParser::PROPERTY_REGEX, property_value)
132
+ end
133
+
134
+ def property_value_finder
135
+ @property_value_finder ||=
136
+ Maven::FileParser::PropertyValueFinder.
137
+ new(dependency_files: dependency_files)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "dependabot/dependency_file"
6
+ require "dependabot/maven/file_updater"
7
+ require "dependabot/maven/file_parser/property_value_finder"
8
+
9
+ module Dependabot
10
+ module Maven
11
+ class FileUpdater
12
+ class PropertyValueUpdater
13
+ def initialize(dependency_files:)
14
+ @dependency_files = dependency_files
15
+ end
16
+
17
+ def update_pomfiles_for_property_change(property_name:, callsite_pom:,
18
+ updated_value:)
19
+ declaration_details = property_value_finder.property_details(
20
+ property_name: property_name,
21
+ callsite_pom: callsite_pom
22
+ )
23
+ node = declaration_details.fetch(:node)
24
+ filename = declaration_details.fetch(:file)
25
+
26
+ pom_to_update = dependency_files.find { |f| f.name == filename }
27
+ updated_content = pom_to_update.content.sub(
28
+ %r{<#{Regexp.quote(node.name)}>
29
+ \s*#{Regexp.quote(node.content)}\s*
30
+ </#{Regexp.quote(node.name)}>}xm,
31
+ "<#{node.name}>#{updated_value}</#{node.name}>"
32
+ )
33
+
34
+ updated_pomfiles = dependency_files.dup
35
+ updated_pomfiles[updated_pomfiles.index(pom_to_update)] =
36
+ update_file(file: pom_to_update, content: updated_content)
37
+
38
+ updated_pomfiles
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :dependency_files
44
+
45
+ def property_value_finder
46
+ @property_value_finder ||=
47
+ Maven::FileParser::PropertyValueFinder.
48
+ new(dependency_files: dependency_files)
49
+ end
50
+
51
+ def update_file(file:, content:)
52
+ updated_file = file.dup
53
+ updated_file.content = content
54
+ updated_file
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/file_updaters"
5
+ require "dependabot/file_updaters/base"
6
+
7
+ module Dependabot
8
+ module Maven
9
+ class FileUpdater < Dependabot::FileUpdaters::Base
10
+ require_relative "file_updater/declaration_finder"
11
+ require_relative "file_updater/property_value_updater"
12
+
13
+ def self.updated_files_regex
14
+ [/^pom\.xml$/, %r{/pom\.xml$}]
15
+ end
16
+
17
+ def updated_dependency_files
18
+ updated_files = dependency_files.dup
19
+
20
+ # Loop through each of the changed requirements, applying changes to
21
+ # all pomfiles for that change. Note that the logic is different here
22
+ # to other languages because Java has property inheritance across
23
+ # files
24
+ dependencies.each do |dependency|
25
+ updated_files = update_pomfiles_for_dependency(
26
+ pomfiles: updated_files,
27
+ dependency: dependency
28
+ )
29
+ end
30
+
31
+ updated_files.select! { |f| f.name.end_with?("pom.xml") }
32
+ updated_files.reject! { |f| original_pomfiles.include?(f) }
33
+
34
+ raise "No files changed!" if updated_files.none?
35
+ if updated_files.any? { |f| f.name.end_with?("pom_parent.xml") }
36
+ raise "Updated a supporting POM!"
37
+ end
38
+
39
+ updated_files
40
+ end
41
+
42
+ private
43
+
44
+ def check_required_files
45
+ raise "No pom.xml!" unless get_original_file("pom.xml")
46
+ end
47
+
48
+ def update_pomfiles_for_dependency(pomfiles:, dependency:)
49
+ files = pomfiles.dup
50
+
51
+ # The UpdateChecker ensures the order of requirements is preserved
52
+ # when updating, so we can zip them together in new/old pairs.
53
+ reqs = dependency.requirements.zip(dependency.previous_requirements).
54
+ reject { |new_req, old_req| new_req == old_req }
55
+
56
+ # Loop through each changed requirement and update the pomfiles
57
+ reqs.each do |new_req, old_req|
58
+ raise "Bad req match" unless new_req[:file] == old_req[:file]
59
+ next if new_req[:requirement] == old_req[:requirement]
60
+
61
+ if new_req.dig(:metadata, :property_name)
62
+ files = update_pomfiles_for_property_change(files, new_req)
63
+ pom = files.find { |f| f.name == new_req.fetch(:file) }
64
+ files[files.index(pom)] =
65
+ remove_property_suffix_in_pom(dependency, pom, old_req)
66
+ else
67
+ pom = files.find { |f| f.name == new_req.fetch(:file) }
68
+ files[files.index(pom)] =
69
+ update_version_in_pom(dependency, pom, old_req, new_req)
70
+ end
71
+ end
72
+
73
+ files
74
+ end
75
+
76
+ def update_pomfiles_for_property_change(pomfiles, req)
77
+ property_name = req.fetch(:metadata).fetch(:property_name)
78
+
79
+ PropertyValueUpdater.new(dependency_files: pomfiles).
80
+ update_pomfiles_for_property_change(
81
+ property_name: property_name,
82
+ callsite_pom: pomfiles.find { |f| f.name == req.fetch(:file) },
83
+ updated_value: req.fetch(:requirement)
84
+ )
85
+ end
86
+
87
+ def update_version_in_pom(dependency, pom, previous_req, requirement)
88
+ updated_content = pom.content
89
+
90
+ original_pom_declarations(dependency, previous_req).each do |old_dec|
91
+ updated_content = updated_content.gsub(
92
+ old_dec,
93
+ updated_pom_declaration(old_dec, previous_req, requirement)
94
+ )
95
+ end
96
+
97
+ raise "Expected content to change!" if updated_content == pom.content
98
+
99
+ updated_file(file: pom, content: updated_content)
100
+ end
101
+
102
+ def remove_property_suffix_in_pom(dep, pom, req)
103
+ updated_content = pom.content
104
+
105
+ original_pom_declarations(dep, req).each do |old_declaration|
106
+ updated_content = updated_content.gsub(old_declaration) do |old_dec|
107
+ version_string =
108
+ old_dec.match(%r{(?<=\<version\>).*(?=\</version\>)})
109
+ cleaned_version_string = version_string.to_s.gsub(/(?<=\}).*/, "")
110
+
111
+ old_dec.gsub(
112
+ "<version>#{version_string}</version>",
113
+ "<version>#{cleaned_version_string}</version>"
114
+ )
115
+ end
116
+ end
117
+
118
+ updated_file(file: pom, content: updated_content)
119
+ end
120
+
121
+ def original_pom_declarations(dependency, requirement)
122
+ declaration_finder(dependency, requirement).declaration_strings
123
+ end
124
+
125
+ # The declaration finder may need to make remote calls (to get parent
126
+ # POMs if it's searching for the value of a property), so we cache it.
127
+ def declaration_finder(dependency, requirement)
128
+ @declaration_finders ||= {}
129
+ @declaration_finders[dependency.hash + requirement.hash] ||=
130
+ begin
131
+ DeclarationFinder.new(
132
+ dependency: dependency,
133
+ declaring_requirement: requirement,
134
+ dependency_files: dependency_files
135
+ )
136
+ end
137
+ end
138
+
139
+ def updated_pom_declaration(old_declaration, previous_req, requirement)
140
+ original_req_string = previous_req.fetch(:requirement)
141
+
142
+ old_declaration.gsub(
143
+ %r{<version>\s*#{Regexp.quote(original_req_string)}\s*</version>},
144
+ "<version>#{requirement.fetch(:requirement)}</version>"
145
+ )
146
+ end
147
+
148
+ def original_pomfiles
149
+ @original_pomfiles ||=
150
+ dependency_files.select { |f| f.name.end_with?("pom.xml") }
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ Dependabot::FileUpdaters.register("maven", Dependabot::Maven::FileUpdater)