11"""Miscellaneous checkers"""
22
33import ast
4+ import re
45from dataclasses import dataclass
56from pathlib import Path
67from typing import TYPE_CHECKING
910from robot .errors import VariableError
1011from robot .parsing .model .blocks import For , TestCaseSection
1112from robot .parsing .model .statements import Arguments , KeywordCall , Teardown
12- from robot .utils import unescape
1313from robot .variables .search import search_variable
1414
1515from robocop .linter .diagnostics import Diagnostic
@@ -884,7 +884,7 @@ def visit_VariableSection(self, node): # noqa: N802
884884 if self .variables_expected_sign_type is None :
885885 return None
886886 for child in node .body :
887- if not isinstance (child , Variable ) or utils . get_errors ( child ) :
887+ if not isinstance (child , Variable ) or child . errors :
888888 continue
889889 var_token = child .get_token (Token .VARIABLE )
890890 self .check_assign_type (
@@ -940,7 +940,7 @@ def visit_KeywordSection(self, node) -> None: # noqa: N802
940940 visit_TestCaseSection = visit_KeywordSection # noqa: N815
941941
942942 def visit_Variable (self , node ) -> None : # noqa: N802
943- if utils . get_errors ( node ) :
943+ if node . errors :
944944 return
945945 if not node .value : # catch variable declaration without any value
946946 self .report (self .empty_variable , node = node , end_col = node .end_col_offset )
@@ -1008,7 +1008,7 @@ class IfChecker(VisitorChecker):
10081008 multiline_inline_if : MultilineInlineIfRule
10091009
10101010 def visit_TestCase (self , node ) -> None : # noqa: N802
1011- if utils . get_errors ( node ) :
1011+ if node . errors :
10121012 return
10131013 self .check_adjacent_ifs (node )
10141014
@@ -1194,7 +1194,7 @@ def __init__(self) -> None:
11941194 self .section_variables : dict [str , CachedVariable ] = {}
11951195
11961196 def visit_Variable (self , node ) -> None :
1197- if utils . get_errors ( node ) :
1197+ if node . errors :
11981198 return
11991199 var_token = node .get_token (Token .VARIABLE )
12001200 variable_match = search_variable (var_token .value , ignore_errors = True )
@@ -1209,6 +1209,9 @@ class UnusedVariablesChecker(VisitorChecker):
12091209 argument_overwritten_before_usage : arguments .ArgumentOverwrittenBeforeUsageRule
12101210 variable_overwritten_before_usage : variables .VariableOverwrittenBeforeUsageRule
12111211
1212+ _ESCAPED_VAR_PATTERN = re .compile (r"\$([A-Za-z_]\w*)" )
1213+ _VARIABLE_NAME_PATTERN = re .compile (r"\w+" )
1214+
12121215 def __init__ (self ) -> None :
12131216 self .arguments : dict [str , CachedVariable ] = {}
12141217 self .variables : list [dict [str , CachedVariable ]] = [
@@ -1301,20 +1304,24 @@ def add_argument(self, argument, normalized_name, token) -> None:
13011304
13021305 def parse_arguments (self , node ) -> None :
13031306 """Store arguments from [Arguments]. Ignore @{args} and &{kwargs}, strip default values."""
1304- if utils . get_errors ( node ) :
1307+ if node . errors :
13051308 return
13061309 for arg in node .get_tokens (Token .ARGUMENT ):
13071310 if arg .value [0 ] in ("@" , "&" ): # ignore *args and &kwargs
13081311 continue
1309- variable_match = search_variable (arg .value , ignore_errors = True )
1310- if variable_match .after :
1311- self .find_not_nested_variable (variable_match .after , is_var = False )
1312- name = utils .remove_variable_type_conversion (variable_match .base )
1313- normalized_name = utils .normalize_robot_name (name )
1314- self .add_argument (variable_match .base , normalized_name , token = arg )
1312+ arg_name , default_value = utils .split_argument_default_value (arg .value )
1313+ if default_value :
1314+ self .find_not_nested_variable (default_value , can_be_escaped = False )
1315+ base_name = arg_name [2 :- 1 ]
1316+ name = utils .remove_variable_type_conversion (base_name )
1317+ name = utils .normalize_robot_name (name )
1318+ self .add_argument (base_name , name , token = arg )
1319+ # ${test.kws[0].msgs[${index}]} FIXME
13151320
13161321 def parse_embedded_arguments (self , name_token ) -> None :
13171322 """Store embedded arguments from keyword name. Ignore embedded variables patterns (${var:pattern})."""
1323+ if "$" not in name_token .value :
1324+ return
13181325 try :
13191326 for token in name_token .tokenize_variables ():
13201327 if token .type == Token .VARIABLE :
@@ -1329,7 +1336,7 @@ def visit_If(self, node): # noqa: N802
13291336 return
13301337 self .branch_level += 1
13311338 for token in node .header .get_tokens (Token .ARGUMENT ):
1332- self .find_not_nested_variable (token .value , is_var = False )
1339+ self .find_not_nested_variable (token .value , can_be_escaped = True )
13331340 self .variables .append ({})
13341341 for item in node .body :
13351342 self .visit (item )
@@ -1352,7 +1359,7 @@ def visit_If(self, node): # noqa: N802
13521359
13531360 def visit_IfBranch (self , node ) -> None : # noqa: N802
13541361 for token in node .header .get_tokens (Token .ARGUMENT ):
1355- self .find_not_nested_variable (token .value , is_var = False )
1362+ self .find_not_nested_variable (token .value , can_be_escaped = True )
13561363 self .variables .append ({})
13571364 for child in node .body :
13581365 self .visit (child )
@@ -1377,7 +1384,7 @@ def add_variables_from_if_to_scope(self, if_variables: dict[str, CachedVariable]
13771384
13781385 def visit_LibraryImport (self , node ) -> None : # noqa: N802
13791386 for token in node .get_tokens (Token .NAME , Token .ARGUMENT ):
1380- self .find_not_nested_variable (token .value , is_var = False )
1387+ self .find_not_nested_variable (token .value , can_be_escaped = False )
13811388
13821389 visit_TestTags = visit_ForceTags = visit_Metadata = visit_DefaultTags = ( # noqa: N815
13831390 visit_Variable # noqa: N815
@@ -1425,9 +1432,9 @@ def visit_While(self, node): # noqa: N802
14251432 self .in_loop = True
14261433 self .used_in_scope .append (set ())
14271434 for token in node .header .get_tokens (Token .ARGUMENT ):
1428- self .find_not_nested_variable (token .value , is_var = False )
1435+ self .find_not_nested_variable (token .value , can_be_escaped = True )
14291436 if node .limit :
1430- self .find_not_nested_variable (node .limit , is_var = False )
1437+ self .find_not_nested_variable (node .limit , can_be_escaped = False )
14311438 self .generic_visit (node )
14321439 self .in_loop = False
14331440 self .revisit_variables_used_in_loop ()
@@ -1440,7 +1447,7 @@ def visit_For(self, node): # noqa: N802
14401447 self .used_in_scope .append (set ())
14411448 self .ignore_overwriting = True
14421449 for token in node .header .get_tokens (Token .ARGUMENT , "OPTION" ): # Token.Option does not exist for RF3 and RF4
1443- self .find_not_nested_variable (token .value , is_var = False )
1450+ self .find_not_nested_variable (token .value , can_be_escaped = False )
14441451 for token in node .header .get_tokens (Token .VARIABLE ):
14451452 self .handle_assign_variable (token , ignore_var_conversion = False )
14461453 self .generic_visit (node )
@@ -1471,7 +1478,7 @@ def visit_Try(self, node): # noqa: N802
14711478 self .variables .append ({})
14721479 # variables in EXCEPT ${error_pattern}
14731480 for token in try_branch .header .get_tokens (Token .ARGUMENT , Token .OPTION ):
1474- self .find_not_nested_variable (token .value , is_var = False )
1481+ self .find_not_nested_variable (token .value , can_be_escaped = True )
14751482 # except AS ${err}
14761483 if self .try_assign (try_branch ) is not None :
14771484 error_var = try_branch .header .get_token (Token .VARIABLE )
@@ -1499,27 +1506,29 @@ def visit_Try(self, node): # noqa: N802
14991506
15001507 def visit_Group (self , node ): # noqa: N802
15011508 for token in node .header .get_tokens (Token .ARGUMENT ):
1502- self .find_not_nested_variable (token .value , is_var = False )
1509+ self .find_not_nested_variable (token .value , can_be_escaped = True )
15031510 self .generic_visit (node )
15041511
15051512 def visit_KeywordCall (self , node ) -> None : # noqa: N802
1506- for token in node .get_tokens (Token .ARGUMENT , Token .KEYWORD ): # argument can be used in the keyword name
1507- self .find_not_nested_variable (token .value , is_var = False )
1513+ for token in node .get_tokens (Token .KEYWORD ): # argument can be used in the keyword name
1514+ self .find_not_nested_variable (token .value , can_be_escaped = False )
1515+ for token in node .get_tokens (Token .ARGUMENT ):
1516+ self .find_not_nested_variable (token .value , can_be_escaped = True )
15081517 for token in node .get_tokens (Token .ASSIGN ): # we first check args, then assign for used and then overwritten
15091518 self .handle_assign_variable (token )
15101519
15111520 def visit_Var (self , node ) -> None : # noqa: N802
15121521 if node .errors : # for example invalid variable definition like $var}
15131522 return
15141523 for arg in node .get_tokens (Token .ARGUMENT ):
1515- self .find_not_nested_variable (arg .value , is_var = False )
1524+ self .find_not_nested_variable (arg .value , can_be_escaped = True )
15161525 variable = node .get_token (Token .VARIABLE )
15171526 if variable and utils .is_var_scope_local (node ):
15181527 self .handle_assign_variable (variable )
15191528
15201529 def visit_TemplateArguments (self , node ) -> None : # noqa: N802
15211530 for argument in node .data_tokens :
1522- self .find_not_nested_variable (argument .value , is_var = False )
1531+ self .find_not_nested_variable (argument .value , can_be_escaped = False )
15231532
15241533 def handle_assign_variable (self , token , ignore_var_conversion : bool = True ) -> None :
15251534 """
@@ -1565,48 +1574,69 @@ def handle_assign_variable(self, token, ignore_var_conversion: bool = True) -> N
15651574 variable = CachedVariable (variable_match .name , token , is_used = False )
15661575 self .variables [- 1 ][normalized ] = variable
15671576
1568- def find_not_nested_variable (self , value , is_var ) -> None :
1577+ def find_not_nested_variable (self , value : str , can_be_escaped : bool ) -> None :
15691578 r"""
15701579 Find and process not nested variable.
15711580
1572- Search `value` string until there is ${variable} without other variables inside. Unescaped escaped syntax
1573- ($var or \\${var}). If a variable does exist in assign variables or arguments, it is removed to denote it was
1574- used.
1581+ Examples:
1582+ '${value}' -> value
1583+ ${value_${nested}} -> nested
1584+ 'String with ${var} and $escaped' -> var, escaped
1585+
1586+ Found variables are added to the scope.
1587+
15751588 """
1576- try :
1577- variables = list (VariableMatches (value ))
1578- except VariableError : # for example, ${variable which wasn't closed properly
1579- return
1580- if not variables :
1581- if is_var :
1582- self .update_used_variables (value )
1583- elif "$" in value :
1584- self .find_escaped_variables (value ) # $var
1585- if r"\${" in value : # \\${var}
1586- unescaped = unescape (value )
1587- self .find_not_nested_variable (unescaped , is_var = False )
1588- return
1589- replaced , after = "" , ""
1590- for match in variables :
1591- replaced += f"{ match .before } placeholder{ match .after } "
1592- if match .before and "$" not in match .before and is_var : # ${test.kws[0].msgs[${index}]}
1593- self .update_used_variables (match .before )
1594- # handle ${variable}[item][${syntax}]
1595- if match .base and match .base .startswith ("{" ) and match .base .endswith ("}" ): # inline val
1596- self .find_not_nested_variable (match .base [1 :- 1 ].strip (), is_var = False )
1589+ identifiers = set ("$@&%" )
1590+ n = len (value )
1591+ i = 0
1592+ full_match = False # whether string is a variable only
1593+ while True :
1594+ # find the next '{'
1595+ pos = value .find ("{" , i )
1596+ if pos == - 1 :
1597+ break
1598+ # must be preceded by an identifier char
1599+ if pos == 0 or value [pos - 1 ] not in identifiers :
1600+ i = pos + 1
1601+ continue
1602+ # found an identifier + '{' opening
1603+ start = pos + 1 # first char inside braces
1604+ depth = 1
1605+ j = start
1606+
1607+ while j < n :
1608+ # detect nested identifier + '{' (counts as increased nesting)
1609+ if value [j ] in identifiers and j + 1 < n and value [j + 1 ] == "{" :
1610+ depth += 1
1611+ j += 2
1612+ continue
1613+
1614+ if value [j ] == "}" :
1615+ depth -= 1
1616+ if depth == 0 :
1617+ # call with the content inside the outermost braces
1618+ self .update_used_variables (value [start :j ])
1619+ full_match = start == 2 and j == n - 1
1620+ i = j + 1
1621+ break
1622+ j += 1
15971623 else :
1598- self .find_not_nested_variable (match .base , is_var = True )
1599- for item in match .items :
1600- self .find_not_nested_variable (item , is_var = False )
1601- after = match .after
1602- self .find_escaped_variables (replaced )
1603- if after and "$" not in after and is_var : # ${test.kws[0].msgs[${index}]}
1604- self .update_used_variables (after )
1624+ # no matching closing brace found
1625+ break
1626+ # no need to search further if we matched fully ('${var}')
1627+ if not can_be_escaped or full_match :
1628+ return
1629+ self .find_escaped_variables (value )
16051630
16061631 def find_escaped_variables (self , value ) -> None :
16071632 """Find all $var escaped variables in the value string and process them."""
1608- for var in utils .find_escaped_variables (value ):
1609- self .update_used_variables (var )
1633+ # TODO: create iter_escaped_variables function
1634+ if "$" not in value :
1635+ return
1636+ for match in self ._ESCAPED_VAR_PATTERN .finditer (value ):
1637+ variable_name = match .group (1 )
1638+ if variable_name .isidentifier ():
1639+ self .update_used_variables (variable_name )
16101640
16111641 def update_used_variables (self , variable_name ) -> None :
16121642 """
@@ -1637,11 +1667,16 @@ def _set_variable_as_used(self, normalized_name: str, variable_scope: dict[str,
16371667 else :
16381668 self .search_by_tokenize (normalized_name , variable_scope )
16391669
1640- @staticmethod
1641- def search_by_tokenize (variable_name , variable_scope ) -> list [str ]:
1670+ def search_by_tokenize (self , variable_name , variable_scope ) -> list [str ]:
16421671 """Search variables in string by tokenizing variable name using Python ast."""
16431672 if not variable_scope :
16441673 return []
1674+ # there is no syntax like ${var * 2}
1675+ if self ._VARIABLE_NAME_PATTERN .fullmatch (variable_name ):
1676+ if variable_name in variable_scope :
1677+ variable_scope [variable_name ].is_used = True
1678+ return [variable_name ]
1679+ return []
16451680 found = []
16461681 for name in utils .get_variables_from_string (variable_name ):
16471682 if name in variable_scope :
@@ -1977,7 +2012,7 @@ def should_report_missing_type(self, var_name: str) -> bool:
19772012
19782013 def visit_Variable (self , node : Variable ) -> None : # noqa: N802
19792014 """Check variables in *** Variables *** section."""
1980- if utils . get_errors ( node ) :
2015+ if node . errors :
19812016 return
19822017 token = node .data_tokens [0 ]
19832018 if self .should_report_missing_type (token .value ):
@@ -2011,6 +2046,7 @@ def visit_Var(self, node: Var) -> None: # noqa: N802
20112046
20122047 def visit_KeywordCall (self , node : KeywordCall ) -> None : # noqa: N802
20132048 """Check assignment expressions (${var} = Keyword)."""
2049+ # TODO: we already search for variable in unused var checker - we can combine
20142050 for token in node .get_tokens (Token .ASSIGN ):
20152051 if self .should_report_missing_type (token .value ):
20162052 var_match = search_variable (token .value , ignore_errors = True )
0 commit comments