3939from typing import Any , AnyStr , Mapping , NoReturn , Optional , Sequence , Callable , Type , Union
4040
4141from jsonata import datetimeutils , jexception , parser , utils
42+ from jsonata .regex_engine import CompiledPattern
4243
4344
4445class Functions :
@@ -514,7 +515,7 @@ class RegexpMatch:
514515 # @returns {object} - structure that represents the match(es)
515516 #
516517 @staticmethod
517- def evaluate_matcher (matcher : re . Pattern , string : Optional [str ]) -> list [RegexpMatch ]:
518+ def evaluate_matcher (matcher : CompiledPattern , string : Optional [str ]) -> list [RegexpMatch ]:
518519 res = []
519520 matches = matcher .finditer (string )
520521 for m in matches :
@@ -537,7 +538,7 @@ def evaluate_matcher(matcher: re.Pattern, string: Optional[str]) -> list[RegexpM
537538 # @returns {Boolean} - true if str contains token
538539 #
539540 @staticmethod
540- def contains (string : Optional [str ], token : Union [None , str , re . Pattern ]) -> Optional [bool ]:
541+ def contains (string : Optional [str ], token : Union [None , str , CompiledPattern ]) -> Optional [bool ]:
541542 # undefined inputs always return undefined
542543 if string is None :
543544 return None
@@ -549,7 +550,7 @@ def contains(string: Optional[str], token: Union[None, str, re.Pattern]) -> Opti
549550
550551 if isinstance (token , str ):
551552 result = (string .find (str (token )) != - 1 )
552- elif isinstance (token , re . Pattern ):
553+ elif Functions . is_regex (token ):
553554 matches = Functions .evaluate_matcher (token , string )
554555 # if (dbg) System.out.println("match = "+matches)
555556 # result = (typeof matches !== 'undefined')
@@ -568,7 +569,7 @@ def contains(string: Optional[str], token: Union[None, str, re.Pattern]) -> Opti
568569 # @returns {Array} The array of match objects
569570 #
570571 @staticmethod
571- def match_ (string : Optional [str ], regex : Optional [re . Pattern ], limit : Optional [int ]) -> Optional [list [dict [str , Any ]]]:
572+ def match_ (string : Optional [str ], regex : Optional [CompiledPattern ], limit : Optional [int ]) -> Optional [list [dict [str , Any ]]]:
572573 # undefined inputs always return undefined
573574 if string is None :
574575 return None
@@ -636,7 +637,7 @@ def safe_replacement(in_: str) -> str:
636637 # @return
637638 #
638639 @staticmethod
639- def safe_replace_all (s : str , pattern : re . Pattern , replacement : Optional [Any ]) -> Optional [str ]:
640+ def safe_replace_all (s : str , pattern : CompiledPattern , replacement : Optional [Any ]) -> Optional [str ]:
640641
641642 if not (isinstance (replacement , str )):
642643 return Functions .safe_replace_all_fn (s , pattern , replacement )
@@ -647,7 +648,7 @@ def safe_replace_all(s: str, pattern: re.Pattern, replacement: Optional[Any]) ->
647648 r = None
648649 for i in range (0 , 10 ):
649650 try :
650- r = re .sub (pattern , replacement , s )
651+ r = pattern .sub (replacement , s )
651652 break
652653 except Exception as e :
653654 msg = str (e )
@@ -696,15 +697,15 @@ def to_jsonata_match(mr: re.Match[str]) -> dict[str, list[str]]:
696697 # @return
697698 #
698699 @staticmethod
699- def safe_replace_all_fn (s : str , pattern : re . Pattern , fn : Optional [Any ]) -> str :
700+ def safe_replace_all_fn (s : str , pattern : CompiledPattern , fn : Optional [Any ]) -> str :
700701 def replace_fn (t ):
701702 res = Functions .func_apply (fn , [Functions .to_jsonata_match (t )])
702703 if isinstance (res , str ):
703704 return res
704705 else :
705706 raise jexception .JException ("D3012" , - 1 )
706707
707- r = re .sub (pattern , replace_fn , s )
708+ r = pattern .sub (replace_fn , s )
708709 return r
709710
710711 #
@@ -716,12 +717,12 @@ def replace_fn(t):
716717 # @return
717718 #
718719 @staticmethod
719- def safe_replace_first (s : str , pattern : re . Pattern , replacement : str ) -> Optional [str ]:
720+ def safe_replace_first (s : str , pattern : CompiledPattern , replacement : str ) -> Optional [str ]:
720721 replacement = Functions .safe_replacement (replacement )
721722 r = None
722723 for i in range (0 , 10 ):
723724 try :
724- r = re .sub (pattern , replacement , s , count = 1 )
725+ r = pattern .sub (replacement , s , 1 )
725726 break
726727 except Exception as e :
727728 msg = str (e )
@@ -744,7 +745,7 @@ def safe_replace_first(s: str, pattern: re.Pattern, replacement: str) -> Optiona
744745 return r
745746
746747 @staticmethod
747- def replace (string : Optional [str ], pattern : Union [str , re . Pattern ], replacement : Optional [Any ], limit : Optional [int ]) -> Optional [str ]:
748+ def replace (string : Optional [str ], pattern : Union [str , CompiledPattern ], replacement : Optional [Any ], limit : Optional [int ]) -> Optional [str ]:
748749 if string is None :
749750 return None
750751
@@ -938,7 +939,7 @@ def decode_url(string: Optional[str]) -> Optional[str]:
938939 return urllib .parse .unquote (string , errors = "strict" )
939940
940941 @staticmethod
941- def split (string : Optional [str ], pattern : Union [str , Optional [re . Pattern ]], limit : Optional [float ]) -> Optional [list [str ]]:
942+ def split (string : Optional [str ], pattern : Union [str , Optional [CompiledPattern ]], limit : Optional [float ]) -> Optional [list [str ]]:
942943 if string is None :
943944 return None
944945
@@ -2019,6 +2020,23 @@ def append(arg1: Optional[Any], arg2: Optional[Any]) -> Optional[Any]:
20192020 def is_lambda (result : Optional [Any ]) -> bool :
20202021 return isinstance (result , parser .Parser .Symbol ) and result ._jsonata_lambda
20212022
2023+ #
2024+ # Tests whether a value is a compiled regex, from the stdlib re module
2025+ # or from a pluggable regex_engine (e.g. re2) with a compatible interface.
2026+ #
2027+ @staticmethod
2028+ def is_regex (value : Optional [Any ]) -> bool :
2029+ if isinstance (value , re .Pattern ):
2030+ return True
2031+ if value is None or inspect .ismodule (value ) or inspect .isclass (value ):
2032+ return False
2033+ return (
2034+ callable (getattr (value , "search" , None ))
2035+ and callable (getattr (value , "finditer" , None ))
2036+ and callable (getattr (value , "sub" , None ))
2037+ and callable (getattr (value , "split" , None ))
2038+ )
2039+
20222040 #
20232041 # Return value from an object for a given key
20242042 # @param {Object} input - Object/Array
@@ -2191,7 +2209,15 @@ def function_eval(expr: Optional[str], focus: Optional[Any]) -> Optional[Any]:
21912209 # undefined inputs always return undefined
21922210 if expr is None :
21932211 return None
2194- input = jsonata .Jsonata .CURRENT .jsonata .input # = this.input;
2212+
2213+ # Capture the enclosing instance *before* constructing the nested
2214+ # ast below: Jsonata.__init__ unconditionally overwrites
2215+ # Jsonata.CURRENT.jsonata as a side effect, so re-reading
2216+ # CURRENT.jsonata afterwards would silently return the inner
2217+ # expression instead of the enclosing one, losing access to its
2218+ # environment (e.g. outer variable bindings).
2219+ enclosing = jsonata .Jsonata .CURRENT .jsonata
2220+ input = enclosing .input # = this.input;
21952221 if focus is not None :
21962222 input = focus
21972223 # if the input is a JSON array, then wrap it in a singleton sequence so it gets treated as a single input
@@ -2201,18 +2227,47 @@ def function_eval(expr: Optional[str], focus: Optional[Any]) -> Optional[Any]:
22012227
22022228 ast = None
22032229 try :
2204- ast = jsonata .Jsonata (expr )
2205- except Exception as err :
2206- # error parsing the expression passed to $eval
2207- # populateMessage(err)
2208- raise jexception .JException ("D3120" , - 1 )
2209- result = None
2210- try :
2211- result = ast .evaluate (input , jsonata .Jsonata .CURRENT .jsonata .environment )
2212- except Exception as err :
2213- # error evaluating the expression passed to $eval
2214- # populateMessage(err)
2215- raise jexception .JException ("D3121" , - 1 )
2230+ try :
2231+ # Only used to parse expr into an AST (ast.ast below); the
2232+ # actual evaluation reuses the enclosing instance directly
2233+ # (see below), so regex_engine here just needs to be valid
2234+ # for parsing -- it does not need to be evaluated against.
2235+ ast = jsonata .Jsonata (expr , enclosing .regex_engine )
2236+ except Exception as err :
2237+ # error parsing the expression passed to $eval
2238+ # populateMessage(err)
2239+ raise jexception .JException ("D3120" , - 1 )
2240+
2241+ # Constructing `ast` overwrote Jsonata.CURRENT.jsonata as a side
2242+ # effect (see Jsonata.__init__); restore it to the enclosing
2243+ # instance before evaluating, so enclosing.eval()'s own
2244+ # get_per_thread_instance() lookup resolves correctly.
2245+ jsonata .Jsonata .CURRENT .jsonata = enclosing
2246+
2247+ result = None
2248+ try :
2249+ # Evaluate ast.ast (the parsed tree) using the *enclosing*
2250+ # instance's low-level eval(), reusing enclosing.environment
2251+ # directly rather than calling ast.evaluate(input, environment)
2252+ # (which copies environment's bindings one level deep into a
2253+ # fresh child frame rooted at ast's own static frame). This
2254+ # mirrors jsonata-java's Functions.functionEval, which calls
2255+ # Jsonata.current.get().evaluate(ast.ast, input, env) rather
2256+ # than constructing a second, disconnected evaluation
2257+ # context. Reusing the environment directly means $eval sees
2258+ # bindings at every level of the enclosing scope chain (not
2259+ # just the immediate frame) and correctly inherits any
2260+ # stack/timeout guardrails registered on an ancestor frame,
2261+ # since Frame.lookup walks the parent chain.
2262+ result = enclosing .eval (ast .ast , input , enclosing .environment )
2263+ except Exception as err :
2264+ # error evaluating the expression passed to $eval
2265+ # populateMessage(err)
2266+ raise jexception .JException ("D3121" , - 1 )
2267+ finally :
2268+ # Restore the enclosing instance as current now that $eval's
2269+ # parsing/evaluation is done.
2270+ jsonata .Jsonata .CURRENT .jsonata = enclosing
22162271
22172272 return result
22182273
0 commit comments