Skip to content

Commit 488b043

Browse files
committed
Add support for nested maven properties
Extends the Maven parsing logic to support nested properties Fixes #13713
1 parent 5f5d8b9 commit 488b043

File tree

3 files changed

+94
-31
lines changed

3 files changed

+94
-31
lines changed

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

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

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

2425
sig do
2526
params(
@@ -37,10 +38,15 @@ def initialize(dependency_files:, credentials: [])
3738
)
3839
end
3940

41+
# rubocop:disable Metrics/PerceivedComplexity
4042
sig do
41-
params(property_name: String, callsite_pom: DependencyFile).returns(T.nilable(T::Hash[Symbol, T.untyped]))
43+
params(
44+
property_name: String,
45+
callsite_pom: DependencyFile,
46+
seen_properties: T::Set[String]
47+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
4248
end
43-
def property_details(property_name:, callsite_pom:)
49+
def property_details(property_name:, callsite_pom:, seen_properties: Set.new)
4450
pom = callsite_pom
4551
doc = Nokogiri::XML(pom.content)
4652
doc.remove_namespaces!
@@ -63,53 +69,66 @@ def property_details(property_name:, callsite_pom:)
6369
raise DependencyFileNotEvaluatable, e.message
6470
end
6571

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,
72+
if node.nil? && parent_pom(pom)
73+
return property_details(
7074
property_name: property_name,
71-
callsite_pom: callsite_pom
75+
callsite_pom: T.must(parent_pom(pom)),
76+
seen_properties: seen_properties
7277
)
7378
end
79+
return nil unless node
7480

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

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

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

87100
private
88101

89-
sig { returns(T::Array[DependencyFile]) }
90-
attr_reader :dependency_files
91-
102+
# Extract property placeholders from a string and resolve them
103+
# These properties can be simple properties such as ${project.version}
104+
# or more complex such as ${my.property.${other.property}} or constant.${property}
105+
# See https://maven.apache.org/pom.html#properties
92106
sig do
93107
params(
94-
expression: String,
95-
property_name: String,
96-
callsite_pom: DependencyFile
97-
)
98-
.returns(T.nilable(T::Hash[Symbol, String]))
108+
content: String,
109+
callsite_pom: DependencyFile,
110+
pom: DependencyFile,
111+
node: T.untyped,
112+
seen_properties: T::Set[String]
113+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
99114
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}'."
115+
def resolve_property_placeholder(content, callsite_pom, pom, node, seen_properties)
116+
resolved_value = content.gsub(/\$\{(.+?)}/) do
117+
inner_name = Regexp.last_match(1)
118+
resolved = property_details(
119+
property_name: T.must(inner_name),
120+
callsite_pom: callsite_pom,
121+
seen_properties: seen_properties
106122
)
123+
T.must(resolved)[:value]
107124
end
108125

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)
126+
{ file: pom.name, node: node, value: resolved_value }
111127
end
112128

129+
sig { returns(T::Array[DependencyFile]) }
130+
attr_reader :dependency_files
131+
113132
sig { params(property_name: String).returns(String) }
114133
def sanitize_property_name(property_name)
115134
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)