@@ -24,6 +24,12 @@ class CircularReferenceError(ValueError):
2424 pass
2525
2626
27+ class PlaceholderSyntaxError (ValueError ):
28+ """Exception raised when a placeholder has invalid syntax."""
29+
30+ pass
31+
32+
2733class PlaceholderResolver :
2834 """Handles placeholder resolution in configuration data."""
2935
@@ -86,17 +92,43 @@ def _resolve_placeholder(self, match: re.Match) -> str:
8692 f"key '{ placeholder } ' not found in current section." ,
8793 )
8894
95+ def validate_brace_syntax (self , text : str , original_text : str ) -> None :
96+ """Validate that curly braces are balanced and properly ordered."""
97+ brace_level = 0
98+ for char in text :
99+ if char == "{" :
100+ brace_level += 1
101+ elif char == "}" :
102+ brace_level -= 1
103+
104+ # If it ever drops below 0, we've hit a '}' before a '{'
105+ if brace_level < 0 :
106+ break
107+
108+ # If the level isn't exactly 0 at the end, the syntax is broken
109+ if brace_level != 0 :
110+ container_name = self ._get_container_name ()
111+
112+ if "{" in text and "}" not in text :
113+ msg = f"Missing end curly brace in placeholder in value: '{ original_text } '"
114+ elif "}" in text and "{" not in text :
115+ msg = f"Missing start curly brace in placeholder in value: '{ original_text } '"
116+ else :
117+ msg = f"Invalid placeholder syntax in value: '{ original_text } '"
118+
119+ raise PlaceholderSyntaxError (f"In configuration key '{ container_name } ': { msg } " )
120+
89121 def _resolve_string_placeholders (self , text : str ) -> str :
90122 """Resolve all placeholders in a string with recursion protection."""
123+ original_text = text
91124 count = 0
92125 cls = self .__class__
126+
127+ # 1. Resolve valid placeholders
93128 while count < self .max_recursion_depth :
94129 new_text = cls .PLACEHOLDER_PATTERN .sub (self ._resolve_placeholder , text )
95-
96130 if new_text == text :
97- # No more changes, we're done
98131 break
99-
100132 text = new_text
101133 count += 1
102134
@@ -106,7 +138,10 @@ def _resolve_string_placeholders(self, text: str) -> str:
106138 f"Too many nested placeholder expansions in key '{ key_name } '."
107139 )
108140
109- # Replace escaped braces with literal ones
141+ # 2. Syntax Validation using the helper method
142+ self .validate_brace_syntax (text , original_text )
143+
144+ # 3. Final cleanup of escapes
110145 return text .replace ("{{" , "{" ).replace ("}}" , "}" )
111146
112147 def _get_container_name (self ) -> str :
@@ -134,6 +169,7 @@ def replace(self) -> dict[str, Any]:
134169 :return: The configuration with all placeholders resolved.
135170 :raises PlaceholderResolutionError: If a placeholder cannot be resolved.
136171 :raises CircularReferenceError: If a circular reference is detected.
172+ :raises PlaceholderSyntaxError: If a placeholder has invalid syntax.
137173 """
138174 # Use a stack to process all items iteratively
139175 # Stack items: (container, key, context) where container can be dict or list
@@ -183,6 +219,7 @@ def replace_placeholders(
183219 :return: A new dictionary with placeholders replaced.
184220 :raises PlaceholderResolutionError: If a placeholder cannot be resolved.
185221 :raises CircularReferenceError: If a circular reference is detected.
222+ :raises PlaceholderSyntaxError: If a placeholder has invalid syntax.
186223 """
187224 if not isinstance (config , dict ):
188225 return config
0 commit comments