Skip to content

Commit 09fa74f

Browse files
committed
flow scanner engine update
1 parent b3a2c40 commit 09fa74f

File tree

21 files changed

+4127
-1654
lines changed

21 files changed

+4127
-1654
lines changed

packages/code-analyzer-flow-engine/FlowScanner/flow_parser/expression_parser.py

Lines changed: 101 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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
56
67
"""
@@ -38,12 +39,13 @@
3839

3940

4041
def _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)
120122
def _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

133135
def _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

137147
def 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

154165
def 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

173185
def 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

215227
def _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

233246
def _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

262275
def _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

347363
def _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

385400
def _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

435451
def _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)
451469
class 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

Comments
 (0)