1- """
2- Lightweight expression parser to extract data influencing
3- variables from flow expressions.
1+ """Lightweight expression parser to extract data influencing variables from flow expressions.
2+
3+ This module provides functions for parsing Salesforce Flow expressions and formulas,
4+ extracting variable references and determining data flow dependencies.
45
5667"""
3839
3940
4041def _has_skips (function_name : str ) -> bool :
41- """
42- Determine whether this is a supported function that may skip propagation
42+ """Determine whether this is a supported function that may skip propagation.
43+
4344 Args:
44- function_name:
45+ function_name: Name of the function to check.
4546
46- Returns: True if supported function
47+ Returns:
48+ True if the function is supported and may skip propagation.
4749 """
4850 msg = function_name .upper ()
4951
@@ -63,13 +65,13 @@ def _should_propagate_by_arg(function_name: str | None, arg_num: int, last_arg:
6365 """Determine transfer policy for arguments passed to functions.
6466
6567 Args:
66- function_name: name of function (must be uppercase)
67- arg_num: position of argument (starting at 1)
68- last_arg: True if this is the last argument
68+ function_name: Name of function (must be uppercase).
69+ arg_num: Position of argument (starting at 1).
70+ last_arg: True if this is the last argument.
6971
7072 Returns:
7173 True if argument is propagated, False otherwise.
72- Note that for unrecognized functions we default to True
74+ Note that for unrecognized functions we default to True.
7375 """
7476 # TODO: should we be stricter and raise an argument error if too many arguments are passed?
7577 if function_name is None :
@@ -89,7 +91,7 @@ def _should_propagate_by_arg(function_name: str | None, arg_num: int, last_arg:
8991 # must have at least 1 CASE
9092 if arg_num == 1 :
9193 return False
92- if last_arg is True :
94+ if last_arg :
9395 return True
9496 if arg_num % 2 == 0 :
9597 return False
@@ -118,29 +120,38 @@ def _should_propagate_by_arg(function_name: str | None, arg_num: int, last_arg:
118120
119121# parse_utils.parse_expression(txt)
120122def _strip_quoted (msg : str ) -> str :
121- """
122- Replaces quoted values with empty strings
123- Args:
124- msg: string to be processed
123+ """Replace quoted values with empty strings.
125124
126- Returns: message where all quoted strings are empty
125+ Args:
126+ msg: String to be processed.
127127
128+ Returns:
129+ Message where all quoted strings are replaced with empty strings.
128130 """
129131 no_doubles = re .sub (double_re , '""' , msg )
130132 return re .sub (single_re , '\' \' ' , no_doubles )
131133
132134
133135def _strip_whitespace (msg : str ) -> str :
136+ """Remove all whitespace from a string.
137+
138+ Args:
139+ msg: String to process.
140+
141+ Returns:
142+ String with all whitespace removed.
143+ """
134144 return re .sub (r'\s+' , '' , msg )
135145
136146
137147def extract_expression (txt : str ) -> list [str ]:
138- """
148+ """Extract variable references from an expression using regex.
149+
139150 Args:
140- txt: expression in which merge-fields may be present
151+ txt: Expression in which merge-fields may be present.
141152
142153 Returns:
143- List of elementRef names (empty list if no matches)
154+ List of elementRef names (empty list if no matches).
144155 """
145156 accum = []
146157 res = re .finditer (reg , txt )
@@ -152,14 +163,15 @@ def extract_expression(txt: str) -> list[str]:
152163
153164
154165def parse_expression (expression : str ) -> list [str ]:
155- """Main entry point for parsing expressions. Do not use this on templates
156- in which expressions are mixed with text or html.
166+ """Main entry point for parsing expressions.
167+
168+ Do not use this on templates in which expressions are mixed with text or HTML.
157169
158170 Args:
159- expression: expression to be evaluated.
171+ expression: Expression to be evaluated.
160172
161173 Returns:
162- list of variables that data influence the expression
174+ List of variables that data influence the expression.
163175 """
164176 # TODO: might as well extract variables directly here and save the grep
165177 try :
@@ -171,13 +183,13 @@ def parse_expression(expression: str) -> list[str]:
171183
172184
173185def process_expression (expression : str ) -> list [str ]:
174- """Process expression to return list of data influencing variables
186+ """Process expression to return list of data influencing variables.
175187
176188 Args:
177- expression: expression to be processed
189+ expression: Expression to be processed.
178190
179191 Returns:
180- list of variable names that data influence the expression
192+ List of variable names that data influence the expression.
181193 """
182194 expr = _strip_whitespace (expression )
183195 # Handle degenerate cases
@@ -213,12 +225,13 @@ def process_expression(expression: str) -> list[str]:
213225
214226
215227def _extract_results_from_context (context : Context ) -> list [str ]:
216- """returns list of variables names from context
228+ """Extract list of variable names from context.
217229
218230 Args:
219- context:
231+ context: Parsing context containing processed arguments.
220232
221- Returns: list of variable names (de-duped)
233+ Returns:
234+ List of variable names (de-duplicated).
222235 """
223236 res_list = util .safe_list_add (context .prev_arguments_text_array ,
224237 context .current_argument_text_array )
@@ -231,14 +244,14 @@ def _extract_results_from_context(context: Context) -> list[str]:
231244
232245
233246def _update_parent_context (parent_ctx : Context , child_ctx : Context ) -> Context :
234- """Updates the parent context after child context has finished processing
247+ """Update the parent context after child context has finished processing.
235248
236249 Args:
237- parent_ctx: parent context
238- child_ctx: child context
239-
240- Returns: parent_ctx
250+ parent_ctx: Parent context to update.
251+ child_ctx: Child context that has finished processing.
241252
253+ Returns:
254+ Updated parent context.
242255 """
243256 # Add the processed segments to the parent
244257 parent_ctx .current_argument_text_array = util .safe_list_add (
@@ -260,16 +273,18 @@ def _update_parent_context(parent_ctx: Context, child_ctx: Context) -> Context:
260273
261274
262275def _parse_function (ctx : Context ) -> Context | None :
263- """Enter this function after the first parenthesis
264- and call with function name and skip policy in context
276+ """Parse a function call within an expression.
277+
278+ Enter this function after the first parenthesis
279+ and call with function name and skip policy in context.
265280
266281 Args:
267- ctx: function parsing context
282+ ctx: Function parsing context.
268283
269284 Returns:
270- None if the entire function has completed processing
285+ None if the entire function has completed processing,
271286 or a new context if processing was interrupted with a function call
272- in which case it resumes in current position
287+ in which case it resumes in current position.
273288 """
274289 if ctx .current_position + 1 == len (ctx .expression ):
275290 # we are done processing, so collect arguments
@@ -294,7 +309,7 @@ def _parse_function(ctx: Context) -> Context | None:
294309 if i > 0 :
295310 ctx .current_position += 1
296311
297- if empty_call is True :
312+ if empty_call :
298313 # We're in a FOO() situation and want to skip over it
299314 empty_call = False
300315 continue
@@ -340,20 +355,20 @@ def _parse_function(ctx: Context) -> Context | None:
340355 return None
341356 else :
342357 continue
358+ return None
343359
344360 # we've finished processing this function
345361
346362
347363def _handle_argument_end (ctx : Context , is_comma = True ) -> Context :
348- """Decides whether to flush or add to processed buffers the current
349- portion of the argument being scanned.
364+ """Decide whether to flush or add to processed buffers the current argument.
350365
351366 Args:
352- ctx: current context
353- is_comma: True if comma, False if parenthesis
367+ ctx: Current parsing context.
368+ is_comma: True if comma separator , False if closing parenthesis.
354369
355370 Returns:
356- copy of the current context
371+ Updated context.
357372 """
358373 # dispose of last argument
359374 should_propagate = ctx .function_propagate_policy
@@ -363,7 +378,7 @@ def _handle_argument_end(ctx: Context, is_comma=True) -> Context:
363378 ctx .current_argument_no ,
364379 last_arg = (not is_comma ))
365380
366- if should_propagate is True :
381+ if should_propagate :
367382 # add existing text array to processed buffer
368383 ctx .prev_arguments_text_array = util .safe_list_add (
369384 ctx .current_argument_text_array ,
@@ -383,20 +398,21 @@ def _handle_argument_end(ctx: Context, is_comma=True) -> Context:
383398
384399
385400def _handle_open_paren (ctx : Context , is_bracket = False ) -> Context :
386- """When encountering an open parenthesis, we halt current
401+ """Handle an open parenthesis in the expression.
402+
403+ When encountering an open parenthesis, we halt current
387404 argument processing up to the function name start, if any.
388405
389406 Args:
390- ctx: context of current function with index at open paren
407+ ctx: Context of current function with index at open paren.
391408 is_bracket: True if this is a bracket pseudo-function so
392409 that we don't search for a function identifier to
393410 precede it.
394411
395- Caution:
396- Make sure we are not at the end of the expression
397-
398412 Returns:
399- new context to process
413+ New context to process the nested function.
414+
415+ .. warning:: Make sure we are not at the end of the expression.
400416 """
401417 # current position is a (
402418 segment = ctx .expression [ctx .start_of_current_argument_processing : ctx .current_position ]
@@ -433,14 +449,16 @@ def _handle_open_paren(ctx: Context, is_bracket=False) -> Context:
433449
434450
435451def _get_function_name (msg : str ) -> str :
436- """
437- Assumes the string terminates with a ( but the ( is not
438- passed into the msg
439- Args:
440- msg:
452+ """Extract function name from a string.
441453
442- Returns: name of the function
454+ Assumes the string terminates with a '(' but the '(' is not
455+ passed into the msg.
443456
457+ Args:
458+ msg: String containing function name (without the opening parenthesis).
459+
460+ Returns:
461+ Name of the function.
444462 """
445463 res = re .findall (func_name , msg )
446464 assert len (res ) >= 1
@@ -449,6 +467,30 @@ def _get_function_name(msg: str) -> str:
449467
450468@dataclass (init = True , kw_only = True )
451469class Context :
470+ """Context for parsing expressions with nested function calls.
471+
472+ This dataclass tracks the state of expression parsing, including
473+ current position, function names, and argument processing.
474+
475+ Attributes:
476+ expression: The master expression we are working with.
477+ length: Total length of this expression.
478+ current_position: Current position in the expression.
479+ start_of_current_argument_processing: Where in the expression the first
480+ character (after previous comma) appears OR where we resumed
481+ processing for in the current argument.
482+ current_argument_text_array: Text of current argument (only append string
483+ when receiving values from function call return).
484+ current_function_name: Name of current function being parsed (None if
485+ just in a parenthesis or unknown function context).
486+ function_propagate_policy: Whether it is known that all arguments do or
487+ do not propagate. If unknown, set to None.
488+ current_argument_no: Which argument we are on, starting at 1.
489+ is_last_argument: Whether this is the last argument (only relevant for
490+ case statement).
491+ prev_arguments_text_array: Already pruned text from previous arguments
492+ (or None).
493+ """
452494 # The expression is the master expression we are working with
453495 expression : str
454496
0 commit comments