@@ -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\. / , "" )
0 commit comments