@@ -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,66 @@ 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+ # These properties can be simple properties such as ${project.version}
105+ # or more complex such as ${my.property.${other.property}} or constant.${property}
106+ # See https://maven.apache.org/pom.html#properties
92107 sig do
93108 params (
94- expression : String ,
95- property_name : String ,
96- callsite_pom : DependencyFile
97- )
98- . returns ( T . nilable ( T ::Hash [ Symbol , String ] ) )
109+ content : String ,
110+ callsite_pom : DependencyFile ,
111+ pom : DependencyFile ,
112+ node : T . untyped ,
113+ seen_properties : T ::Set [ String ]
114+ ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) )
99115 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 } '."
116+ def resolve_property_placeholder ( content , callsite_pom , pom , node , seen_properties )
117+ resolved_value = content . gsub ( /\$ \{ (.+?)}/ ) do
118+ inner_name = Regexp . last_match ( 1 )
119+ resolved = property_details (
120+ property_name : T . must ( inner_name ) ,
121+ callsite_pom : callsite_pom ,
122+ seen_properties : seen_properties
106123 )
124+ T . must ( resolved ) [ :value ]
107125 end
108126
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 )
127+ { file : pom . name , node : node , value : resolved_value }
111128 end
112129
130+ sig { returns ( T ::Array [ DependencyFile ] ) }
131+ attr_reader :dependency_files
132+
113133 sig { params ( property_name : String ) . returns ( String ) }
114134 def sanitize_property_name ( property_name )
115135 property_name . sub ( /^pom\. / , "" ) . sub ( /^project\. / , "" )
0 commit comments