Skip to content

Commit 5ce5175

Browse files
committed
Add support for nested maven properties
Extends the Maven parsing logic to support nested properties Fixes #13713
1 parent 4ea40bf commit 5ce5175

File tree

3 files changed

+93
-31
lines changed

3 files changed

+93
-31
lines changed

maven/lib/dependabot/maven/file_parser/property_value_finder.rb

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class PropertyValueFinder
2020
require_relative "pom_fetcher"
2121

2222
DOT_SEPARATOR_REGEX = %r{\.(?!\d+([.\/_\-]|$)+)}
23+
# Matches ${...}
24+
MAVEN_PROPERTY_REGEX = /\$\{.+?/
2325

2426
sig do
2527
params(
@@ -37,10 +39,15 @@ def initialize(dependency_files:, credentials: [])
3739
)
3840
end
3941

42+
# rubocop:disable Metrics/PerceivedComplexity
4043
sig do
41-
params(property_name: String, callsite_pom: DependencyFile).returns(T.nilable(T::Hash[Symbol, T.untyped]))
44+
params(
45+
property_name: String,
46+
callsite_pom: DependencyFile,
47+
seen_properties: T::Set[String]
48+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
4249
end
43-
def property_details(property_name:, callsite_pom:)
50+
def property_details(property_name:, callsite_pom:, seen_properties: Set.new)
4451
pom = callsite_pom
4552
doc = Nokogiri::XML(pom.content)
4653
doc.remove_namespaces!
@@ -63,53 +70,64 @@ def property_details(property_name:, callsite_pom:)
6370
raise DependencyFileNotEvaluatable, e.message
6471
end
6572

66-
# and value is an expression
67-
if node && /\$\{(?<expression>.+)\}/.match(node.content.strip)
68-
return extract_value_from_expression(
69-
expression: node.content.strip,
73+
if node.nil? && parent_pom(pom)
74+
return property_details(
7075
property_name: property_name,
71-
callsite_pom: callsite_pom
76+
callsite_pom: T.must(parent_pom(pom)),
77+
seen_properties: seen_properties
7278
)
7379
end
80+
return nil unless node
7481

75-
# If we found a property, return it
76-
return { file: pom.name, node: node, value: node.content.strip } if node
82+
content = node.content.strip
7783

78-
# Otherwise, look for a value in this pom's parent
79-
return unless (parent = parent_pom(pom))
84+
# Detect infinite recursion such as ${property1} where property1=${property1}
85+
if seen_properties.include?(property_name)
86+
raise Dependabot::DependencyFileNotParseable.new(
87+
callsite_pom.name,
88+
"Error trying to resolve recursive expression '${#{property_name}}'."
89+
)
90+
end
8091

81-
property_details(
82-
property_name: property_name,
83-
callsite_pom: parent
84-
)
92+
seen_properties << property_name
93+
94+
# If the content has no placeholders, return it as-is
95+
return { file: pom.name, node: node, value: content } unless content.match?(MAVEN_PROPERTY_REGEX)
96+
97+
resolve_property_placeholder(content, callsite_pom, pom, node, seen_properties)
8598
end
99+
# rubocop:enable Metrics/PerceivedComplexity
86100

87101
private
88102

89-
sig { returns(T::Array[DependencyFile]) }
90-
attr_reader :dependency_files
91-
103+
# Extract property placeholders from a string and resolve them
104+
# See https://maven.apache.org/pom.html#properties
92105
sig do
93106
params(
94-
expression: String,
95-
property_name: String,
96-
callsite_pom: DependencyFile
97-
)
98-
.returns(T.nilable(T::Hash[Symbol, String]))
107+
content: String,
108+
callsite_pom: DependencyFile,
109+
pom: DependencyFile,
110+
node: T.untyped,
111+
seen_properties: T::Set[String]
112+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
99113
end
100-
def extract_value_from_expression(expression:, property_name:, callsite_pom:)
101-
# and the expression is pointing to self then raise the error
102-
if expression.eql?("${#{property_name}}")
103-
raise Dependabot::DependencyFileNotParseable.new(
104-
callsite_pom.name,
105-
"Error trying to resolve recursive expression '#{expression}'."
114+
def resolve_property_placeholder(content, callsite_pom, pom, node, seen_properties)
115+
resolved_value = content.gsub(/\$\{(.+?)}/) do
116+
inner_name = Regexp.last_match(1)
117+
resolved = property_details(
118+
property_name: T.must(inner_name),
119+
callsite_pom: callsite_pom,
120+
seen_properties: seen_properties
106121
)
122+
T.must(resolved)[:value]
107123
end
108124

109-
# and the expression is pointing to another tag, then get the value of that tag
110-
property_details(property_name: T.must(expression.slice(2..-2)), callsite_pom: callsite_pom)
125+
{ file: pom.name, node: node, value: resolved_value }
111126
end
112127

128+
sig { returns(T::Array[DependencyFile]) }
129+
attr_reader :dependency_files
130+
113131
sig { params(property_name: String).returns(String) }
114132
def sanitize_property_name(property_name)
115133
property_name.sub(/^pom\./, "").sub(/^project\./, "")

maven/spec/dependabot/maven/file_parser/property_value_finder_spec.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@
3939
its([:value]) { is_expected.to eq("0.0.2-RELEASE") }
4040
end
4141

42+
context "when the property is an nested property using project definition properties" do
43+
let(:base_pom_fixture_name) { "pom_with_nested_properties.xml" }
44+
let(:property_name) { "channels.maven.groupId" }
45+
46+
its([:value]) { is_expected.to eq("org.reproducer.channels") }
47+
end
48+
49+
context "when the property is nested multiple times" do
50+
let(:base_pom_fixture_name) { "pom_with_nested_properties.xml" }
51+
let(:property_name) { "channels.maven.groupId.nested" }
52+
53+
its([:value]) { is_expected.to eq("org.reproducer.channels2") }
54+
end
55+
4256
context "when the property name starts with 'project' but not an attribute of the project" do
4357
let(:base_pom_fixture_name) { "property_name_starts_with_project_pom.xml" }
4458
let(:property_name) { "project.dependency.spring-boot.version" }
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!-- This pom tests that properties can reference other properties that reference other properties
3+
This is used in some complex projects like WildFly to manage variants of their projects dynamically
4+
For more context see https://github.com/dependabot/dependabot-core/issues/13713
5+
-->
6+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
7+
<modelVersion>4.0.0</modelVersion>
8+
9+
<groupId>org.reproducer</groupId>
10+
<artifactId>test</artifactId>
11+
<version>1.0.0-SNAPSHOT</version>
12+
13+
<properties>
14+
<channels>channels2</channels>
15+
<channels.maven.groupId.nested>${project.groupId}.${channels}</channels.maven.groupId.nested>
16+
<channels.maven.groupId>${project.groupId}.channels</channels.maven.groupId>
17+
</properties>
18+
19+
<dependencyManagement>
20+
<dependencies>
21+
22+
<dependency>
23+
<groupId>${channels.maven.groupId}</groupId>
24+
<artifactId>wildfly-preview</artifactId>
25+
<version>19.0.0.Final</version>
26+
</dependency>
27+
28+
</dependencies>
29+
</dependencyManagement>
30+
</project>

0 commit comments

Comments
 (0)