Skip to content

Commit e42be25

Browse files
Merge branch 'main' into harry/pnpm-trust-downgrade-version-fallback
2 parents 9fd5cf7 + c511045 commit e42be25

File tree

3 files changed

+121
-31
lines changed

3 files changed

+121
-31
lines changed

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

Lines changed: 57 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,73 @@ 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
77+
)
78+
end
79+
# If the property can’t be resolved for any reason, we return nil which
80+
# causes Dependabot to skip the dependency.
81+
# This differs from Maven’s behavior, where an unresolved property would fail the entire build.
82+
# We intentionally choose this as a compromise so Dependabot can continue parsing the rest of the project,
83+
# rather than failing completely due to a single unknown property.
84+
# The trade-off is that some dependencies may not be updated as expected.
85+
Dependabot.logger.warn "Could not resolve property '#{property_name}'" unless node
86+
return nil unless node
87+
88+
content = node.content.strip
89+
90+
# Detect infinite recursion such as ${property1} where property1=${property1}
91+
if seen_properties.include?(property_name)
92+
raise Dependabot::DependencyFileNotParseable.new(
93+
callsite_pom.name,
94+
"Error trying to resolve recursive expression '${#{property_name}}'."
7295
)
7396
end
7497

75-
# If we found a property, return it
76-
return { file: pom.name, node: node, value: node.content.strip } if node
98+
seen_properties << property_name
7799

78-
# Otherwise, look for a value in this pom's parent
79-
return unless (parent = parent_pom(pom))
100+
# If the content has no placeholders, return it as-is
101+
return { file: pom.name, node: node, value: content } unless content.match?(MAVEN_PROPERTY_REGEX)
80102

81-
property_details(
82-
property_name: property_name,
83-
callsite_pom: parent
84-
)
103+
resolve_property_placeholder(content, callsite_pom, pom, node, seen_properties)
85104
end
105+
# rubocop:enable Metrics/PerceivedComplexity
86106

87107
private
88108

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

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)
133+
{ file: pom.name, node: node, value: resolved_value }
111134
end
112135

136+
sig { returns(T::Array[DependencyFile]) }
137+
attr_reader :dependency_files
138+
113139
sig { params(property_name: String).returns(String) }
114140
def sanitize_property_name(property_name)
115141
property_name.sub(/^pom\./, "").sub(/^project\./, "")

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

Lines changed: 33 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" }
@@ -76,6 +90,25 @@
7690
end
7791
end
7892

93+
context "when the property cannot be resolved" do
94+
subject(:property_result) { property_details }
95+
96+
let(:base_pom_fixture_name) { "property_pom_duplicate_tags.xml" }
97+
let(:property_name) { "property.that.does.not.exist" }
98+
99+
it "returns nil" do
100+
expect(property_result).to be_nil
101+
end
102+
103+
it "logs a warning" do
104+
expect(Dependabot.logger)
105+
.to receive(:warn)
106+
.with("Could not resolve property '#{property_name}'")
107+
108+
property_result
109+
end
110+
end
111+
79112
context "when the latest tag is pointing to another tag, then get the value of that tag" do
80113
let(:base_pom_fixture_name) { "property_pom_duplicate_tags.xml" }
81114
let(:property_name) { "orika.version" }
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
This POM verifies that properties can reference other properties, even across multiple levels.
4+
Some complex projects (e.g., WildFly) rely on this pattern to dynamically manage project variants.
5+
For more context, see https://github.com/dependabot/dependabot-core/issues/13713
6+
-->
7+
<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">
8+
<modelVersion>4.0.0</modelVersion>
9+
10+
<groupId>org.reproducer</groupId>
11+
<artifactId>test</artifactId>
12+
<version>1.0.0-SNAPSHOT</version>
13+
14+
<properties>
15+
<channels>channels2</channels>
16+
<channels.maven.groupId.nested>${project.groupId}.${channels}</channels.maven.groupId.nested>
17+
<channels.maven.groupId>${project.groupId}.channels</channels.maven.groupId>
18+
</properties>
19+
20+
<dependencyManagement>
21+
<dependencies>
22+
23+
<dependency>
24+
<groupId>${channels.maven.groupId}</groupId>
25+
<artifactId>wildfly-preview</artifactId>
26+
<version>19.0.0.Final</version>
27+
</dependency>
28+
29+
</dependencies>
30+
</dependencyManagement>
31+
</project>

0 commit comments

Comments
 (0)